summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan "Geenz" Goodman <geenz@geenzo.com>2026-07-06 19:01:48 -0400
committerJonathan "Geenz" Goodman <geenz@geenzo.com>2026-07-06 19:01:48 -0400
commit845fe1f83c4967fe1d49715d5f17b61a4adc68ee (patch)
treeaa201e6d850b9a07045de9581562d38168b2b14d
parentb55a7038511a6ed3d97e17b13c9d83bc2017ba61 (diff)
Get things working a little more consistently between BP and PBR. Also add some bias controls for the texel-to-pixel ratio based streaming.
-rw-r--r--indra/llprimitive/lltextureentry.cpp13
-rw-r--r--indra/llprimitive/lltextureentry.h14
-rw-r--r--indra/newview/app_settings/settings.xml17
-rw-r--r--indra/newview/llfetchedgltfmaterial.cpp39
-rw-r--r--indra/newview/llviewerobject.cpp18
-rw-r--r--indra/newview/llviewertexture.cpp18
-rw-r--r--indra/newview/llviewertexture.h13
-rw-r--r--indra/newview/llviewertexturelist.cpp226
-rw-r--r--indra/newview/llvovolume.cpp63
9 files changed, 346 insertions, 75 deletions
diff --git a/indra/llprimitive/lltextureentry.cpp b/indra/llprimitive/lltextureentry.cpp
index 5296e86a69..2b0f989701 100644
--- a/indra/llprimitive/lltextureentry.cpp
+++ b/indra/llprimitive/lltextureentry.cpp
@@ -356,7 +356,6 @@ S32 LLTextureEntry::setScale(F32 s, F32 t)
{
mScaleS = s;
mScaleT = t;
- mMinScaleSq = -1.f; // invalidate cache for getMinScaleSq()
retval = TEM_CHANGE_TEXTURE;
}
@@ -369,7 +368,6 @@ S32 LLTextureEntry::setScaleS(F32 s)
if (mScaleS != s)
{
mScaleS = s;
- mMinScaleSq = -1.f; // invalidate cache for getMinScaleSq()
retval = TEM_CHANGE_TEXTURE;
}
return retval;
@@ -381,22 +379,11 @@ S32 LLTextureEntry::setScaleT(F32 t)
if (mScaleT != t)
{
mScaleT = t;
- mMinScaleSq = -1.f; // invalidate cache for getMinScaleSq()
retval = TEM_CHANGE_TEXTURE;
}
return retval;
}
-F32 LLTextureEntry::getMinScaleSq() const
-{
- if (mMinScaleSq < 0.f)
- {
- F32 m = llmin(fabsf(mScaleS), fabsf(mScaleT));
- mMinScaleSq = m * m;
- }
- return mMinScaleSq;
-}
-
S32 LLTextureEntry::setColor(const LLColor4 &color)
{
if (mColor != color)
diff --git a/indra/llprimitive/lltextureentry.h b/indra/llprimitive/lltextureentry.h
index 765a5f5fdc..78c61b4d65 100644
--- a/indra/llprimitive/lltextureentry.h
+++ b/indra/llprimitive/lltextureentry.h
@@ -143,13 +143,6 @@ public:
F32 getScaleS() const { return mScaleS; }
F32 getScaleT() const { return mScaleT; }
- // Cached min(|mScaleS|, |mScaleT|)^2, lazily computed and invalidated
- // in setScale/setScaleS/setScaleT. Used by the texture streaming face
- // loop (updateImageDecodePriority) to avoid the per-face per-frame
- // sqrt/abs/min/multiply chain. Returns the raw (unclamped) value;
- // callers still apply TextureScaleMin/MaxAreaFactor clamps.
- F32 getMinScaleSq() const;
-
void getOffset(F32 *s, F32 *t) const { *s = mOffsetS; *t = mOffsetT; }
F32 getOffsetS() const { return mOffsetS; }
F32 getOffsetT() const { return mOffsetT; }
@@ -225,13 +218,6 @@ public:
F32 mOffsetT; // S, T offset
F32 mRotation; // anti-clockwise rotation in rad about the bottom left corner
-private:
- // Cache for getMinScaleSq(). -1.f sentinel = stale. Invalidated by
- // setScale/setScaleS/setScaleT. Mutable so getMinScaleSq() can fill it
- // on first read without breaking const correctness for read-only callers.
- mutable F32 mMinScaleSq = -1.f;
-public:
-
static const LLTextureEntry null;
// LLSD key defines
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index cfd92e03ca..2fec0b53d4 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -12124,27 +12124,16 @@
<key>Value</key>
<string />
</map>
- <key>TextureScaleMinAreaFactor</key>
+ <key>TextureDownrezCoverageBias</key>
<map>
<key>Comment</key>
- <string>Limits how texture scale affects area calculation.</string>
+ <string>Which end of a texture's texels-per-pixel spread sizes it. Each texture tracks the screen coverage of its most demanding use (lowest texels per pixel) and least demanding use (highest texels per pixel, most oversampled). 0 = size to the most demanding use (best quality); 1 = size to the least demanding use (frees the most memory). Interpolation is geometric (log-space), so the resulting discard level moves linearly with this value - 0.5 sits halfway between the two ends in mip levels. Default 0.25 is the best universal balance.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>F32</string>
<key>Value</key>
- <real>0.0095</real>
- </map>
- <key>TextureScaleMaxAreaFactor</key>
- <map>
- <key>Comment</key>
- <string>Limits how texture scale affects area calculation.</string>
- <key>Persist</key>
- <integer>1</integer>
- <key>Type</key>
- <string>F32</string>
- <key>Value</key>
- <real>25.0</real>
+ <real>0.25</real>
</map>
<key>ThreadPoolSizes</key>
<map>
diff --git a/indra/newview/llfetchedgltfmaterial.cpp b/indra/newview/llfetchedgltfmaterial.cpp
index a05f725673..306067d2cf 100644
--- a/indra/newview/llfetchedgltfmaterial.cpp
+++ b/indra/newview/llfetchedgltfmaterial.cpp
@@ -73,6 +73,23 @@ void LLFetchedGLTFMaterial::bind(LLViewerTexture* media_tex)
LLViewerTexture* baseColorTex = media_tex ? media_tex : mBaseColorTexture;
LLViewerTexture* emissiveTex = media_tex ? media_tex : mEmissiveTexture;
+ if (media_tex)
+ {
+ // The real basecolor/emissive stay registered for coverage while
+ // media hides them - stamp them so the streaming cooldown doesn't
+ // fight that coverage (coarsen -> refetch tug-of-war) the whole
+ // time media is playing. Blinn has no hidden-texture-under-media
+ // state, so without this the two systems diverge on media faces.
+ if (mBaseColorTexture.notNull())
+ {
+ if (LLImageGL* gl_tex = mBaseColorTexture->getGLTexture()) { gl_tex->stampBound(); }
+ }
+ if (mEmissiveTexture.notNull())
+ {
+ if (LLImageGL* gl_tex = mEmissiveTexture->getGLTexture()) { gl_tex->stampBound(); }
+ }
+ }
+
if (!LLPipeline::sShadowRender || (mAlphaMode == LLGLTFMaterial::ALPHA_MODE_MASK))
{
if (mAlphaMode == LLGLTFMaterial::ALPHA_MODE_MASK)
@@ -97,13 +114,33 @@ void LLFetchedGLTFMaterial::bind(LLViewerTexture* media_tex)
if (!LLPipeline::sShadowRender)
{
- if (mNormalTexture.notNull() && mNormalTexture->getDiscardLevel() <= 4)
+ // Bind the normal map at whatever resolution is resident - matching
+ // how Blinn-Phong normal maps degrade (soft, never absent). The old
+ // "getDiscardLevel() <= 4" gate made PBR normals a cliff instead of
+ // a gradient: any material the streamer legitimately sized past
+ // discard 4 (distant / tiled) rendered with NO normal map while the
+ // equivalent Blinn content rendered a soft one, making PBR look
+ // categorically flatter. The flat-normal fallback remains only for
+ // the genuinely-not-yet-loaded window, where it is the correct
+ // normal-shaped default.
+ if (mNormalTexture.notNull() && mNormalTexture->hasGLTexture())
{
shader->bindTexture(LLShaderMgr::BUMP_MAP, mNormalTexture);
}
else
{
shader->bindTexture(LLShaderMgr::BUMP_MAP, LLViewerFetchedTexture::sFlatNormalImagep);
+ if (mNormalTexture.notNull())
+ {
+ // In active use, just not loaded yet - stamp it so the
+ // streaming last-bound cooldown doesn't read "unbound" as
+ // "unseen" and pin it at the deepest mip before its first
+ // real bind.
+ if (LLImageGL* gl_tex = mNormalTexture->getGLTexture())
+ {
+ gl_tex->stampBound();
+ }
+ }
}
if (mMetallicRoughnessTexture.notNull())
diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp
index 7c26cb3c9f..4e4282a8ca 100644
--- a/indra/newview/llviewerobject.cpp
+++ b/indra/newview/llviewerobject.cpp
@@ -5229,7 +5229,20 @@ void LLViewerObject::setTE(const U8 te, const LLTextureEntry& texture_entry)
const LLUUID& image_id = getTE(te)->getID();
LLViewerTexture* bakedTexture = getBakedTextureForMagicId(image_id);
- mTEImages[te] = bakedTexture ? bakedTexture : LLViewerTextureManager::getFetchedTexture(image_id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
+ if (bakedTexture)
+ {
+ mTEImages[te] = bakedTexture;
+ }
+ else
+ {
+ LLViewerFetchedTexture* img = LLViewerTextureManager::getFetchedTexture(image_id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
+ // Same creation seed the PBR path applies in updateTEMaterialTextures'
+ // fetch_texture - without it a Blinn texture has decode_priority 0 and
+ // cannot even fetch headers until its first coverage measurement,
+ // while the equivalent PBR texture starts fetching immediately.
+ img->addTextureStats(64.f * 64.f, true);
+ mTEImages[te] = img;
+ }
updateAvatarMeshVisibility(image_id, old_image_id);
@@ -5240,11 +5253,14 @@ void LLViewerObject::updateTEMaterialTextures(U8 te)
{
if (getTE(te)->getMaterialParams().notNull())
{
+ // Same creation seed as the PBR fetch_texture below - see setTE.
const LLUUID& norm_id = getTE(te)->getMaterialParams()->getNormalID();
mTENormalMaps[te] = LLViewerTextureManager::getFetchedTexture(norm_id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
+ mTENormalMaps[te]->addTextureStats(64.f * 64.f, true);
const LLUUID& spec_id = getTE(te)->getMaterialParams()->getSpecularID();
mTESpecularMaps[te] = LLViewerTextureManager::getFetchedTexture(spec_id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
+ mTESpecularMaps[te]->addTextureStats(64.f * 64.f, true);
}
LLFetchedGLTFMaterial* mat = (LLFetchedGLTFMaterial*) getTE(te)->getGLTFRenderMaterial();
diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp
index ef49e0e67f..baf5874507 100644
--- a/indra/newview/llviewertexture.cpp
+++ b/indra/newview/llviewertexture.cpp
@@ -3070,6 +3070,13 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c
static LLCachedControl<F32> ch_emissive (gSavedSettings, "TextureChannelRatioEmissive", 0.5f);
const F32 channel_ratio[4] = { (F32)ch_normal, (F32)ch_basecolor, (F32)ch_specular, (F32)ch_emissive };
+ // Downrez bias: 0 sizes each bucket to its most demanding use (the lowest
+ // texels-per-pixel variant - the quality bound); 1 sizes to its least
+ // demanding use (the most oversampled variant - frees the most memory).
+ // Values between lerp across the texture's measured coverage spread.
+ static LLCachedControl<F32> downrez_bias(gSavedSettings, "TextureDownrezCoverageBias", 0.25f);
+ const F32 cov_bias = llclampf((F32)downrez_bias);
+
// Continuous ideal discard = sharpest (smallest) requirement across the
// channels this texture is actually used in.
F32 ideal = (F32)dim_max_i; // default: coarsest, until a measurement arrives
@@ -3081,6 +3088,17 @@ S32 LLViewerLODTexture::computeDesiredDiscard(S32 dim_max_i, bool avatar_bake) c
{
continue;
}
+ if (cov_bias > 0.f && mChannelCoverageMin[b] > 0.f && mChannelCoverageMin[b] < coverage)
+ {
+ // Geometric (log-space) lerp between the coverage bounds. The
+ // consumer below is log4(coverage), and min/max are routinely
+ // orders of magnitude apart - a linear pixel-area lerp barely
+ // moves the resulting discard until bias approaches 1, then
+ // plunges (reads as binary). Interpolating the RATIO instead
+ // moves the discard linearly with bias: 0.5 = halfway between
+ // the two ends in mip levels.
+ coverage *= powf(mChannelCoverageMin[b] / coverage, cov_bias);
+ }
measured = true;
F32 allowed_texels = coverage * r_global * llmax(channel_ratio[b], 0.01f);
F32 d;
diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h
index 6fc6ad72f9..28d896eff5 100644
--- a/indra/newview/llviewertexture.h
+++ b/indra/newview/llviewertexture.h
@@ -150,6 +150,11 @@ public:
virtual F32 getMaxVirtualSize() ;
+ // Read-only debug access to the per-bucket coverage bounds (see
+ // mChannelCoverage) - used by the RENDER_DEBUG_TEXTURE_PRIORITY overlay.
+ F32 getChannelCoverage(S32 bucket) const { return (bucket >= 0 && bucket < 4) ? mChannelCoverage[bucket] : 0.f; }
+ F32 getChannelCoverageMin(S32 bucket) const { return (bucket >= 0 && bucket < 4) ? mChannelCoverageMin[bucket] : 0.f; }
+
LLFrameTimer* getLastReferencedTimer() { return &mLastReferencedTimer; }
S32 getFullWidth() const { return mFullWidth; }
@@ -205,13 +210,17 @@ protected:
mutable S32 mMaxVirtualSizeResetInterval;
LLFrameTimer mLastReferencedTimer;
- // Largest screen-space pixel coverage among the texture's faces, per
+ // Screen-space pixel coverage bounds among the texture's faces, per
// priority bucket (0=Normal, 1=BaseColor, 2=Specular, 3=Emissive). 0 = the
// texture is not used in that channel (or hasn't been measured yet).
// Populated by LLViewerTextureList::updateImageDecodePriority; consumed by
// LLViewerLODTexture::computeDesiredDiscard. This is the only view-dependent
- // streaming signal - distance, size, and channel role all collapse into it.
+ // streaming signal - distance, size, tiling, and channel role all collapse
+ // into it. Max = the most demanding use (lowest texels-per-pixel variant,
+ // the quality bound); Min = the least demanding positive use (highest
+ // texels-per-pixel, most oversampled - the downrez-bias end).
F32 mChannelCoverage[4] = { 0.f, 0.f, 0.f, 0.f };
+ F32 mChannelCoverageMin[4] = { 0.f, 0.f, 0.f, 0.f };
ll_face_list_t mFaceList[LLRender::NUM_TEXTURE_CHANNELS]; //reverse pointer pointing to the faces using this image as texture
U32 mNumFaces[LLRender::NUM_TEXTURE_CHANNELS];
diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp
index 16a3418bc7..e503bbc466 100644
--- a/indra/newview/llviewertexturelist.cpp
+++ b/indra/newview/llviewertexturelist.cpp
@@ -64,6 +64,8 @@
#include "llviewerwindow.h"
#include "llsurface.h"
#include "llvoavatarself.h"
+#include "llvovolume.h"
+#include "llviewertextureanim.h"
#include "llprogressview.h"
////////////////////////////////////////////////////////////////////////////
@@ -916,18 +918,35 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
{
llassert(!gCubeSnapshot);
+ // Refresh spotlight priorities first: light projector textures register as
+ // LIGHT_TEX volumes (no faces), and both their fetch priority
+ // (addTextureStats inside updateSpotLightPriority) and their coverage
+ // (folded into the block below) derive from mSpotLightPriority.
+ for (S32 vi = 0; vi < imagep->getNumVolumes(LLRender::LIGHT_TEX); ++vi)
+ {
+ LLVOVolume* volume = (*imagep->getVolumeList(LLRender::LIGHT_TEX))[vi];
+ volume->updateSpotLightPriority();
+ }
+
if (imagep->getBoostLevel() < LLViewerFetchedTexture::BOOST_HIGH) // don't bother checking face list for boosted textures
{
- static LLCachedControl<F32> texture_scale_min(gSavedSettings, "TextureScaleMinAreaFactor", 0.0095f);
- static LLCachedControl<F32> texture_scale_max(gSavedSettings, "TextureScaleMaxAreaFactor", 25.f);
+ // Bounds on the per-face UV repeat-area divisor (mined from the old
+ // getTextureVirtualSize texel_area clamp [1/64, 128]): atlas/crop boost
+ // capped at 64x (3 mips finer), tiling penalty at 128x (3.5 mips
+ // coarser) so pathological UV scales can't explode either direction.
+ constexpr F32 MIN_REPEAT_AREA = 1.f / 64.f;
+ constexpr F32 MAX_REPEAT_AREA = 128.f;
- // Largest screen-space pixel coverage per priority bucket
- // (0=Normal, 1=BaseColor, 2=Specular, 3=Emissive), plus the overall max.
- // The per-bucket maxima drive desired discard with per-channel ratios;
- // the overall max is the fetch priority (raw - no bias). A bucket with
- // faces but zero coverage (all off-screen) publishes 0, which
+ // Per priority bucket (0=Normal, 1=BaseColor, 2=Specular, 3=Emissive):
+ // the HIGHEST per-face effective coverage (= the lowest texels-per-pixel
+ // use, the most demanding variant - drives desired discard) and the
+ // LOWEST positive coverage (= the highest texels-per-pixel use, the most
+ // oversampled variant - available for downrez-bias policy). The overall
+ // max is the fetch priority (raw - no bias). A bucket with faces but
+ // zero coverage (all off-screen) publishes 0, which
// computeDesiredDiscard reads as "coarsest mip".
F32 channel_coverage[4] = { 0.f, 0.f, 0.f, 0.f };
+ F32 channel_coverage_min[4] = { FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX };
bool bucket_used[4] = { false, false, false, false };
F32 max_coverage = 0.f;
@@ -952,7 +971,13 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
// Used in so many places that scanning the face list isn't worth it
// (and isn't time-sliced) - treat as full-screen so it loads sharp.
for (S32 b = 0; b < 4; ++b)
- if (bucket_used[b]) channel_coverage[b] = (F32)MAX_IMAGE_AREA;
+ {
+ if (bucket_used[b])
+ {
+ channel_coverage[b] = (F32)MAX_IMAGE_AREA;
+ channel_coverage_min[b] = (F32)MAX_IMAGE_AREA;
+ }
+ }
max_coverage = (F32)MAX_IMAGE_AREA;
}
else
@@ -980,20 +1005,163 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
face->mLastTextureUpdate = gFrameCount;
}
- F32 vsize = face->getPixelArea();
+ // Most-demanding-point measurement: the spec is that the
+ // LOWEST pixel:texel ratio governs, so pixel density is
+ // evaluated at the face's NEAREST point and applied to the
+ // face's true world area. The previous whole-face average
+ // (bounding-disc pixel area) under-resolved perspective
+ // surfaces: on a floor, the tile at your feet covers far
+ // more screen than the average tile, and the GPU samples
+ // fine mips right there - tiled (PBR-heavy) content went
+ // soft while untiled content looked fine.
+ const LLVector4a* ext = face->isState(LLFace::RIGGED) ? face->mRiggedExtents : face->mExtents;
+ LLVector4a diag;
+ diag.setSub(ext[1], ext[0]);
+ // World area of the face ~ product of the two largest AABB
+ // dims (max pairwise product; robust for flat faces).
+ F32 dx = diag[0], dy = diag[1], dz = diag[2];
+ F32 area_world = llmax(dx * dy, llmax(dx * dz, dy * dz));
+ // Pixels per meter at the nearest point. Distance floored:
+ // nearer than this the screen clamp below governs anyway.
+ F32 dist = llmax(face->mDistanceToCamera, 0.5f);
+ F32 ppm = LLDrawable::sCurPixelAngle / dist;
+ F32 face_px = area_world * ppm * ppm;
+ if (face_px <= 0.f)
+ {
+ // Degenerate extents: the face hasn't been through a
+ // geometry build yet (or a rigged face has no rigged
+ // extents) - it isn't renderable, so it must not be
+ // measured. Skipping matters especially for the
+ // per-bucket MIN bound: any invented placeholder
+ // value (the old fallback hit LLFace::init's 16px
+ // default) becomes the texture's least-demanding
+ // "use" and, under TextureDownrezCoverageBias, drags
+ // the whole texture to its deepest mip - and it
+ // poisoned BP and PBR asymmetrically since the two
+ // systems register faces at different points in the
+ // geometry lifecycle.
+ continue;
+ }
- // Scale coverage by texture repeat: a texture shown at a
- // fraction of a face (atlas) needs only that fraction of
- // texels; a tiled texture needs more. getMinScaleSq() is the
- // cached min(|scaleS|,|scaleT|)^2, invalidated by setScale*.
+ // Effective UV repeat AREA across this face: the tiling
+ // term of texels-drawn-per-screen-pixel. More tiling =>
+ // each tile is smaller on screen => coarser mips suffice
+ // (penalty). Repeats < 1 (atlas/crop) => only a sub-rect
+ // of the image is shown, but discard levels are whole-
+ // image, so the full image must be resident at 1/repeats
+ // times the crop's pixel count (boost).
S32 te_offset = face->getTEOffset(); // offset is -1 if not inited
LLViewerObject* objp = face->getViewerObject();
const LLTextureEntry* te = (te_offset < 0 || te_offset >= objp->getNumTEs()) ? nullptr : objp->getTE(te_offset);
- F32 min_scale = te ? llclamp(te->getMinScaleSq(), texture_scale_min(), texture_scale_max()) : 1.f;
- vsize /= min_scale;
+
+ F32 repeats = 1.f;
+ if (te)
+ {
+ // UV scale source: every channel reads the repeat
+ // values ITS renderer actually applies, then flows
+ // through the identical pipeline below. Sources:
+ // diffuse -> TE scale
+ // Blinn normal/spec -> LLMaterial per-map repeats
+ // PBR channels -> KHR texture_transform scale
+ // Fallback for any missing material is the TE scale -
+ // never a silent hardcoded 1.
+ F32 scale_s = te->getScaleS();
+ F32 scale_t = te->getScaleT();
+ if (i >= LLRender::BASECOLOR_MAP)
+ {
+ // LLRender channel -> LLGLTFMaterial::TextureInfo
+ static const S32 gltf_info[4] = {
+ LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR, // BASECOLOR_MAP (3)
+ LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS, // METALLIC_ROUGHNESS_MAP (4)
+ LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL, // GLTF_NORMAL_MAP (5)
+ LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE, // EMISSIVE_MAP (6)
+ };
+ if (const LLGLTFMaterial* gltf_mat = te->getGLTFRenderMaterial())
+ {
+ const LLVector2& s = gltf_mat->mTextureTransform[gltf_info[i - LLRender::BASECOLOR_MAP]].mScale;
+ scale_s = s.mV[0];
+ scale_t = s.mV[1];
+ }
+ }
+ else if (i == LLRender::NORMAL_MAP || i == LLRender::SPECULAR_MAP)
+ {
+ // Blinn-Phong normal/specular maps carry their own
+ // repeats in LLMaterial - the renderer builds their
+ // texture matrices from these, NOT from the TE's
+ // diffuse scale. Reading the diffuse scale here made
+ // Blinn normals scale differently than PBR normals
+ // (whose per-channel transform IS read above).
+ if (const LLMaterial* mat = te->getMaterialParams().get())
+ {
+ if (i == LLRender::NORMAL_MAP)
+ {
+ mat->getNormalRepeat(scale_s, scale_t);
+ }
+ else
+ {
+ mat->getSpecularRepeat(scale_s, scale_t);
+ }
+ }
+ }
+
+ // Continuously-animated scale (llSetTextureAnim SCALE)
+ // bypasses both static sources via mTextureMatrix -
+ // the live animated values win on either path.
+ if (LLVOVolume* vvo = face->getDrawable() ? face->getDrawable()->getVOVolume() : nullptr)
+ {
+ LLViewerTextureAnim* anim = vvo->mTextureAnimp;
+ if (anim && (anim->mMode & LLTextureAnim::ON) && (anim->mMode & LLTextureAnim::SCALE)
+ && (anim->mFace < 0 || anim->mFace == te_offset))
+ {
+ scale_s = anim->mScaleS;
+ scale_t = anim->mScaleT;
+ }
+ }
+
+ repeats = fabsf(scale_s * scale_t);
+
+ // Mesh atlas sub-rect: a face whose intrinsic UVs span
+ // only part of [0,1]^2 shows that fraction of the
+ // image. Applies identically to both paths - the
+ // transforms above stack on the raw face UVs.
+ if (LLVolume* vol = objp->getVolume())
+ {
+ if (te_offset >= 0 && te_offset < vol->getNumVolumeFaces())
+ {
+ const LLVolumeFace& vf = vol->getVolumeFace(te_offset);
+ F32 span = fabsf((vf.mTexCoordExtents[1].mV[0] - vf.mTexCoordExtents[0].mV[0])
+ * (vf.mTexCoordExtents[1].mV[1] - vf.mTexCoordExtents[0].mV[1]));
+ if (span > 0.f)
+ {
+ repeats *= span;
+ }
+ }
+ }
+ }
+
+ repeats = llclamp(repeats, MIN_REPEAT_AREA, MAX_REPEAT_AREA);
+
+ // Apply the two sides of the repeat term in the right
+ // order relative to the screen clamp:
+ // - tiling (repeats > 1): the per-tile footprint at the
+ // nearest point, THEN clamped - one tile can't draw
+ // more pixels than the screen. (Clamping the whole
+ // face first and then dividing crushed near tiles.)
+ // - atlas/crop (repeats < 1): boost AFTER the clamp -
+ // whole-image residency for a crop legitimately
+ // demands more than its screen coverage.
+ F32 tiling = llmax(repeats, 1.f);
+ F32 crop = llmin(repeats, 1.f);
+ F32 vsize = llmin(face_px / tiling, LLViewerTexture::sWindowPixelArea) / crop;
if (bucket >= 0 && bucket < 4)
+ {
channel_coverage[bucket] = llmax(channel_coverage[bucket], vsize);
+ if (vsize > 0.f)
+ {
+ channel_coverage_min[bucket] = llmin(channel_coverage_min[bucket], vsize);
+ }
+ }
max_coverage = llmax(max_coverage, vsize);
}
}
@@ -1010,15 +1178,34 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
// Floor so terrain never collapses to nothing at draw distance.
F32 cov = LLViewerTexture::sWindowPixelArea * llmax(near_frac, 0.05f);
channel_coverage[1] = cov; // terrain detail maps are diffuse / base color
+ channel_coverage_min[1] = cov;
max_coverage = cov;
}
+ // Light projector textures register as LIGHT_TEX volumes, not faces.
+ // mSpotLightPriority (refreshed above) is already a screen-pixel-area
+ // estimate of the lit radius - fold it in as BaseColor coverage so
+ // projectors stream view-dependently like everything else.
+ for (S32 vi = 0; vi < imagep->getNumVolumes(LLRender::LIGHT_TEX); ++vi)
+ {
+ LLVOVolume* volume = (*imagep->getVolumeList(LLRender::LIGHT_TEX))[vi];
+ F32 cov = llmin(volume->getSpotLightPriority(), LLViewerTexture::sWindowPixelArea);
+ if (cov > 0.f)
+ {
+ channel_coverage[1] = llmax(channel_coverage[1], cov);
+ channel_coverage_min[1] = llmin(channel_coverage_min[1], cov);
+ max_coverage = llmax(max_coverage, cov);
+ }
+ }
+
imagep->addTextureStats(max_coverage);
- // Publish per-bucket coverage for LLViewerLODTexture::computeDesiredDiscard.
+ // Publish per-bucket coverage bounds for
+ // LLViewerLODTexture::computeDesiredDiscard.
for (S32 b = 0; b < 4; ++b)
{
imagep->mChannelCoverage[b] = channel_coverage[b];
+ imagep->mChannelCoverageMin[b] = (channel_coverage_min[b] == FLT_MAX) ? 0.f : channel_coverage_min[b];
}
}
@@ -1030,13 +1217,6 @@ void LLViewerTextureList::updateImageDecodePriority(LLViewerFetchedTexture* imag
imagep->getFullWidth()));
#endif
- // make sure to addTextureStats for any spotlights that are using this texture
- for (S32 vi = 0; vi < imagep->getNumVolumes(LLRender::LIGHT_TEX); ++vi)
- {
- LLVOVolume* volume = (*imagep->getVolumeList(LLRender::LIGHT_TEX))[vi];
- volume->updateSpotLightPriority();
- }
-
F32 max_inactive_time = 20.f; // inactive time before deleting saved raw image
S32 min_refs = 3; // 1 for mImageList, 1 for mUUIDMap, and 1 for "entries" in updateImagesFetchTextures
diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp
index 3b41ccb6fc..d57456aa5a 100644
--- a/indra/newview/llvovolume.cpp
+++ b/indra/newview/llvovolume.cpp
@@ -878,11 +878,15 @@ void LLVOVolume::updateTextureVirtualSize(bool forced)
LLViewerFetchedTexture* img = LLViewerTextureManager::staticCastToFetchedTexture(imagep) ;
if(img)
{
- debug_text << img->getDiscardLevel() << ":" << img->getDesiredDiscardLevel() << ":" << img->getWidth() << ":" << (S32) sqrtf(vsize) << ":" << (S32) sqrtf(img->getMaxVirtualSize()) << "\n";
- /*F32 pri = img->getDecodePriority();
- pri = llmax(pri, 0.0f);
- if (pri < min_vsize) min_vsize = pri;
- if (pri > max_vsize) max_vsize = pri;*/
+ // cur:desired:width then per-bucket coverage bounds
+ // (N/BC/S/E, sqrt so values read as pixel dimensions,
+ // max~min) - the exact inputs computeDesiredDiscard sees.
+ debug_text << img->getDiscardLevel() << ":" << img->getDesiredDiscardLevel() << ":" << img->getWidth()
+ << " N" << (S32)sqrtf(img->getChannelCoverage(0)) << "~" << (S32)sqrtf(img->getChannelCoverageMin(0))
+ << " BC" << (S32)sqrtf(img->getChannelCoverage(1)) << "~" << (S32)sqrtf(img->getChannelCoverageMin(1))
+ << " S" << (S32)sqrtf(img->getChannelCoverage(2)) << "~" << (S32)sqrtf(img->getChannelCoverageMin(2))
+ << " E" << (S32)sqrtf(img->getChannelCoverage(3)) << "~" << (S32)sqrtf(img->getChannelCoverageMin(3))
+ << "\n";
}
}
else if (gPipeline.hasRenderDebugMask(LLPipeline::RENDER_DEBUG_FACE_AREA))
@@ -928,7 +932,7 @@ void LLVOVolume::updateTextureVirtualSize(bool forced)
{
LLLightImageParams* params = getLightImageParams();
LLUUID id = params->getLightTexture();
- mLightTexture = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE);
+ mLightTexture = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
if (mLightTexture.notNull())
{
F32 rad = getLightRadius();
@@ -1797,6 +1801,41 @@ void LLVOVolume::regenFaces()
facep->setNormalMap(getTENormalMap(i));
facep->setSpecularMap(getTESpecularMap(i));
}
+
+ // Register PBR channel textures HERE, at geometry build, exactly when
+ // the Blinn textures above register - mirrored from rebuildGeom
+ // (which still re-runs this when the material resolves later).
+ // Without this, PBR textures had zero registered faces until the
+ // spatial group's budget-throttled rebuildGeom ran, so the streaming
+ // math read "not measured -> deepest mip" for PBR content while
+ // identical Blinn content on the same geometry measured immediately.
+ {
+ const LLTextureEntry* te = facep->getTextureEntry();
+ LLFetchedGLTFMaterial* gltf_mat = te ? (LLFetchedGLTFMaterial*)te->getGLTFRenderMaterial() : nullptr;
+ if (gltf_mat)
+ {
+ if (!facep->hasMedia())
+ {
+ facep->setTexture(LLRender::DIFFUSE_MAP, nullptr);
+ }
+ facep->setTexture(LLRender::NORMAL_MAP, nullptr);
+ facep->setTexture(LLRender::SPECULAR_MAP, nullptr);
+ 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);
+ }
+ else
+ {
+ // No (or no longer a) PBR material: clear any stale GLTF
+ // channel registrations so a removed material's textures
+ // stop accruing phantom coverage from this face.
+ facep->setTexture(LLRender::BASECOLOR_MAP, nullptr);
+ facep->setTexture(LLRender::GLTF_NORMAL_MAP, nullptr);
+ facep->setTexture(LLRender::METALLIC_ROUGHNESS_MAP, nullptr);
+ facep->setTexture(LLRender::EMISSIVE_MAP, nullptr);
+ }
+ }
facep->setViewerObject(this);
// If the face had media on it, this will have broken the link between the LLViewerMediaTexture and the face.
@@ -3367,7 +3406,7 @@ LLViewerTexture* LLVOVolume::getLightTexture()
{
if (mLightTexture.isNull() || id != mLightTexture->getID())
{
- mLightTexture = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE);
+ mLightTexture = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE);
}
}
else
@@ -5851,6 +5890,16 @@ void LLVolumeGeometryManager::rebuildGeom(LLSpatialGroup* group)
facep->setTexture(LLRender::METALLIC_ROUGHNESS_MAP, gltf_mat->mMetallicRoughnessTexture);
facep->setTexture(LLRender::EMISSIVE_MAP, gltf_mat->mEmissiveTexture);
}
+ else
+ {
+ // Face is not (or no longer) PBR: clear any stale GLTF
+ // channel registrations, or a removed material's textures
+ // keep accruing phantom coverage from this face forever.
+ facep->setTexture(LLRender::BASECOLOR_MAP, nullptr);
+ facep->setTexture(LLRender::GLTF_NORMAL_MAP, nullptr);
+ facep->setTexture(LLRender::METALLIC_ROUGHNESS_MAP, nullptr);
+ facep->setTexture(LLRender::EMISSIVE_MAP, nullptr);
+ }
//ALWAYS null out vertex buffer on rebuild -- if the face lands in a render
// batch, it will recover its vertex buffer reference from the spatial group