Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions indra/llrender/llgltexture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ LLGLTexture::LLGLTexture(const LLImageRaw* raw, bool usemipmaps)
mUseMipMaps = usemipmaps ;
// Create an empty image of the specified size and width
mGLTexturep = new LLImageGL(raw, usemipmaps) ;
mGLTexturep->setOwner(this);
mFullWidth = mGLTexturep->getWidth();
mFullHeight = mGLTexturep->getHeight();
mComponents = mGLTexturep->getComponents();
Expand All @@ -57,6 +58,11 @@ LLGLTexture::LLGLTexture(const LLImageRaw* raw, bool usemipmaps)

LLGLTexture::~LLGLTexture()
{
// A deferred alpha analysis may outlive this texture, with the GL texture it holds.
if (mGLTexturep && mGLTexturep->getOwner() == this)
{
mGLTexturep->setOwner(nullptr);
}
cleanup();
}

Expand Down Expand Up @@ -131,6 +137,7 @@ void LLGLTexture::generateGLTexture()
if(mGLTexturep.isNull())
{
mGLTexturep = new LLImageGL(mFullWidth, mFullHeight, mComponents, mUseMipMaps) ;
mGLTexturep->setOwner(this);
}
}

Expand Down
3 changes: 3 additions & 0 deletions indra/llrender/llgltexture.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ class LLGLTexture : public LLTexture

virtual void dump(); // debug info to LL_INFOS()

// The GL texture's alpha mask answer changed (LLImageGL::getIsAlphaMask), on the main thread.
virtual void onAlphaMaskChanged() {}

virtual const LLUUID& getID() const;

void setBoostLevel(S32 level);
Expand Down
33 changes: 30 additions & 3 deletions indra/llrender/llimagegl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include "linden_common.h"

#include "llimagegl.h"
#include "llgltexture.h"

#include "llerror.h"
#include "llimage.h"
Expand Down Expand Up @@ -2655,18 +2656,37 @@ void LLImageGL::setAlphaAnalysis(const AlphaAnalysis& analysis)
return;
}

mIsMask = analysis.mIsMask;
setIsMask(analysis.mIsMask);
mMaxAlpha = analysis.mMaxAlpha;
mMinAlpha = analysis.mMinAlpha;
}

void LLImageGL::clearAlphaAnalysis()
{
// Provisional, while an analysis is out: no answer, so the owner is not told, and the next answer is weighed
// against the last one, and against this if anything read it meanwhile.
mIsMask = false;
mProvisionalRead.store(false, std::memory_order_relaxed);
mMaxAlpha = 255;
mMinAlpha = 0;
}

void LLImageGL::setIsMask(bool is_mask)
{
// A face built while the provisional answer stood went by it, as by an answer.
const bool provisional_read = mProvisionalRead.exchange(false, std::memory_order_relaxed);
const bool changed = mAnsweredMask != is_mask || (provisional_read && is_mask);
mIsMask = is_mask;
if (changed)
{
mAnsweredMask = is_mask;
if (mOwner && on_main_thread())
{
mOwner->onAlphaMaskChanged();
}
}
}

void LLImageGL::publishPendingAlpha()
{
if (mPendingAlpha.mHasPickMask)
Expand All @@ -2679,7 +2699,7 @@ void LLImageGL::publishPendingAlpha()
}
if (mPendingAlpha.mHasAnalysis)
{
mIsMask = mPendingAlpha.mAnalysis.mIsMask;
setIsMask(mPendingAlpha.mAnalysis.mIsMask);
mMaxAlpha = mPendingAlpha.mAnalysis.mMaxAlpha;
mMinAlpha = mPendingAlpha.mAnalysis.mMinAlpha;
}
Expand Down Expand Up @@ -3104,6 +3124,11 @@ bool LLImageGL::getBoundRecently() const
bool LLImageGL::getIsAlphaMask() const
{
llassert_always(!sSkipAnalyzeAlpha);
if (mIsMask != mAnsweredMask)
{
// the provisional no-mask of a mask texture, which a face built now goes by
mProvisionalRead.store(true, std::memory_order_relaxed);
}
return mIsMask;
}

