diff options
| author | Jonathan "Geenz" Goodman <geenz@lindenlab.com> | 2026-05-22 13:13:30 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-22 13:13:30 -0400 |
| commit | 6b4e3f3288ec1e9917cecd862a7a52945e5b4db2 (patch) | |
| tree | eb7f6952eaa5e78326308882db39500a4f149271 /indra/llrender | |
| parent | 99ab6316b4ae9058f22d9f57d21e795ca45797fd (diff) | |
| parent | dad44ba5d67d04a73708a9a25bbe1ddec29a6a9a (diff) | |
Merge pull request #5829 from secondlife/geenz/texture-quality
Texture streaming rework
Diffstat (limited to 'indra/llrender')
| -rw-r--r-- | indra/llrender/llimagegl.cpp | 96 | ||||
| -rw-r--r-- | indra/llrender/llimagegl.h | 17 | ||||
| -rw-r--r-- | indra/llrender/llrender.cpp | 29 |
3 files changed, 125 insertions, 17 deletions
diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 4a3d32c7ff..6bcc34938c 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -66,8 +66,8 @@ static LLMutex sTexMemMutex; static std::unordered_map<U32, U64> sTextureAllocs; static U64 sTextureBytes = 0; -// track a texture alloc on the currently bound texture. -// asserts that no currently tracked alloc exists +// Per-mip upload paths call this once per level; only free_tex_image +// removes a texture's accounting entirely. void LLImageGLMemory::alloc_tex_image(U32 width, U32 height, U32 intformat, U32 count) { U32 texUnit = gGL.getCurrentTexUnitIndex(); @@ -80,15 +80,46 @@ void LLImageGLMemory::alloc_tex_image(U32 width, U32 height, U32 intformat, U32 sTexMemMutex.lock(); - // it is a precondition that no existing allocation exists for this texture - llassert(sTextureAllocs.find(texName) == sTextureAllocs.end()); - - sTextureAllocs[texName] = size; + auto iter = sTextureAllocs.find(texName); + if (iter != sTextureAllocs.end()) + { + iter->second += size; + } + else + { + sTextureAllocs[texName] = size; + } sTextureBytes += size; sTexMemMutex.unlock(); } +// Add mip 1..N bytes to existing accounting. Use after glGenerateMipmap. +void LLImageGLMemory::account_extra_mip_bytes(U32 base_width, U32 base_height, U32 intformat) +{ + U64 extra = 0; + U32 w = base_width; + U32 h = base_height; + while (w > 1 || h > 1) + { + w = w > 1 ? w >> 1 : 1; + h = h > 1 ? h >> 1 : 1; + extra += LLImageGL::dataFormatBytes(intformat, w, h); + } + + U32 texUnit = gGL.getCurrentTexUnitIndex(); + U32 texName = gGL.getTexUnit(texUnit)->getCurrTexture(); + + sTexMemMutex.lock(); + auto iter = sTextureAllocs.find(texName); + if (iter != sTextureAllocs.end()) + { + iter->second += extra; + sTextureBytes += extra; + } + sTexMemMutex.unlock(); +} + // track texture free on given texName void LLImageGLMemory::free_tex_image(U32 texName) { @@ -684,7 +715,10 @@ void LLImageGL::dump() //---------------------------------------------------------------------------- void LLImageGL::forceUpdateBindStats(void) const { - mLastBindTime = sLastFrameTime; + // Intentionally a no-op: mLastBindTime is written only by real bind + // paths so the staleness signal reflects actual GPU use. Callers that + // still invoke this (avatar "keep alive" sites, deleted-texture + // fallback) no longer falsely refresh staleness. } bool LLImageGL::updateBindStats() const @@ -838,7 +872,7 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 mMipLevels = wpo2(llmax(w, h)); //use legacy mipmap generation mode (note: making this condional can cause rendering issues) - // -- but making it not conditional triggers deprecation warnings when core profile is enabled + // - but making it not conditional triggers deprecation warnings when core profile is enabled // (some rendering issues while core profile is enabled are acceptable at this point in time) if (!LLRender::sGLCoreProfile) { @@ -864,6 +898,7 @@ bool LLImageGL::setImage(const U8* data_in, bool data_hasmips /* = false */, S32 { LL_PROFILE_GPU_ZONE("generate mip map"); glGenerateMipmap(mTarget); + account_extra_mip_bytes(w, h, mFormatInternal); } stop_glerror(); } @@ -1461,7 +1496,12 @@ void LLImageGL::setManualImage(U32 target, S32 miplevel, S32 intformat, S32 widt LL_PROFILE_ZONE_NUM(width); LL_PROFILE_ZONE_NUM(height); - free_cur_tex_image(); + // Release prior accounting only on the base mip; per-mip iteration + // accumulates the rest via the additive alloc_tex_image. + if (miplevel == 0) + { + free_cur_tex_image(); + } const bool use_sub_image = should_stagger_image_set(compress); if (!use_sub_image) { @@ -1613,7 +1653,6 @@ bool LLImageGL::createGLTexture(S32 discard_level, const LLImageRaw* imageraw, S { destroyGLTexture(); mCurrentDiscardLevel = discard_level; - mLastBindTime = sLastFrameTime; mGLTextureCreated = false; return true ; } @@ -1729,9 +1768,7 @@ bool LLImageGL::createGLTexture(S32 discard_level, const U8* data_in, bool data_ mTextureMemory = (S64Bytes)getMipBytes(mCurrentDiscardLevel); - - // mark this as bound at this point, so we don't throw it out immediately - mLastBindTime = sLastFrameTime; + mGLCreateTime = sLastFrameTime; checkActiveThread(); return true; @@ -1858,7 +1895,7 @@ bool LLImageGL::readBackRaw(S32 discard_level, LLImageRaw* imageraw, bool compre LLGLint is_compressed = 0; if (compressed_ok) { - glGetTexLevelParameteriv(mTarget, is_compressed, GL_TEXTURE_COMPRESSED, (GLint*)&is_compressed); + glGetTexLevelParameteriv(mTarget, gl_discard, GL_TEXTURE_COMPRESSED, (GLint*)&is_compressed); } //----------------------------------------------------------------------------------------------- @@ -2039,6 +2076,28 @@ S32 LLImageGL::getWidth(S32 discard_level) const return width; } +// static +S32 LLImageGL::dimDerivedMaxDiscard(S32 width, S32 height) +{ + if (width <= 0 || height <= 0) + { + return 0; + } + // max(w,h) - min() caps short on rectangular textures + // (1024x512 reaches 1x1 at discard 10, not 9). + return (S32)floorf(log2f((F32)llmax(width, height))); +} + +void LLImageGL::stampBound() const +{ + // Skip the store on same-frame re-binds - bindFast is per-draw and + // would dirty this cache line per bind per texture otherwise. + if (mLastBindTime != sLastFrameTime) + { + mLastBindTime = sLastFrameTime; + } +} + S64 LLImageGL::getBytes(S32 discard_level) const { if (discard_level < 0) @@ -2468,7 +2527,12 @@ bool LLImageGL::scaleDown(S32 desired_discard) return false; } - desired_discard = llmin(desired_discard, mMaxDiscardLevel); + // GL pyramid reaches 1x1 regardless of codec levels; + // mMaxDiscardLevel is hardcapped at MAX_DISCARD_LEVEL. + S32 dim_max_discard = (mWidth > 0 && mHeight > 0) + ? dimDerivedMaxDiscard(mWidth, mHeight) + : (S32)mMaxDiscardLevel; + desired_discard = llmin(desired_discard, dim_max_discard); if (desired_discard <= mCurrentDiscardLevel) { @@ -2501,6 +2565,7 @@ bool LLImageGL::scaleDown(S32 desired_discard) LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("scaleDown - glGenerateMipmap"); gGL.getTexUnit(0)->bind(this); glGenerateMipmap(mTarget); + account_extra_mip_bytes(desired_width, desired_height, mFormatInternal); gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); } } @@ -2546,6 +2611,7 @@ bool LLImageGL::scaleDown(S32 desired_discard) { LL_PROFILE_ZONE_NAMED_CATEGORY_TEXTURE("scaleDown - glGenerateMipmap"); glGenerateMipmap(mTarget); + account_extra_mip_bytes(desired_width, desired_height, mFormatInternal); } gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index 6b4492c09e..0c85446b84 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -51,6 +51,11 @@ class LLWindow; namespace LLImageGLMemory { void alloc_tex_image(U32 width, U32 height, U32 intformat, U32 count); + + // Add mip 1..N bytes to existing accounting. Call after glGenerateMipmap + // when only the base mip was accounted; without this the bytes counter + // undercounts mipmap-generated textures by ~25%. + void account_extra_mip_bytes(U32 base_width, U32 base_height, U32 intformat); void free_tex_image(U32 texName); void free_tex_images(U32 count, const U32* texNames); void free_cur_tex_image(); @@ -151,6 +156,15 @@ public: S32 getDiscardLevel() const { return mCurrentDiscardLevel; } S32 getMaxDiscardLevel() const { return mMaxDiscardLevel; } + // floor(log2(max(w, h))) - deepest GL pyramid level (down to 1x1). + // Returns 0 for non-positive inputs. + static S32 dimDerivedMaxDiscard(S32 width, S32 height); + + // Record the wall-clock bind time - every bind path that touches a + // streaming-managed texture must call this, or the staleness signal + // sees the texture as never-bound and ramps it toward eviction. + void stampBound() const; + // override the current discard level // should only be used for local textures where you know exactly what you're doing void setDiscardLevel(S32 level) { mCurrentDiscardLevel = level; } @@ -224,7 +238,8 @@ public: public: // Various GL/Rendering options S64Bytes mTextureMemory; - mutable F32 mLastBindTime; // last time this was bound, by discard level + mutable F32 mLastBindTime = 0.f; // wall-clock time at last stampBound; drives streaming staleness + F32 mGLCreateTime = 0.f; // wall-clock time the GL texture was created; staleness fallback for never-bound textures private: U32 createPickMask(S32 pWidth, S32 pHeight); diff --git a/indra/llrender/llrender.cpp b/indra/llrender/llrender.cpp index 57be8570af..5e845fbcce 100644 --- a/indra/llrender/llrender.cpp +++ b/indra/llrender/llrender.cpp @@ -197,6 +197,10 @@ void LLTexUnit::bindFast(LLTexture* texture) glActiveTexture(GL_TEXTURE0 + mIndex); gGL.mCurrTextureUnitIndex = mIndex; mCurrTexture = gl_tex->getTexName(); + mCurrTexType = gl_tex->getTarget(); + // bindFast bypasses updateBindStats(); stamp directly so the staleness + // signal sees per-frame use of batched textures. + gl_tex->stampBound(); if (!mCurrTexture) { LL_PROFILE_ZONE_NAMED("MISSING TEXTURE"); @@ -249,11 +253,17 @@ bool LLTexUnit::bind(LLTexture* texture, bool for_rendering, bool forceBind) setTextureFilteringOption(gl_tex->mFilterOption); } } + else + { + // Already current - still being used, keep it fresh. + gl_tex->stampBound(); + } } else { //if deleted, will re-generate it immediately texture->forceImmediateUpdate() ; + gl_tex->stampBound(); gl_tex->forceUpdateBindStats() ; return texture->bindDefaultImage(mIndex); @@ -325,6 +335,11 @@ bool LLTexUnit::bind(LLImageGL* texture, bool for_rendering, bool forceBind, S32 stop_glerror(); } } + else + { + // Already current - still being used, keep it fresh. + texture->stampBound(); + } stop_glerror(); @@ -1718,7 +1733,16 @@ LLVertexBuffer* LLRender::genBuffer(U32 attribute_mask, S32 count) LLVertexBuffer * vb = new LLVertexBuffer(attribute_mask); vb->allocateBuffer(count, 0); - vb->setBuffer(); + // Non-Apple path uses glBufferSubData inside setXxxData, so the VBO + // must already be bound. On Apple, the VBO is lazily created in + // _unmapBuffer (LLAppleVBOPool); calling setBuffer() here would bind + // mGLBuffer == 0 and then setupVertexBuffer would issue + // glVertexAttribIPointer with a non-null offset against no bound + // GL_ARRAY_BUFFER -> GL_INVALID_OPERATION in core profile. + if (!gGLManager.mIsApple) + { + vb->setBuffer(); + } vb->setPositionData(mVerticesp.get()); @@ -1733,6 +1757,9 @@ LLVertexBuffer* LLRender::genBuffer(U32 attribute_mask, S32 count) } #if LL_DARWIN + // unmapBuffer creates the GL buffer, uploads, and leaves it bound, + // drawBuffer's later setBuffer() then runs setupVertexBuffer against + // a valid VBO. vb->unmapBuffer(); #endif vb->unbind(); |
