From b9e42e97fd08f176d4106fce298b0b0be79041cd Mon Sep 17 00:00:00 2001 From: Viscerous Date: Mon, 15 Jun 2026 18:18:16 +0200 Subject: [PATCH 1/6] Depth-splice rigged attachment alpha into world alpha --- indra/newview/app_settings/settings.xml | 11 +++ indra/newview/lldrawpoolalpha.cpp | 107 +++++++++++++++++++----- indra/newview/lldrawpoolalpha.h | 9 +- indra/newview/llspatialpartition.h | 31 +++++++ indra/newview/llvoavatar.cpp | 21 +++-- indra/newview/pipeline.cpp | 33 +++++++- 6 files changed, 181 insertions(+), 31 deletions(-) diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index b1e161182e..e4853a7ea4 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -7848,6 +7848,17 @@ Value 0.5 + RenderInterleavedAlpha + + Comment + Depth-interleave rigged attachment alpha with the distance-sorted world alpha so transparent objects composite correctly both in front of and behind avatars. Disable to restore two-pass (all rigged first) order. + Persist + 1 + Type + Boolean + Value + 1 + RenderDebugGLSession Comment diff --git a/indra/newview/lldrawpoolalpha.cpp b/indra/newview/lldrawpoolalpha.cpp index 8755120920..d21e5db526 100644 --- a/indra/newview/lldrawpoolalpha.cpp +++ b/indra/newview/lldrawpoolalpha.cpp @@ -26,6 +26,8 @@ #include "llviewerprecompiledheaders.h" +#include + #include "lldrawpoolalpha.h" #include "llglheaders.h" @@ -198,14 +200,28 @@ void LLDrawPoolAlpha::renderPostDeferred(S32 pass) // already being setup for rendering LLGLSLShader::unbind(); - if (!LLPipeline::sRenderingHUDs) + static LLCachedControl interleaved_alpha(gSavedSettings, "RenderInterleavedAlpha", true); + + if (interleaved_alpha && !LLPipeline::sRenderingHUDs && + getType() == LLDrawPool::POOL_ALPHA_POST_WATER) { - // first pass, render rigged objects only and render to depth buffer - forwardRender(true); + // single pass: depth-interleave whole avatars (rigged alpha, drawn in + // attachment order with depth writes) with the distance-sorted alpha + // groups, so world alpha composites correctly both in front of and + // behind avatars. + forwardRender(false, true); } + else + { + if (!LLPipeline::sRenderingHUDs) + { + // first pass, render rigged objects only and render to depth buffer + forwardRender(true); + } - // second pass, regular forward alpha rendering - forwardRender(); + // second pass, regular forward alpha rendering + forwardRender(); + } // final pass, render to depth for depth of field effects if (!LLPipeline::sImpostorRender && LLPipeline::RenderDepthOfField && !gCubeSnapshot && !LLPipeline::sRenderingHUDs && getType() == LLDrawPool::POOL_ALPHA_POST_WATER) @@ -228,7 +244,7 @@ void LLDrawPoolAlpha::renderPostDeferred(S32 pass) } } -void LLDrawPoolAlpha::forwardRender(bool rigged) +void LLDrawPoolAlpha::forwardRender(bool rigged, bool merged) { gPipeline.enableLightsDynamic(); @@ -245,7 +261,8 @@ void LLDrawPoolAlpha::forwardRender(bool rigged) || getType() == LLDrawPoolAlpha::POOL_ALPHA_PRE_WATER; // needed for accurate water fog - LLGLDepthTest depth(GL_TRUE, write_depth ? GL_TRUE : GL_FALSE); + // in merged mode depth writes are decided per group inside renderAlpha + LLGLDepthTest depth(GL_TRUE, (write_depth && !merged) ? GL_TRUE : GL_FALSE); mColorSFactor = LLRender::BF_SOURCE_ALPHA; // } regular alpha blend mColorDFactor = LLRender::BF_ONE_MINUS_SOURCE_ALPHA; // } @@ -253,8 +270,11 @@ void LLDrawPoolAlpha::forwardRender(bool rigged) mAlphaDFactor = LLRender::BF_ONE_MINUS_SOURCE_ALPHA; // } gGL.blendFunc(mColorSFactor, mColorDFactor, mAlphaSFactor, mAlphaDFactor); - if (rigged && mType == LLDrawPool::POOL_ALPHA_POST_WATER) + if ((rigged || merged) && mType == LLDrawPool::POOL_ALPHA_POST_WATER) { // draw GLTF scene to depth buffer before rigged alpha + // (explicit depth-write scope: in merged mode the pass-wide depth state + // above has writes off) + LLGLDepthTest gltf_depth(GL_TRUE, GL_TRUE); LL::GLTFSceneManager::instance().render(false, false); LL::GLTFSceneManager::instance().render(false, true); LL::GLTFSceneManager::instance().render(false, false, true); @@ -263,11 +283,11 @@ void LLDrawPoolAlpha::forwardRender(bool rigged) // If the face is more than 90% transparent, then don't update the Depth buffer for Dof // We don't want the nearly invisible objects to cause of DoF effects - renderAlpha(getVertexDataMask() | LLVertexBuffer::MAP_TEXTURE_INDEX | LLVertexBuffer::MAP_TANGENT | LLVertexBuffer::MAP_TEXCOORD1 | LLVertexBuffer::MAP_TEXCOORD2, false, rigged); + renderAlpha(getVertexDataMask() | LLVertexBuffer::MAP_TEXTURE_INDEX | LLVertexBuffer::MAP_TANGENT | LLVertexBuffer::MAP_TEXCOORD1 | LLVertexBuffer::MAP_TEXCOORD2, false, rigged, merged); gGL.setColorMask(true, false); - if (!rigged && (LLPipeline::sRenderingHUDs || getType() == LLDrawPoolAlpha::POOL_ALPHA_POST_WATER)) + if ((!rigged || merged) && (LLPipeline::sRenderingHUDs || getType() == LLDrawPoolAlpha::POOL_ALPHA_POST_WATER)) { //render "highlight alpha" on final non-rigged pass for non-HUDs (HUDs only run pre-water alpha pass) // NOTE -- hacky call here protected by !rigged instead of alongside "forwardRender" // so renderDebugAlpha is executed while gls_pipeline_alpha and depth GL state @@ -561,7 +581,7 @@ void LLDrawPoolAlpha::renderRiggedPbrEmissives(std::vector& emissiv } } -void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, bool rigged) +void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, bool rigged, bool merged) { LL_PROFILE_ZONE_SCOPED_CATEGORY_DRAWPOOL; bool initialized_lighting = false; @@ -572,18 +592,21 @@ void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, bool rigged) const LLGLSLShader* lastAvatarShader = nullptr; bool skipLastSkin = false; - LLCullResult::sg_iterator begin; - LLCullResult::sg_iterator end; + LLCullResult::sg_iterator iter = nullptr; + LLCullResult::sg_iterator iter_end = nullptr; + LLCullResult::sg_iterator rigged_iter = nullptr; + LLCullResult::sg_iterator rigged_end = nullptr; - if (rigged) + if (merged || rigged) { - begin = gPipeline.beginRiggedAlphaGroups(); - end = gPipeline.endRiggedAlphaGroups(); + rigged_iter = gPipeline.beginRiggedAlphaGroups(); + rigged_end = gPipeline.endRiggedAlphaGroups(); } - else + + if (merged || !rigged) { - begin = gPipeline.beginAlphaGroups(); - end = gPipeline.endAlphaGroups(); + iter = gPipeline.beginAlphaGroups(); + iter_end = gPipeline.endAlphaGroups(); } LLEnvironment& env = LLEnvironment::instance(); @@ -596,10 +619,37 @@ void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, bool rigged) } - for (LLCullResult::sg_iterator i = begin; i != end; ++i) + // depth-write conditions that apply to every group in this pass; in merged + // mode, stamped rigged groups additionally write depth (see below) + const bool write_depth_always = LLDrawPoolWater::sSkipScreenCopy || + LLPipeline::sImpostorRenderAlphaDepthPass || + getType() == LLDrawPoolAlpha::POOL_ALPHA_PRE_WATER; + + while (iter != iter_end || rigged_iter != rigged_end) { LL_PROFILE_ZONE_NAMED_CATEGORY_DRAWPOOL("renderAlpha - group"); - LLSpatialGroup* group = *i; + + if (merged) + { // single back-to-front walk over both streams: take the farther of + // the two heads. All of an avatar's rigged groups share one depth + // (see LLSpatialBridge::mAvatarp), so each avatar's attachment-order + // run drains contiguously; on ties rigged draws first so world + // alpha at the same depth composites over it. + if (rigged_iter == rigged_end) + { + rigged = false; + } + else if (iter == iter_end) + { + rigged = true; + } + else + { + rigged = (*rigged_iter)->mDepth >= (*iter)->mDepth; + } + } + + LLSpatialGroup* group = rigged ? *rigged_iter++ : *iter++; llassert(group); llassert(group->getSpatialPartition()); @@ -628,6 +678,21 @@ void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, bool rigged) } } + // merged mode: rigged alpha writes depth so attachment-order + // layering holds and impostors/DoF still see it -- but only when + // the group carries an avatar stamp (mAvatarp, see + // LLSpatialBridge). An unstamped group (e.g. animesh) has no + // defined position in the rigged order and must not be allowed to + // depth-reject geometry behind it (it still blends; it just can't + // erase). Non-merged passes keep the pass-wide depth state set by + // the caller. + std::optional depth_state; + if (merged) + { + bool write_depth = write_depth_always || (rigged && group->mAvatarp != nullptr); + depth_state.emplace(GL_TRUE, write_depth ? GL_TRUE : GL_FALSE); + } + static std::vector emissives; static std::vector rigged_emissives; static std::vector pbr_emissives; diff --git a/indra/newview/lldrawpoolalpha.h b/indra/newview/lldrawpoolalpha.h index 25044beda0..c92c752f35 100644 --- a/indra/newview/lldrawpoolalpha.h +++ b/indra/newview/lldrawpoolalpha.h @@ -58,13 +58,18 @@ class LLDrawPoolAlpha final: public LLRenderPass /*virtual*/ void renderPostDeferred(S32 pass); /*virtual*/ S32 getNumPasses() { return 1; } - void forwardRender(bool write_depth = false); + // rigged: render the rigged alpha groups (depth-writing) instead of the + // distance-sorted alpha groups + // merged: render both streams in a single back-to-front walk, splicing each + // avatar's rigged run in at the avatar's depth; depth writes are + // then decided per group inside renderAlpha + void forwardRender(bool rigged = false, bool merged = false); /*virtual*/ void prerender(); void renderDebugAlpha(); void renderGroupAlpha(LLSpatialGroup* group, U32 type, U32 mask, bool texture = true); - void renderAlpha(U32 mask, bool depth_only = false, bool rigged = false); + void renderAlpha(U32 mask, bool depth_only = false, bool rigged = false, bool merged = false); void renderAlphaHighlight(); static bool sShowDebugAlpha; diff --git a/indra/newview/llspatialpartition.h b/indra/newview/llspatialpartition.h index 4b312b1597..88d68442c4 100644 --- a/indra/newview/llspatialpartition.h +++ b/indra/newview/llspatialpartition.h @@ -249,6 +249,28 @@ class LLSpatialGroup : public LLOcclusionCullingGroup } }; + struct CompareDepthRenderOrder + { + bool operator()(const LLSpatialGroup* const& lhs, const LLSpatialGroup* const& rhs) + { + // farther avatars draw first so rigged alpha can be depth-interleaved + // with the distance-sorted alpha groups; all of an avatar's groups + // share one depth (see LLSpatialBridge::mAvatarp), keeping each + // avatar's run contiguous and in attachment render order + if (lhs->mDepth != rhs->mDepth) + { + return lhs->mDepth > rhs->mDepth; + } + + if (lhs->mAvatarp != rhs->mAvatarp) + { + return lhs->mAvatarp < rhs->mAvatarp; + } + + return lhs->mRenderOrder > rhs->mRenderOrder; + } + }; + typedef enum { GEOM_DIRTY = LLViewerOctreeGroup::INVALID_STATE, @@ -458,6 +480,15 @@ class LLSpatialBridge : public LLDrawable, public LLSpatialPartition //transform agent space bounding box into this Spatial Bridge's coordinate frame void transformExtents(const LLVector4a* src, LLVector4a* dst); LLDrawable* mDrawable; + + // rigged alpha draw-order stamp, set per detailed update by + // LLVOAvatar::idleUpdateMisc -- one stamp per attachment, however many + // child prims/groups the linkset spans. LLPipeline::postSort fans it out + // to each of this bridge's visible rigged alpha groups. Raw pointer is + // only ever compared, never dereferenced (avatar may die first). + LLVOAvatar* mAvatarp = nullptr; + U32 mRenderOrder = 0; + F32 mDepth = 0.f; }; class LLCullResult diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index cd71a2c660..b44edd191a 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -3025,6 +3025,13 @@ void LLVOAvatar::idleUpdateMisc(bool detailed_update) if (detailed_update) { U32 draw_order = 0; + // camera-axis depth of this avatar; stamped onto every rigged + // attachment bridge below so the rigged alpha groups can be + // depth-interleaved with the distance-sorted alpha groups while each + // avatar's attachment-order run stays contiguous + // (LLSpatialGroup::CompareDepthRenderOrder, LLDrawPoolAlpha::renderAlpha) + LLViewerCamera* camera = LLViewerCamera::getInstance(); + F32 rigged_depth = (getRenderPosition() - camera->getOrigin()) * camera->getAtAxis(); bool attachment_selected = LLSelectMgr::getInstance()->getSelection()->getObjectCount() > 0 && LLSelectMgr::getInstance()->getSelection()->isAttachment(); for (const auto& [attachment_point_id, attachment] : mAttachmentPoints) { @@ -3086,12 +3093,14 @@ void LLVOAvatar::idleUpdateMisc(bool detailed_update) bridge->updateMove(); bridge->setState(LLDrawable::EARLY_MOVE); - LLSpatialGroup* group = attached_object->mDrawable->getSpatialGroup(); - if (group) - { //set draw order of group - group->mAvatarp = this; - group->mRenderOrder = draw_order++; - } + //set draw order of the attachment; LLPipeline::postSort + //fans the stamp out to each of the bridge's visible + //rigged alpha groups, so the whole linkset sorts with + //this avatar in attachment order no matter how its + //prims bin into the bridge octree + bridge->mAvatarp = this; + bridge->mRenderOrder = draw_order++; + bridge->mDepth = rigged_depth; } } diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 4ef88f5deb..49062cff3d 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -3591,6 +3591,8 @@ void LLPipeline::postSort(LLCamera &camera) LL_PUSH_CALLSTACKS(); + static LLCachedControl interleaved_alpha(gSavedSettings, "RenderInterleavedAlpha", true); + // build render map for (LLCullResult::sg_iterator i = sCull->beginVisibleGroups(); i != sCull->endVisibleGroups(); ++i) { @@ -3663,6 +3665,23 @@ void LLPipeline::postSort(LLCamera &camera) if (rigged_alpha != group->mDrawMap.end()) { // store rigged alpha groups for LLDrawPoolAlpha prepass (skip distance update, rigged attachments use depth buffer) + // fan the attachment's draw-order stamp (LLVOAvatar::idleUpdateMisc) + // out from the bridge to every visible group of the linkset, + // however its prims bin into the bridge octree. The depth copy + // gives all of an avatar's groups one shared depth -- what lets + // the interleaved walk treat the avatar as a single contiguous + // run -- and is gated so the legacy path keeps bounds-front depths. + LLSpatialBridge* stamp_bridge = group->getSpatialPartition()->asBridge(); + if (stamp_bridge && stamp_bridge->mAvatarp) + { + group->mAvatarp = stamp_bridge->mAvatarp; + group->mRenderOrder = stamp_bridge->mRenderOrder; + if (interleaved_alpha) + { + group->mDepth = stamp_bridge->mDepth; + } + } + if (hasRenderType(LLDrawPool::POOL_ALPHA)) { sCull->pushRiggedAlphaGroup(group); @@ -3707,8 +3726,18 @@ void LLPipeline::postSort(LLCamera &camera) // order alpha groups by distance std::sort(sCull->beginAlphaGroups(), sCull->endAlphaGroups(), LLSpatialGroup::CompareDepthGreater()); - // order rigged alpha groups by avatar attachment order - std::sort(sCull->beginRiggedAlphaGroups(), sCull->endRiggedAlphaGroups(), LLSpatialGroup::CompareRenderOrder()); + if (interleaved_alpha) + { + // order rigged alpha groups by avatar depth, then attachment order, + // so LLDrawPoolAlpha can depth-interleave whole avatars with the + // distance-sorted alpha groups above + std::sort(sCull->beginRiggedAlphaGroups(), sCull->endRiggedAlphaGroups(), LLSpatialGroup::CompareDepthRenderOrder()); + } + else + { + // order rigged alpha groups by avatar attachment order + std::sort(sCull->beginRiggedAlphaGroups(), sCull->endRiggedAlphaGroups(), LLSpatialGroup::CompareRenderOrder()); + } } LL_PUSH_CALLSTACKS(); From ae9851494da61dfa9f08f7f59d9dc233f3f69b22 Mon Sep 17 00:00:00 2001 From: Viscerous Date: Thu, 9 Jul 2026 18:11:47 +0200 Subject: [PATCH 2/6] Interleaved alpha: stamp avatar depth from animated extents --- indra/newview/lldrawpoolalpha.cpp | 13 +++++++++++-- indra/newview/llvoavatar.cpp | 25 +++++++++++++++++++------ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/indra/newview/lldrawpoolalpha.cpp b/indra/newview/lldrawpoolalpha.cpp index d21e5db526..6da78da231 100644 --- a/indra/newview/lldrawpoolalpha.cpp +++ b/indra/newview/lldrawpoolalpha.cpp @@ -625,6 +625,12 @@ void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, bool rigged, bool m LLPipeline::sImpostorRenderAlphaDepthPass || getType() == LLDrawPoolAlpha::POOL_ALPHA_PRE_WATER; + // merged mode: per-group depth-write guard, re-emplaced only when the write + // state changes so a run of same-state groups pays one glDepthMask, not one + // per group (see below) + std::optional depth_state; + bool depth_state_writes = false; + while (iter != iter_end || rigged_iter != rigged_end) { LL_PROFILE_ZONE_NAMED_CATEGORY_DRAWPOOL("renderAlpha - group"); @@ -686,11 +692,14 @@ void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, bool rigged, bool m // depth-reject geometry behind it (it still blends; it just can't // erase). Non-merged passes keep the pass-wide depth state set by // the caller. - std::optional depth_state; if (merged) { bool write_depth = write_depth_always || (rigged && group->mAvatarp != nullptr); - depth_state.emplace(GL_TRUE, write_depth ? GL_TRUE : GL_FALSE); + if (!depth_state || write_depth != depth_state_writes) + { + depth_state.emplace(GL_TRUE, write_depth ? GL_TRUE : GL_FALSE); + depth_state_writes = write_depth; + } } static std::vector emissives; diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index b44edd191a..4fb4672ca4 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -3025,13 +3025,26 @@ void LLVOAvatar::idleUpdateMisc(bool detailed_update) if (detailed_update) { U32 draw_order = 0; - // camera-axis depth of this avatar; stamped onto every rigged - // attachment bridge below so the rigged alpha groups can be - // depth-interleaved with the distance-sorted alpha groups while each - // avatar's attachment-order run stays contiguous - // (LLSpatialGroup::CompareDepthRenderOrder, LLDrawPoolAlpha::renderAlpha) + // camera-axis depth of this avatar, stamped onto every rigged attachment + // bridge below so its alpha interleaves with world alpha + // (LLDrawPoolAlpha::renderAlpha). From the animated extents, so avatars + // sharing a sit target still order deterministically; pulled a quarter + // of the bounds toward the camera to match the metric + // LLSpatialPartition::calcDistance stamps onto world alpha groups. LLViewerCamera* camera = LLViewerCamera::getInstance(); - F32 rigged_depth = (getRenderPosition() - camera->getOrigin()) * camera->getAtAxis(); + const LLVector3& at_axis = camera->getAtAxis(); + F32 rigged_depth; + if (mLastAnimExtents[0] == LLVector3() || mLastAnimExtents[1] == LLVector3()) + { // extents not computed yet + rigged_depth = (getRenderPosition() - camera->getOrigin()) * at_axis; + } + else + { + const LLVector3 center = (mLastAnimExtents[0] + mLastAnimExtents[1]) * 0.5f; + const LLVector3 half = (mLastAnimExtents[1] - mLastAnimExtents[0]) * 0.5f; + rigged_depth = (center - camera->getOrigin()) * at_axis - + 0.25f * (half * at_axis.scaledVec(at_axis)); + } bool attachment_selected = LLSelectMgr::getInstance()->getSelection()->getObjectCount() > 0 && LLSelectMgr::getInstance()->getSelection()->isAttachment(); for (const auto& [attachment_point_id, attachment] : mAttachmentPoints) { From be9c16ca8470780df5873bc6eb2d70157df9fdb4 Mon Sep 17 00:00:00 2001 From: Viscerous Date: Fri, 10 Jul 2026 11:44:23 +0200 Subject: [PATCH 3/6] Include standalone animesh in interleaved rigged alpha sorting pass --- indra/newview/llcontrolavatar.cpp | 17 ++++++++++++ indra/newview/lldrawpoolalpha.cpp | 5 ++-- indra/newview/llspatialpartition.h | 11 ++++---- indra/newview/llvoavatar.cpp | 42 ++++++++++++++++-------------- indra/newview/llvoavatar.h | 1 + 5 files changed, 49 insertions(+), 27 deletions(-) diff --git a/indra/newview/llcontrolavatar.cpp b/indra/newview/llcontrolavatar.cpp index 660fb1b41a..8983947fac 100644 --- a/indra/newview/llcontrolavatar.cpp +++ b/indra/newview/llcontrolavatar.cpp @@ -366,6 +366,23 @@ void LLControlAvatar::idleUpdate(LLAgent &agent, const F64 &time) else { LLVOAvatar::idleUpdate(agent,time); + + // stamp the rigged alpha draw order onto the animesh bridge, same as + // LLVOAvatar::idleUpdateMisc does for attachment bridges, worn animesh + // is skipped: the wearer's attachment loop stamps this same bridge + // twith he wearer's depth and attachment order, keeping it inside + // the wearer's contiguous run. Fetched from the drawable, not + // mControlAVBridge, which can go stale (see markForDeath). + if (mRootVolp && mRootVolp->mDrawable && !getAttachedAvatar()) + { + LLSpatialBridge* bridge = mRootVolp->mDrawable->getSpatialBridge(); + if (bridge) + { + bridge->mAvatarp = this; + bridge->mRenderOrder = 0; + bridge->mDepth = calcRiggedAlphaDepth(); + } + } } } diff --git a/indra/newview/lldrawpoolalpha.cpp b/indra/newview/lldrawpoolalpha.cpp index 6da78da231..e425cb7898 100644 --- a/indra/newview/lldrawpoolalpha.cpp +++ b/indra/newview/lldrawpoolalpha.cpp @@ -686,8 +686,9 @@ void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, bool rigged, bool m // merged mode: rigged alpha writes depth so attachment-order // layering holds and impostors/DoF still see it -- but only when - // the group carries an avatar stamp (mAvatarp, see - // LLSpatialBridge). An unstamped group (e.g. animesh) has no + // the group carries an avatar stamp (mAvatarp, stamped for + // attachments by LLVOAvatar::idleUpdateMisc and for animesh by + // LLControlAvatar::idleUpdate). A not-yet-stamped group has no // defined position in the rigged order and must not be allowed to // depth-reject geometry behind it (it still blends; it just can't // erase). Non-merged passes keep the pass-wide depth state set by diff --git a/indra/newview/llspatialpartition.h b/indra/newview/llspatialpartition.h index 88d68442c4..ca11cf1d10 100644 --- a/indra/newview/llspatialpartition.h +++ b/indra/newview/llspatialpartition.h @@ -481,11 +481,12 @@ class LLSpatialBridge : public LLDrawable, public LLSpatialPartition void transformExtents(const LLVector4a* src, LLVector4a* dst); LLDrawable* mDrawable; - // rigged alpha draw-order stamp, set per detailed update by - // LLVOAvatar::idleUpdateMisc -- one stamp per attachment, however many - // child prims/groups the linkset spans. LLPipeline::postSort fans it out - // to each of this bridge's visible rigged alpha groups. Raw pointer is - // only ever compared, never dereferenced (avatar may die first). + // rigged alpha draw-order stamp -- one stamp per attachment (set per + // detailed update by LLVOAvatar::idleUpdateMisc) or per animesh (set per + // frame by LLControlAvatar::idleUpdate), however many child prims/groups + // the linkset spans. LLPipeline::postSort fans it out to each of this + // bridge's visible rigged alpha groups. Raw pointer is only ever + // compared, never dereferenced (avatar may die first). LLVOAvatar* mAvatarp = nullptr; U32 mRenderOrder = 0; F32 mDepth = 0.f; diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index 4fb4672ca4..a79057837b 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -3008,6 +3008,27 @@ static void override_bbox(LLDrawable* drawable, LLVector4a* extents) drawable->movePartition(); } +// camera-axis depth of this avatar, stamped onto every rigged alpha bridge +// (attachments below, animesh in LLControlAvatar::idleUpdate) so its alpha +// interleaves with world alpha (LLDrawPoolAlpha::renderAlpha). From the +// animated extents, so avatars sharing a sit target still order +// deterministically; pulled a quarter of the bounds toward the camera to match +// the metric LLSpatialPartition::calcDistance stamps onto world alpha groups. +F32 LLVOAvatar::calcRiggedAlphaDepth() const +{ + LLViewerCamera* camera = LLViewerCamera::getInstance(); + const LLVector3& at_axis = camera->getAtAxis(); + if (mLastAnimExtents[0] == LLVector3() || mLastAnimExtents[1] == LLVector3()) + { // extents not computed yet + return (getRenderPosition() - camera->getOrigin()) * at_axis; + } + + const LLVector3 center = (mLastAnimExtents[0] + mLastAnimExtents[1]) * 0.5f; + const LLVector3 half = (mLastAnimExtents[1] - mLastAnimExtents[0]) * 0.5f; + return (center - camera->getOrigin()) * at_axis - + 0.25f * (half * at_axis.scaledVec(at_axis)); +} + void LLVOAvatar::idleUpdateMisc(bool detailed_update) { LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR; @@ -3025,26 +3046,7 @@ void LLVOAvatar::idleUpdateMisc(bool detailed_update) if (detailed_update) { U32 draw_order = 0; - // camera-axis depth of this avatar, stamped onto every rigged attachment - // bridge below so its alpha interleaves with world alpha - // (LLDrawPoolAlpha::renderAlpha). From the animated extents, so avatars - // sharing a sit target still order deterministically; pulled a quarter - // of the bounds toward the camera to match the metric - // LLSpatialPartition::calcDistance stamps onto world alpha groups. - LLViewerCamera* camera = LLViewerCamera::getInstance(); - const LLVector3& at_axis = camera->getAtAxis(); - F32 rigged_depth; - if (mLastAnimExtents[0] == LLVector3() || mLastAnimExtents[1] == LLVector3()) - { // extents not computed yet - rigged_depth = (getRenderPosition() - camera->getOrigin()) * at_axis; - } - else - { - const LLVector3 center = (mLastAnimExtents[0] + mLastAnimExtents[1]) * 0.5f; - const LLVector3 half = (mLastAnimExtents[1] - mLastAnimExtents[0]) * 0.5f; - rigged_depth = (center - camera->getOrigin()) * at_axis - - 0.25f * (half * at_axis.scaledVec(at_axis)); - } + const F32 rigged_depth = calcRiggedAlphaDepth(); bool attachment_selected = LLSelectMgr::getInstance()->getSelection()->getObjectCount() > 0 && LLSelectMgr::getInstance()->getSelection()->isAttachment(); for (const auto& [attachment_point_id, attachment] : mAttachmentPoints) { diff --git a/indra/newview/llvoavatar.h b/indra/newview/llvoavatar.h index 3f33dc54b8..6bf975a202 100644 --- a/indra/newview/llvoavatar.h +++ b/indra/newview/llvoavatar.h @@ -282,6 +282,7 @@ class LLVOAvatar : void updateRootPositionAndRotation(LLAgent &agent, F32 speed, bool was_sit_ground_constrained); void idleUpdateVoiceVisualizer(bool voice_enabled, const LLVector3 &position); + F32 calcRiggedAlphaDepth() const; void idleUpdateMisc(bool detailed_update); virtual void idleUpdateAppearanceAnimation(); void idleUpdateLipSync(bool voice_enabled); From 95ae0513880b7a322b1d5c4a2f17fb0ad5499c2c Mon Sep 17 00:00:00 2001 From: Viscerous Date: Sat, 11 Jul 2026 10:34:55 +0200 Subject: [PATCH 4/6] Interleaved alpha: split the rigged sort key out of group depth --- indra/newview/llcontrolavatar.cpp | 2 +- indra/newview/lldrawpoolalpha.cpp | 38 +++++++++++++++++------------- indra/newview/lldrawpoolalpha.h | 18 ++++++++------ indra/newview/llspatialpartition.h | 29 ++++++++++++++++------- indra/newview/llvoavatar.cpp | 6 +++-- indra/newview/pipeline.cpp | 12 ++++------ 6 files changed, 62 insertions(+), 43 deletions(-) diff --git a/indra/newview/llcontrolavatar.cpp b/indra/newview/llcontrolavatar.cpp index 8983947fac..d1ffd18744 100644 --- a/indra/newview/llcontrolavatar.cpp +++ b/indra/newview/llcontrolavatar.cpp @@ -380,7 +380,7 @@ void LLControlAvatar::idleUpdate(LLAgent &agent, const F64 &time) { bridge->mAvatarp = this; bridge->mRenderOrder = 0; - bridge->mDepth = calcRiggedAlphaDepth(); + bridge->mAvatarDepth = calcRiggedAlphaDepth(); } } } diff --git a/indra/newview/lldrawpoolalpha.cpp b/indra/newview/lldrawpoolalpha.cpp index e425cb7898..e97355b0d0 100644 --- a/indra/newview/lldrawpoolalpha.cpp +++ b/indra/newview/lldrawpoolalpha.cpp @@ -209,14 +209,14 @@ void LLDrawPoolAlpha::renderPostDeferred(S32 pass) // attachment order with depth writes) with the distance-sorted alpha // groups, so world alpha composites correctly both in front of and // behind avatars. - forwardRender(false, true); + forwardRender(EAlphaStream::INTERLEAVED); } else { if (!LLPipeline::sRenderingHUDs) { // first pass, render rigged objects only and render to depth buffer - forwardRender(true); + forwardRender(EAlphaStream::RIGGED); } // second pass, regular forward alpha rendering @@ -244,7 +244,7 @@ void LLDrawPoolAlpha::renderPostDeferred(S32 pass) } } -void LLDrawPoolAlpha::forwardRender(bool rigged, bool merged) +void LLDrawPoolAlpha::forwardRender(EAlphaStream stream) { gPipeline.enableLightsDynamic(); @@ -253,7 +253,7 @@ void LLDrawPoolAlpha::forwardRender(bool rigged, bool merged) //enable writing to alpha for emissive effects gGL.setColorMask(true, true); - bool write_depth = rigged || + bool write_depth = stream == EAlphaStream::RIGGED || LLDrawPoolWater::sSkipScreenCopy // we want depth written so that rendered alpha will // contribute to the alpha mask used for impostors @@ -261,8 +261,8 @@ void LLDrawPoolAlpha::forwardRender(bool rigged, bool merged) || getType() == LLDrawPoolAlpha::POOL_ALPHA_PRE_WATER; // needed for accurate water fog - // in merged mode depth writes are decided per group inside renderAlpha - LLGLDepthTest depth(GL_TRUE, (write_depth && !merged) ? GL_TRUE : GL_FALSE); + // in interleaved mode depth writes are decided per group inside renderAlpha + LLGLDepthTest depth(GL_TRUE, (write_depth && stream != EAlphaStream::INTERLEAVED) ? GL_TRUE : GL_FALSE); mColorSFactor = LLRender::BF_SOURCE_ALPHA; // } regular alpha blend mColorDFactor = LLRender::BF_ONE_MINUS_SOURCE_ALPHA; // } @@ -270,10 +270,10 @@ void LLDrawPoolAlpha::forwardRender(bool rigged, bool merged) mAlphaDFactor = LLRender::BF_ONE_MINUS_SOURCE_ALPHA; // } gGL.blendFunc(mColorSFactor, mColorDFactor, mAlphaSFactor, mAlphaDFactor); - if ((rigged || merged) && mType == LLDrawPool::POOL_ALPHA_POST_WATER) + if (stream != EAlphaStream::WORLD && mType == LLDrawPool::POOL_ALPHA_POST_WATER) { // draw GLTF scene to depth buffer before rigged alpha - // (explicit depth-write scope: in merged mode the pass-wide depth state - // above has writes off) + // (explicit depth-write scope: in interleaved mode the pass-wide depth + // state above has writes off) LLGLDepthTest gltf_depth(GL_TRUE, GL_TRUE); LL::GLTFSceneManager::instance().render(false, false); LL::GLTFSceneManager::instance().render(false, true); @@ -283,11 +283,11 @@ void LLDrawPoolAlpha::forwardRender(bool rigged, bool merged) // If the face is more than 90% transparent, then don't update the Depth buffer for Dof // We don't want the nearly invisible objects to cause of DoF effects - renderAlpha(getVertexDataMask() | LLVertexBuffer::MAP_TEXTURE_INDEX | LLVertexBuffer::MAP_TANGENT | LLVertexBuffer::MAP_TEXCOORD1 | LLVertexBuffer::MAP_TEXCOORD2, false, rigged, merged); + renderAlpha(getVertexDataMask() | LLVertexBuffer::MAP_TEXTURE_INDEX | LLVertexBuffer::MAP_TANGENT | LLVertexBuffer::MAP_TEXCOORD1 | LLVertexBuffer::MAP_TEXCOORD2, false, stream); gGL.setColorMask(true, false); - if ((!rigged || merged) && (LLPipeline::sRenderingHUDs || getType() == LLDrawPoolAlpha::POOL_ALPHA_POST_WATER)) + if (stream != EAlphaStream::RIGGED && (LLPipeline::sRenderingHUDs || getType() == LLDrawPoolAlpha::POOL_ALPHA_POST_WATER)) { //render "highlight alpha" on final non-rigged pass for non-HUDs (HUDs only run pre-water alpha pass) // NOTE -- hacky call here protected by !rigged instead of alongside "forwardRender" // so renderDebugAlpha is executed while gls_pipeline_alpha and depth GL state @@ -581,9 +581,13 @@ void LLDrawPoolAlpha::renderRiggedPbrEmissives(std::vector& emissiv } } -void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, bool rigged, bool merged) +void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, EAlphaStream stream) { LL_PROFILE_ZONE_SCOPED_CATEGORY_DRAWPOOL; + const bool merged = stream == EAlphaStream::INTERLEAVED; + // stream of the current group; flips per group in the interleaved walk + bool rigged = stream == EAlphaStream::RIGGED; + bool initialized_lighting = false; bool light_enabled = true; @@ -637,10 +641,10 @@ void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, bool rigged, bool m if (merged) { // single back-to-front walk over both streams: take the farther of - // the two heads. All of an avatar's rigged groups share one depth - // (see LLSpatialBridge::mAvatarp), so each avatar's attachment-order - // run drains contiguously; on ties rigged draws first so world - // alpha at the same depth composites over it. + // the two heads. All of an avatar's rigged groups share one avatar + // depth (see LLSpatialBridge::mAvatarp), so each avatar's + // attachment-order run drains contiguously; on ties rigged draws + // first so world alpha at the same depth composites over it. if (rigged_iter == rigged_end) { rigged = false; @@ -651,7 +655,7 @@ void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, bool rigged, bool m } else { - rigged = (*rigged_iter)->mDepth >= (*iter)->mDepth; + rigged = (*rigged_iter)->mAvatarDepth >= (*iter)->mDepth; } } diff --git a/indra/newview/lldrawpoolalpha.h b/indra/newview/lldrawpoolalpha.h index c92c752f35..571e3f1555 100644 --- a/indra/newview/lldrawpoolalpha.h +++ b/indra/newview/lldrawpoolalpha.h @@ -58,18 +58,22 @@ class LLDrawPoolAlpha final: public LLRenderPass /*virtual*/ void renderPostDeferred(S32 pass); /*virtual*/ S32 getNumPasses() { return 1; } - // rigged: render the rigged alpha groups (depth-writing) instead of the - // distance-sorted alpha groups - // merged: render both streams in a single back-to-front walk, splicing each - // avatar's rigged run in at the avatar's depth; depth writes are - // then decided per group inside renderAlpha - void forwardRender(bool rigged = false, bool merged = false); + // which alpha stream(s) a pass draws + enum class EAlphaStream + { + WORLD, // the distance-sorted alpha groups + RIGGED, // the rigged alpha groups only (depth-writing prepass) + INTERLEAVED // both streams in one back-to-front walk; depth writes + // decided per group inside renderAlpha + }; + + void forwardRender(EAlphaStream stream = EAlphaStream::WORLD); /*virtual*/ void prerender(); void renderDebugAlpha(); void renderGroupAlpha(LLSpatialGroup* group, U32 type, U32 mask, bool texture = true); - void renderAlpha(U32 mask, bool depth_only = false, bool rigged = false, bool merged = false); + void renderAlpha(U32 mask, bool depth_only = false, EAlphaStream stream = EAlphaStream::WORLD); void renderAlphaHighlight(); static bool sShowDebugAlpha; diff --git a/indra/newview/llspatialpartition.h b/indra/newview/llspatialpartition.h index ca11cf1d10..2206ff0c03 100644 --- a/indra/newview/llspatialpartition.h +++ b/indra/newview/llspatialpartition.h @@ -43,6 +43,7 @@ #include "llvoavatar.h" #include "llfetchedgltfmaterial.h" +#include #include #include @@ -242,7 +243,8 @@ class LLSpatialGroup : public LLOcclusionCullingGroup { if (lhs->mAvatarp != rhs->mAvatarp) { - return lhs->mAvatarp < rhs->mAvatarp; + // std::less: raw < on unrelated pointers is unspecified + return std::less()(lhs->mAvatarp, rhs->mAvatarp); } return lhs->mRenderOrder > rhs->mRenderOrder; @@ -255,19 +257,26 @@ class LLSpatialGroup : public LLOcclusionCullingGroup { // farther avatars draw first so rigged alpha can be depth-interleaved // with the distance-sorted alpha groups; all of an avatar's groups - // share one depth (see LLSpatialBridge::mAvatarp), keeping each - // avatar's run contiguous and in attachment render order - if (lhs->mDepth != rhs->mDepth) + // share one avatar depth (see LLSpatialBridge::mAvatarp), keeping + // each avatar's run contiguous and in attachment render order + if (lhs->mAvatarDepth != rhs->mAvatarDepth) { - return lhs->mDepth > rhs->mDepth; + return lhs->mAvatarDepth > rhs->mAvatarDepth; } if (lhs->mAvatarp != rhs->mAvatarp) { - return lhs->mAvatarp < rhs->mAvatarp; + return std::less()(lhs->mAvatarp, rhs->mAvatarp); } - return lhs->mRenderOrder > rhs->mRenderOrder; + if (lhs->mRenderOrder != rhs->mRenderOrder) + { + return lhs->mRenderOrder > rhs->mRenderOrder; + } + + // bounds depth keeps groups of one attachment (which tie on all + // of the above) in a stable back-to-front order + return lhs->mDepth > rhs->mDepth; } }; @@ -373,6 +382,10 @@ class LLSpatialGroup : public LLOcclusionCullingGroup //used by LLVOAVatar to set render order in alpha draw pool to preserve legacy render order behavior LLVOAvatar* mAvatarp = nullptr; U32 mRenderOrder = 0; + // rigged alpha sort key (LLSpatialBridge::mAvatarDepth, fanned out in + // LLPipeline::postSort); separate from mDepth so mixed groups keep their + // bounds depth in the world-alpha sort + F32 mAvatarDepth = 0.f; // Reflection Probe associated with this node (if any) LLPointer mReflectionProbe = nullptr; } LL_ALIGN_POSTFIX(16); @@ -489,7 +502,7 @@ class LLSpatialBridge : public LLDrawable, public LLSpatialPartition // compared, never dereferenced (avatar may die first). LLVOAvatar* mAvatarp = nullptr; U32 mRenderOrder = 0; - F32 mDepth = 0.f; + F32 mAvatarDepth = 0.f; }; class LLCullResult diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index a79057837b..e7199a63bb 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -3112,10 +3112,12 @@ void LLVOAvatar::idleUpdateMisc(bool detailed_update) //fans the stamp out to each of the bridge's visible //rigged alpha groups, so the whole linkset sorts with //this avatar in attachment order no matter how its - //prims bin into the bridge octree + //prims bin into the bridge octree. Staleness-safe under + //detailed_update: mUpdatePeriod > 1 implies isImpostor(), + //whose attachments never enter the live alpha streams bridge->mAvatarp = this; bridge->mRenderOrder = draw_order++; - bridge->mDepth = rigged_depth; + bridge->mAvatarDepth = rigged_depth; } } diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 49062cff3d..5f639a959c 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -3667,19 +3667,15 @@ void LLPipeline::postSort(LLCamera &camera) { // store rigged alpha groups for LLDrawPoolAlpha prepass (skip distance update, rigged attachments use depth buffer) // fan the attachment's draw-order stamp (LLVOAvatar::idleUpdateMisc) // out from the bridge to every visible group of the linkset, - // however its prims bin into the bridge octree. The depth copy - // gives all of an avatar's groups one shared depth -- what lets - // the interleaved walk treat the avatar as a single contiguous - // run -- and is gated so the legacy path keeps bounds-front depths. + // however its prims bin into the bridge octree. The shared + // mAvatarDepth keys each avatar's contiguous run in the + // interleaved walk; mDepth stays bounds-derived for the world sort. LLSpatialBridge* stamp_bridge = group->getSpatialPartition()->asBridge(); if (stamp_bridge && stamp_bridge->mAvatarp) { group->mAvatarp = stamp_bridge->mAvatarp; group->mRenderOrder = stamp_bridge->mRenderOrder; - if (interleaved_alpha) - { - group->mDepth = stamp_bridge->mDepth; - } + group->mAvatarDepth = stamp_bridge->mAvatarDepth; } if (hasRenderType(LLDrawPool::POOL_ALPHA)) From a3a373d55ca893cff1a0c26e1b84e6a706df6640 Mon Sep 17 00:00:00 2001 From: Viscerous Date: Sun, 12 Jul 2026 13:48:30 +0200 Subject: [PATCH 5/6] Interleaved alpha: sort the wearer's whole ensemble at one avatar depth --- indra/newview/llcontrolavatar.cpp | 10 +++--- indra/newview/lldrawpoolalpha.cpp | 35 ++++++++------------- indra/newview/llspatialpartition.h | 50 +++++++++++++++++++++--------- indra/newview/llvoavatar.cpp | 16 +++++----- indra/newview/pipeline.cpp | 41 ++++++++++++------------ 5 files changed, 82 insertions(+), 70 deletions(-) diff --git a/indra/newview/llcontrolavatar.cpp b/indra/newview/llcontrolavatar.cpp index d1ffd18744..e9e6b73352 100644 --- a/indra/newview/llcontrolavatar.cpp +++ b/indra/newview/llcontrolavatar.cpp @@ -367,12 +367,10 @@ void LLControlAvatar::idleUpdate(LLAgent &agent, const F64 &time) { LLVOAvatar::idleUpdate(agent,time); - // stamp the rigged alpha draw order onto the animesh bridge, same as - // LLVOAvatar::idleUpdateMisc does for attachment bridges, worn animesh - // is skipped: the wearer's attachment loop stamps this same bridge - // twith he wearer's depth and attachment order, keeping it inside - // the wearer's contiguous run. Fetched from the drawable, not - // mControlAVBridge, which can go stale (see markForDeath). + // stamp the animesh bridge like LLVOAvatar::idleUpdateMisc stamps + // attachments. Worn animesh is skipped: the wearer's loop stamps this + // same bridge with its own depth and order. Bridge fetched from the + // drawable -- mControlAVBridge can go stale (see markForDeath). if (mRootVolp && mRootVolp->mDrawable && !getAttachedAvatar()) { LLSpatialBridge* bridge = mRootVolp->mDrawable->getSpatialBridge(); diff --git a/indra/newview/lldrawpoolalpha.cpp b/indra/newview/lldrawpoolalpha.cpp index e97355b0d0..099330f345 100644 --- a/indra/newview/lldrawpoolalpha.cpp +++ b/indra/newview/lldrawpoolalpha.cpp @@ -205,10 +205,8 @@ void LLDrawPoolAlpha::renderPostDeferred(S32 pass) if (interleaved_alpha && !LLPipeline::sRenderingHUDs && getType() == LLDrawPool::POOL_ALPHA_POST_WATER) { - // single pass: depth-interleave whole avatars (rigged alpha, drawn in - // attachment order with depth writes) with the distance-sorted alpha - // groups, so world alpha composites correctly both in front of and - // behind avatars. + // single pass: depth-interleave whole avatars with the distance-sorted + // alpha groups so world alpha composites correctly around avatars forwardRender(EAlphaStream::INTERLEAVED); } else @@ -629,9 +627,8 @@ void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, EAlphaStream stream LLPipeline::sImpostorRenderAlphaDepthPass || getType() == LLDrawPoolAlpha::POOL_ALPHA_PRE_WATER; - // merged mode: per-group depth-write guard, re-emplaced only when the write - // state changes so a run of same-state groups pays one glDepthMask, not one - // per group (see below) + // merged mode: per-group depth-write guard, re-emplaced only when the + // write state changes (one glDepthMask per run, not per group) std::optional depth_state; bool depth_state_writes = false; @@ -640,11 +637,10 @@ void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, EAlphaStream stream LL_PROFILE_ZONE_NAMED_CATEGORY_DRAWPOOL("renderAlpha - group"); if (merged) - { // single back-to-front walk over both streams: take the farther of - // the two heads. All of an avatar's rigged groups share one avatar - // depth (see LLSpatialBridge::mAvatarp), so each avatar's - // attachment-order run drains contiguously; on ties rigged draws - // first so world alpha at the same depth composites over it. + { // take the farther of the two stream heads; an ensemble's groups + // share one avatar depth, so each avatar drains contiguously -- + // rigged run first (ties go rigged), then its unrigged attachment + // groups, which composite over it if (rigged_iter == rigged_end) { rigged = false; @@ -655,7 +651,7 @@ void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, EAlphaStream stream } else { - rigged = (*rigged_iter)->mAvatarDepth >= (*iter)->mDepth; + rigged = (*rigged_iter)->mAvatarDepth >= (*iter)->worldAlphaDepth(); } } @@ -688,15 +684,10 @@ void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, EAlphaStream stream } } - // merged mode: rigged alpha writes depth so attachment-order - // layering holds and impostors/DoF still see it -- but only when - // the group carries an avatar stamp (mAvatarp, stamped for - // attachments by LLVOAvatar::idleUpdateMisc and for animesh by - // LLControlAvatar::idleUpdate). A not-yet-stamped group has no - // defined position in the rigged order and must not be allowed to - // depth-reject geometry behind it (it still blends; it just can't - // erase). Non-merged passes keep the pass-wide depth state set by - // the caller. + // merged mode: stamped rigged groups write depth so attachment-order + // layering holds; an unstamped group has no defined position in the + // rigged order and must not depth-reject geometry behind it (it + // still blends). Non-merged passes keep the caller's depth state. if (merged) { bool write_depth = write_depth_always || (rigged && group->mAvatarp != nullptr); diff --git a/indra/newview/llspatialpartition.h b/indra/newview/llspatialpartition.h index 2206ff0c03..431983c518 100644 --- a/indra/newview/llspatialpartition.h +++ b/indra/newview/llspatialpartition.h @@ -237,6 +237,32 @@ class LLSpatialGroup : public LLOcclusionCullingGroup } }; + // interleaved world-alpha sort key: a stamped attachment's groups sort at + // the wearer's avatar depth, everything else at bounds depth + F32 worldAlphaDepth() const { return mAvatarp ? mAvatarDepth : mDepth; } + + struct CompareWorldAlphaDepth + { + bool operator()(const LLSpatialGroup* const& lhs, const LLSpatialGroup* const& rhs) + { + F32 lhs_depth = lhs->worldAlphaDepth(); + F32 rhs_depth = rhs->worldAlphaDepth(); + if (lhs_depth != rhs_depth) + { + return lhs_depth > rhs_depth; + } + + if (lhs->mAvatarp != rhs->mAvatarp) + { + // std::less: raw < on unrelated pointers is unspecified + return std::less()(lhs->mAvatarp, rhs->mAvatarp); + } + + // groups of one ensemble keep their bounds order among themselves + return lhs->mDepth > rhs->mDepth; + } + }; + struct CompareRenderOrder { bool operator()(const LLSpatialGroup* const& lhs, const LLSpatialGroup* const& rhs) @@ -255,10 +281,8 @@ class LLSpatialGroup : public LLOcclusionCullingGroup { bool operator()(const LLSpatialGroup* const& lhs, const LLSpatialGroup* const& rhs) { - // farther avatars draw first so rigged alpha can be depth-interleaved - // with the distance-sorted alpha groups; all of an avatar's groups - // share one avatar depth (see LLSpatialBridge::mAvatarp), keeping - // each avatar's run contiguous and in attachment render order + // farther avatars draw first; all of an avatar's groups share one + // depth, keeping its run contiguous and in attachment order if (lhs->mAvatarDepth != rhs->mAvatarDepth) { return lhs->mAvatarDepth > rhs->mAvatarDepth; @@ -274,8 +298,7 @@ class LLSpatialGroup : public LLOcclusionCullingGroup return lhs->mRenderOrder > rhs->mRenderOrder; } - // bounds depth keeps groups of one attachment (which tie on all - // of the above) in a stable back-to-front order + // bounds depth: stable back-to-front order within one attachment return lhs->mDepth > rhs->mDepth; } }; @@ -382,9 +405,8 @@ class LLSpatialGroup : public LLOcclusionCullingGroup //used by LLVOAVatar to set render order in alpha draw pool to preserve legacy render order behavior LLVOAvatar* mAvatarp = nullptr; U32 mRenderOrder = 0; - // rigged alpha sort key (LLSpatialBridge::mAvatarDepth, fanned out in - // LLPipeline::postSort); separate from mDepth so mixed groups keep their - // bounds depth in the world-alpha sort + // avatar-ensemble sort key, fanned out from the bridge in + // LLPipeline::postSort; mDepth stays bounds-derived F32 mAvatarDepth = 0.f; // Reflection Probe associated with this node (if any) LLPointer mReflectionProbe = nullptr; @@ -494,12 +516,10 @@ class LLSpatialBridge : public LLDrawable, public LLSpatialPartition void transformExtents(const LLVector4a* src, LLVector4a* dst); LLDrawable* mDrawable; - // rigged alpha draw-order stamp -- one stamp per attachment (set per - // detailed update by LLVOAvatar::idleUpdateMisc) or per animesh (set per - // frame by LLControlAvatar::idleUpdate), however many child prims/groups - // the linkset spans. LLPipeline::postSort fans it out to each of this - // bridge's visible rigged alpha groups. Raw pointer is only ever - // compared, never dereferenced (avatar may die first). + // alpha draw-order stamp, set by LLVOAvatar::idleUpdateMisc / + // LLControlAvatar::idleUpdate and fanned out to this bridge's alpha + // groups in LLPipeline::postSort. mAvatarp is only ever compared, + // never dereferenced (avatar may die first). LLVOAvatar* mAvatarp = nullptr; U32 mRenderOrder = 0; F32 mAvatarDepth = 0.f; diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index e7199a63bb..2481790cc5 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -3107,14 +3107,16 @@ void LLVOAvatar::idleUpdateMisc(bool detailed_update) bridge->setState(LLDrawable::MOVE_UNDAMPED); bridge->updateMove(); bridge->setState(LLDrawable::EARLY_MOVE); + } - //set draw order of the attachment; LLPipeline::postSort - //fans the stamp out to each of the bridge's visible - //rigged alpha groups, so the whole linkset sorts with - //this avatar in attachment order no matter how its - //prims bin into the bridge octree. Staleness-safe under - //detailed_update: mUpdatePeriod > 1 implies isImpostor(), - //whose attachments never enter the live alpha streams + //stamp the attachment's draw order; LLPipeline::postSort + //fans it out to the bridge's alpha groups, sorting the + //wearer's whole ensemble at one avatar depth. HUD alpha + //sorts in HUD space, so unrigged HUDs stay unstamped. + //Stale-safe: mUpdatePeriod > 1 implies isImpostor(), + //whose attachments never enter the live alpha streams + if (rigged || !attached_object->isHUDAttachment()) + { bridge->mAvatarp = this; bridge->mRenderOrder = draw_order++; bridge->mAvatarDepth = rigged_depth; diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 5f639a959c..196b28dcf8 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -3637,11 +3637,22 @@ void LLPipeline::postSort(LLCamera &camera) if (hasRenderType(LLPipeline::RENDER_TYPE_PASS_ALPHA)) { + LLSpatialBridge *bridge = group->getSpatialPartition()->asBridge(); + + // fan the attachment's stamp (LLVOAvatar::idleUpdateMisc) out from + // the bridge to every visible alpha group of the linkset; the shared + // mAvatarDepth keys the wearer's ensemble as one block in the walk + if (bridge && bridge->mAvatarp) + { + group->mAvatarp = bridge->mAvatarp; + group->mRenderOrder = bridge->mRenderOrder; + group->mAvatarDepth = bridge->mAvatarDepth; + } + LLSpatialGroup::draw_map_t::iterator alpha = group->mDrawMap.find(LLRenderPass::PASS_ALPHA); if (alpha != group->mDrawMap.end()) { // store alpha groups for sorting - LLSpatialBridge *bridge = group->getSpatialPartition()->asBridge(); if (LLViewerCamera::sCurCameraID == LLViewerCamera::CAMERA_WORLD && !gCubeSnapshot) { if (bridge) @@ -3665,19 +3676,6 @@ void LLPipeline::postSort(LLCamera &camera) if (rigged_alpha != group->mDrawMap.end()) { // store rigged alpha groups for LLDrawPoolAlpha prepass (skip distance update, rigged attachments use depth buffer) - // fan the attachment's draw-order stamp (LLVOAvatar::idleUpdateMisc) - // out from the bridge to every visible group of the linkset, - // however its prims bin into the bridge octree. The shared - // mAvatarDepth keys each avatar's contiguous run in the - // interleaved walk; mDepth stays bounds-derived for the world sort. - LLSpatialBridge* stamp_bridge = group->getSpatialPartition()->asBridge(); - if (stamp_bridge && stamp_bridge->mAvatarp) - { - group->mAvatarp = stamp_bridge->mAvatarp; - group->mRenderOrder = stamp_bridge->mRenderOrder; - group->mAvatarDepth = stamp_bridge->mAvatarDepth; - } - if (hasRenderType(LLDrawPool::POOL_ALPHA)) { sCull->pushRiggedAlphaGroup(group); @@ -3719,18 +3717,21 @@ void LLPipeline::postSort(LLCamera &camera) if (!sShadowRender) { - // order alpha groups by distance - std::sort(sCull->beginAlphaGroups(), sCull->endAlphaGroups(), LLSpatialGroup::CompareDepthGreater()); - if (interleaved_alpha) { - // order rigged alpha groups by avatar depth, then attachment order, - // so LLDrawPoolAlpha can depth-interleave whole avatars with the - // distance-sorted alpha groups above + // order alpha groups by distance, a stamped attachment's groups + // keyed at the wearer's depth (one contiguous ensemble block) + std::sort(sCull->beginAlphaGroups(), sCull->endAlphaGroups(), LLSpatialGroup::CompareWorldAlphaDepth()); + + // order rigged alpha groups by avatar depth then attachment order + // for the depth-interleaved walk in LLDrawPoolAlpha std::sort(sCull->beginRiggedAlphaGroups(), sCull->endRiggedAlphaGroups(), LLSpatialGroup::CompareDepthRenderOrder()); } else { + // order alpha groups by distance + std::sort(sCull->beginAlphaGroups(), sCull->endAlphaGroups(), LLSpatialGroup::CompareDepthGreater()); + // order rigged alpha groups by avatar attachment order std::sort(sCull->beginRiggedAlphaGroups(), sCull->endRiggedAlphaGroups(), LLSpatialGroup::CompareRenderOrder()); } From 634502b51eef6a029497d332f841439f519bc40a Mon Sep 17 00:00:00 2001 From: Viscerous Date: Mon, 13 Jul 2026 01:19:09 +0200 Subject: [PATCH 6/6] Interleaved alpha: preserve legacy ordering outside the merged pass --- indra/newview/lldrawpoolalpha.cpp | 36 ++++++++++++++++++++++++----- indra/newview/llspatialpartition.h | 21 ++++++++++++++++- indra/newview/pipeline.cpp | 37 ++++++++++++++---------------- indra/newview/pipeline.h | 2 ++ 4 files changed, 69 insertions(+), 27 deletions(-) diff --git a/indra/newview/lldrawpoolalpha.cpp b/indra/newview/lldrawpoolalpha.cpp index 099330f345..afda976ed9 100644 --- a/indra/newview/lldrawpoolalpha.cpp +++ b/indra/newview/lldrawpoolalpha.cpp @@ -164,6 +164,15 @@ void LLDrawPoolAlpha::renderPostDeferred(S32 pass) // prepare shaders llassert(LLPipeline::sRenderDeferred); + const bool interleave = LLPipeline::canUseInterleavedAlpha() && + getType() == LLDrawPool::POOL_ALPHA_POST_WATER; + if (interleave) + { + // postSort deliberately leaves the shared lists in legacy order for + // pre-water; switch them only for the consumer that performs the merge + gPipeline.sortAlphaGroupsForInterleaving(); + } + emissive_shader = &gDeferredEmissiveProgram; prepare_alpha_shader(emissive_shader, false, water_sign); @@ -200,13 +209,12 @@ void LLDrawPoolAlpha::renderPostDeferred(S32 pass) // already being setup for rendering LLGLSLShader::unbind(); - static LLCachedControl interleaved_alpha(gSavedSettings, "RenderInterleavedAlpha", true); - - if (interleaved_alpha && !LLPipeline::sRenderingHUDs && - getType() == LLDrawPool::POOL_ALPHA_POST_WATER) + if (interleave) { // single pass: depth-interleave whole avatars with the distance-sorted - // alpha groups so world alpha composites correctly around avatars + // alpha groups so world alpha composites correctly around avatars. + // The centralized eligibility gate limits the merged walk to the world + // camera; HUD, cube, and reflection renders keep legacy two-pass order. forwardRender(EAlphaStream::INTERLEAVED); } else @@ -651,7 +659,23 @@ void LLDrawPoolAlpha::renderAlpha(U32 mask, bool depth_only, EAlphaStream stream } else { - rigged = (*rigged_iter)->mAvatarDepth >= (*iter)->worldAlphaDepth(); + F32 rigged_depth = (*rigged_iter)->mAvatarDepth; + F32 world_depth = (*iter)->worldAlphaDepth(); + if (rigged_depth != world_depth) + { + rigged = rigged_depth > world_depth; + } + else + { + // equal depth: keep each avatar's ensemble contiguous. Its + // own rigged draws before its own unrigged; a plain world + // group composites over it (rigged first); two coincident + // avatars drain in the sorts' std::less identity order. + const LLVOAvatar* world_av = (*iter)->mAvatarp; + const LLVOAvatar* rigged_av = (*rigged_iter)->mAvatarp; + rigged = !rigged_av || !world_av || world_av == rigged_av || + std::less()(rigged_av, world_av); + } } } diff --git a/indra/newview/llspatialpartition.h b/indra/newview/llspatialpartition.h index 431983c518..527053160a 100644 --- a/indra/newview/llspatialpartition.h +++ b/indra/newview/llspatialpartition.h @@ -254,6 +254,16 @@ class LLSpatialGroup : public LLOcclusionCullingGroup if (lhs->mAvatarp != rhs->mAvatarp) { + // at an exact depth tie, drain stamped ensembles before plain + // world groups so a null head cannot split every ensemble + if (!lhs->mAvatarp) + { + return false; + } + if (!rhs->mAvatarp) + { + return true; + } // std::less: raw < on unrelated pointers is unspecified return std::less()(lhs->mAvatarp, rhs->mAvatarp); } @@ -290,6 +300,16 @@ class LLSpatialGroup : public LLOcclusionCullingGroup if (lhs->mAvatarp != rhs->mAvatarp) { + // An unstamped rigged group has no ensemble key; keep it first + // at an exact depth tie, matching the merge's legacy fallback. + if (!lhs->mAvatarp) + { + return true; + } + if (!rhs->mAvatarp) + { + return false; + } return std::less()(lhs->mAvatarp, rhs->mAvatarp); } @@ -825,4 +845,3 @@ extern const F32 SG_MAX_OBJ_RAD; #endif //LL_LLSPATIALPARTITION_H - diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 196b28dcf8..2786448694 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -3559,6 +3559,19 @@ void renderSoundHighlights(LLDrawable *drawablep) } } +bool LLPipeline::canUseInterleavedAlpha() +{ + static LLCachedControl interleaved_alpha(gSavedSettings, "RenderInterleavedAlpha", true); + return interleaved_alpha && !sRenderingHUDs && !sShadowRender && !gCubeSnapshot && + LLViewerCamera::sCurCameraID == LLViewerCamera::CAMERA_WORLD; +} + +void LLPipeline::sortAlphaGroupsForInterleaving() +{ + std::sort(sCull->beginAlphaGroups(), sCull->endAlphaGroups(), LLSpatialGroup::CompareWorldAlphaDepth()); + std::sort(sCull->beginRiggedAlphaGroups(), sCull->endRiggedAlphaGroups(), LLSpatialGroup::CompareDepthRenderOrder()); +} + void LLPipeline::postSort(LLCamera &camera) { LL_PROFILE_ZONE_SCOPED_CATEGORY_PIPELINE; @@ -3591,8 +3604,6 @@ void LLPipeline::postSort(LLCamera &camera) LL_PUSH_CALLSTACKS(); - static LLCachedControl interleaved_alpha(gSavedSettings, "RenderInterleavedAlpha", true); - // build render map for (LLCullResult::sg_iterator i = sCull->beginVisibleGroups(); i != sCull->endVisibleGroups(); ++i) { @@ -3717,24 +3728,10 @@ void LLPipeline::postSort(LLCamera &camera) if (!sShadowRender) { - if (interleaved_alpha) - { - // order alpha groups by distance, a stamped attachment's groups - // keyed at the wearer's depth (one contiguous ensemble block) - std::sort(sCull->beginAlphaGroups(), sCull->endAlphaGroups(), LLSpatialGroup::CompareWorldAlphaDepth()); - - // order rigged alpha groups by avatar depth then attachment order - // for the depth-interleaved walk in LLDrawPoolAlpha - std::sort(sCull->beginRiggedAlphaGroups(), sCull->endRiggedAlphaGroups(), LLSpatialGroup::CompareDepthRenderOrder()); - } - else - { - // order alpha groups by distance - std::sort(sCull->beginAlphaGroups(), sCull->endAlphaGroups(), LLSpatialGroup::CompareDepthGreater()); - - // order rigged alpha groups by avatar attachment order - std::sort(sCull->beginRiggedAlphaGroups(), sCull->endRiggedAlphaGroups(), LLSpatialGroup::CompareRenderOrder()); - } + // Legacy order is the baseline for every consumer. Post-water replaces + // it with the interleaved order only when it actually uses the merge. + std::sort(sCull->beginAlphaGroups(), sCull->endAlphaGroups(), LLSpatialGroup::CompareDepthGreater()); + std::sort(sCull->beginRiggedAlphaGroups(), sCull->endRiggedAlphaGroups(), LLSpatialGroup::CompareRenderOrder()); } LL_PUSH_CALLSTACKS(); diff --git a/indra/newview/pipeline.h b/indra/newview/pipeline.h index c051306385..a9aac2d038 100644 --- a/indra/newview/pipeline.h +++ b/indra/newview/pipeline.h @@ -383,6 +383,7 @@ class LLPipeline LLCullResult::sg_iterator endAlphaGroups(); LLCullResult::sg_iterator beginRiggedAlphaGroups(); LLCullResult::sg_iterator endRiggedAlphaGroups(); + void sortAlphaGroupsForInterleaving(); void addTrianglesDrawn(S32 index_count); void recordTrianglesDrawn(); @@ -398,6 +399,7 @@ class LLPipeline bool hasAnyRenderType(const U32 type, ...) const; static bool isWaterClip(); + static bool canUseInterleavedAlpha(); void setRenderTypeMask(U32 type, ...); // This is equivalent to 'setRenderTypeMask'