Expand All @@ -3130,6 +3155,8 @@ void LLImageGL::setNeedsAlphaAndPickMask(bool need_mask)
{
mAlphaOffset = INVALID_OFFSET ;
clearAlphaAnalysis();
// no mask from now on: an answer
setIsMask(false);

// Invalidate pending jobs
++mAlphaAnalysisSerial;
Expand Down Expand Up @@ -3563,7 +3590,7 @@ void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h)
// Only apply if no newer analysis has been requested
if (mAlphaAnalysisSerial == request_serial)
{
mIsMask = analysis.mIsMask;
setIsMask(analysis.mIsMask);
mMaxAlpha = analysis.mMaxAlpha;
mMinAlpha = analysis.mMinAlpha;
}
Expand Down
12 changes: 12 additions & 0 deletions indra/llrender/llimagegl.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
#define LL_IMAGEGL_THREAD_CHECK 0 //set to 1 to enable thread debugging for ImageGL

class LLWindow;
class LLGLTexture;

#define BYTES_TO_MEGA_BYTES(x) ((x) >> 20)
#define MEGA_BYTES_TO_BYTES(x) ((x) << 20)
Expand Down Expand Up @@ -371,6 +372,13 @@ class LLImageGL : public LLThreadSafeRefCount
U64 getContentStamp() const { return mContentStamp.load(std::memory_order_relaxed); }

bool getIsAlphaMask() const;
// The texture this one was made for, told on the main thread whenever an analysis answers getIsAlphaMask
// differently from the last one (LLGLTexture::onAlphaMaskChanged): a face built into a pass by the old answer, or
// while a deferred analysis was out and the texture counted as no mask, belongs in another by the new one. The
// provisional no-mask while an analysis is out is no answer, but an answer of mask after it was read is a change
// for the faces built by it. Cleared by the owner as it goes.
void setOwner(LLGLTexture* owner) { mOwner = owner; }
LLGLTexture* getOwner() const { return mOwner; }
// The highest alpha of the level its alpha was last analysed at (analyzeAlpha), or 255 where it was not: no
// alpha channel, an alpha it cannot read, or an analysis not yet back.
U8 getMaxAlpha() const { return mMaxAlpha; }
Expand Down Expand Up @@ -468,6 +476,7 @@ class LLImageGL : public LLThreadSafeRefCount

// Sets the published analysis to what a texture without a readable alpha holds: no mask, and opaque.
void clearAlphaAnalysis();
void setIsMask(bool is_mask);
void publishPendingAlpha();
void discardPendingAlpha();

Expand All @@ -486,6 +495,9 @@ class LLImageGL : public LLThreadSafeRefCount
bool mAutoGenMips = false;

bool mIsMask;
bool mAnsweredMask = false; // what the last analysis answered, mIsMask but for the provisional clear
mutable std::atomic<bool> mProvisionalRead{ false }; // getIsAlphaMask answered the provisional no-mask
LLGLTexture* mOwner = nullptr;
U8 mMaxAlpha;
U8 mMinAlpha;
bool mNeedsAlphaAndPickMask;
Expand Down
62 changes: 62 additions & 0 deletions indra/llrender/llvertexbuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,9 @@ class LLVBOPool
// Of those, the stores kept for reuse.
virtual U64 getVramBytesPooled() const = 0;
virtual LLVertexBuffer::PoolStats getStats() const = 0;
// Frees kept stores freed at least `age` ago that the GPU is done with, until LLTimer::getTotalSeconds() reaches
// deadline. True while any is kept.
virtual bool trim(std::chrono::steady_clock::duration age, F64 deadline) { return false; }
};

// VBO Pool for Apple GPUs (as in M1/M2 etc, not Intel macs)
Expand Down Expand Up @@ -639,6 +642,34 @@ class LLDefaultVBOPool final : public LLVBOPool

}

bool trim(std::chrono::steady_clock::duration age, F64 deadline) override
{
const Time cutoff = std::chrono::steady_clock::now() - age;
for (auto* pool : { &mVBOPool, &mIBOPool })
{
for (Pool::iterator iter = pool->begin(); iter != pool->end(); )
{
// oldest at the back
auto& entries = iter->second;
while (!entries.empty() && entries.back().mAge < cutoff && finished_with(entries.back().mFrame))
{
if (LLTimer::getTotalSeconds() >= deadline)
{
return true;
}
auto& entry = entries.back();
ll_aligned_free_16(entry.mData);
delete_buffers(1, &entry.mGLName);
llassert(mReserved >= iter->first);
mReserved -= iter->first;
entries.pop_back();
}
iter = entries.empty() ? pool->erase(iter) : std::next(iter);
}
}
return mReserved > 0;
}

// clean periodically (clean gets called for every alloc/free)
void clean()
{
Expand Down Expand Up @@ -806,6 +837,37 @@ LLVertexBuffer::PoolStats LLVertexBuffer::getPoolStats()
return sVBOPool ? sVBOPool->getStats() : PoolStats();
}

// Until when updatePoolTrim trims the pool, 0 when it does not.
static F64 sPoolTrimUntil = 0.0;

//static
void LLVertexBuffer::trimPoolSoon()
{
// A store freed in the first five seconds is a second old before the window closes.
constexpr F64 POOL_TRIM_SECONDS = 6.0;
sPoolTrimUntil = LLTimer::getTotalSeconds() + POOL_TRIM_SECONDS;
}

//static
void LLVertexBuffer::updatePoolTrim()
{
if (sPoolTrimUntil == 0.0 || !sVBOPool)
{
return;
}
const F64 now = LLTimer::getTotalSeconds();
if (now >= sPoolTrimUntil)
{
sPoolTrimUntil = 0.0;
return;
}
LL_PROFILE_ZONE_SCOPED_CATEGORY_VERTEX;
// A store kept less than a second is likely to be taken again, as LODs and rebuilds swap geometry.
using namespace std::chrono_literals;
constexpr F64 POOL_TRIM_BUDGET = 0.0005;
sVBOPool->trim(1s, now + POOL_TRIM_BUDGET);
}

//============================================================================
//
//static
Expand Down
7 changes: 7 additions & 0 deletions indra/llrender/llvertexbuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,13 @@ class LLVertexBuffer final : public LLRefCount
// Of those, the ones it keeps for reuse.
static U64 getBytesPooled();

// Lets the stores the pool keeps go over the next few seconds, rather than at its next clean, once they have been
// kept a second and the GPU is done with them: for when a lot of geometry has just been freed, a removed region's
// or the place a teleport left. A frame's budget at a time, in updatePoolTrim.
static void trimPoolSoon();
// Once a frame, on the main thread.
static void updatePoolTrim();

// What the buffer pool has handed out since it was made: stores it had to make, stores it reused, and of those the
// ones freed in a frame the GPU had not been shown finished with, which it gave a new store under the same name.
struct PoolStats
Expand Down
98 changes: 98 additions & 0 deletions indra/llrender/tests/llimagegl_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#include "../algpubuffer.h"
#include "../alstagingring.h"
#include "../alvramusage.h"
#include "../llgltexture.h"
#include "../llimagegl.h"
#include "../llgl.h"
#include "../llrender.h"
Expand Down Expand Up @@ -1959,4 +1960,101 @@ namespace
REQUIRE(LLImageGL::dataFormatComponents(entry.first) == entry.second);
}
}

// A texture that counts what its GL texture tells it of the mask answer.
struct MaskWatcher : public LLGLTexture
{
MaskWatcher() : LLGLTexture(32, 32, 4, false) {}
void onAlphaMaskChanged() override { ++mChanges; }
S32 mChanges = 0;
};

// A square RGBA image whose alpha is 0 in its top half and opaque below (a mask), or 128 everywhere (no mask).
LLPointer<LLImageRaw> makeAlphaRaw(bool mask, U16 side = 32)
{
LLPointer<LLImageRaw> raw = new LLImageRaw(side, side, 4);
U8* data = raw->getData();
for (U32 i = 0; i < (U32)side * side; ++i)
{
data[i * 4 + 0] = data[i * 4 + 1] = data[i * 4 + 2] = 0x80;
data[i * 4 + 3] = mask ? (i < (U32)side * side / 2 ? 0 : 255) : 128;
}
return raw;
}

// The faces of a texture are built into pools by its mask answer, so the texture hears of every change of it, and
// only of a change; once it is gone, the GL texture it made tells nobody.
TEST_CASE_METHOD(llimagegl_data, "LLImageGL: the texture it was made for hears each change of its mask answer", "[LLImageGL]")
{
LLPointer<MaskWatcher> owner = new MaskWatcher();
owner->generateGLTexture();
LLPointer<LLImageGL> img = owner->getGLTexture();
REQUIRE(img->getOwner() == owner.get());

// Small enough to be analysed as it uploads, not on a worker.
REQUIRE(img->createGLTexture(0, makeAlphaRaw(true).get()));
CHECK(img->getIsAlphaMask());
CHECK(owner->mChanges == 1);

UNSCOPED_INFO("the same answer again is no change");
REQUIRE(img->createGLTexture(0, makeAlphaRaw(true).get()));
CHECK(owner->mChanges == 1);

REQUIRE(img->createGLTexture(0, makeAlphaRaw(false).get()));
CHECK_FALSE(img->getIsAlphaMask());
CHECK(owner->mChanges == 2);

owner = nullptr;
CHECK(img->getOwner() == nullptr);
REQUIRE(img->createGLTexture(0, makeAlphaRaw(true).get()));
CHECK(img->getIsAlphaMask());
}

// An image of 64 px or more is analysed on a worker, and counts as no mask until the answer comes back. That
// provisional no-mask is no answer: the texture hears of an answer that differs from the last one, or of a mask
// answer after the provisional no-mask was read.
TEST_CASE_METHOD(llimagegl_data, "LLImageGL: a deferred alpha analysis tells the texture its answer when it comes back", "[LLImageGL]")
{
// The queues the analysis is posted to and answered on, as the viewer's. The main one has to exist before the
// image, which looks it up when built.
LL::WorkQueue mainloop("mainloop");
LL::WorkQueue general("General");
auto settle = [&]()
{
for (U32 i = 0; i < 16; ++i)
{
general.runPending();
mainloop.runPending();
}
};

LLPointer<MaskWatcher> owner = new MaskWatcher();
owner->generateGLTexture();
LLPointer<LLImageGL> img = owner->getGLTexture();

REQUIRE(img->createGLTexture(0, makeAlphaRaw(true, 64).get()));
CHECK_FALSE(img->getIsAlphaMask());
CHECK(owner->mChanges == 0);
settle();
CHECK(img->getIsAlphaMask());
CHECK(owner->mChanges == 1);

UNSCOPED_INFO("the same answer after another deferral, its provisional no-mask unread, is no change");
REQUIRE(img->createGLTexture(0, makeAlphaRaw(true, 64).get()));
settle();
CHECK(img->getIsAlphaMask());
CHECK(owner->mChanges == 1);

UNSCOPED_INFO("read meanwhile it is one: a face built by the provisional no-mask went to the blend pass");
REQUIRE(img->createGLTexture(0, makeAlphaRaw(true, 64).get()));
CHECK_FALSE(img->getIsAlphaMask());
settle();
CHECK(img->getIsAlphaMask());
CHECK(owner->mChanges == 2);

REQUIRE(img->createGLTexture(0, makeAlphaRaw(false, 64).get()));
settle();
CHECK_FALSE(img->getIsAlphaMask());
CHECK(owner->mChanges == 3);
}
}
Loading
Loading