mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Perf] (MG_State): answer texture sampling completeness from a memo
Every draw asks, for every bound texture, whether it is mipmap-complete for the filter in use, and the answer was recomputed from scratch each time: walk the level chain, read each level's texel size, verify each is half the previous. With the Minecraft-shaped bench that walk plus the GetTexelSize calls under it measured about 8% of the render thread on both backends. The answer depends only on the texture's shape - internal format, stored level set, level sizes, level range - and never on its texel content, which is the thing that actually changes between draws. A shape version now moves on exactly those four mutations (SetInternalFormat, SetBaseLevel/SetMaxLevel, and the AllocateStorage/TruncateMipmapLevels pair on both mipmap storage classes), and the completeness answer is memoised against it, one slot for the mipmapped question and one for the plain one. An upload leaves the memo standing, which is the whole point; anything that could change the answer invalidates it. ns per draw, DriverBench on a GTX 1660 SUPER (native / Espryt / Magma): mc_vanilla_draw 257 / 2201->2037 / 1550->1346, mc_ubo_range 203 / 1832->1684 / 1089->934, mc_sampler_churn 272 / 2349->2325 / 1533->1396. Texture-upload cases are unchanged, as expected - they were never asking this question in a loop. Unit tests 421/421.
This commit is contained in:
@@ -16,6 +16,10 @@ namespace MobileGL {
|
||||
namespace GLState {
|
||||
static std::atomic<Uint64> s_nextTextureLifetimeId = 1;
|
||||
|
||||
// Defined further down next to the other sampling-completeness rules; the
|
||||
// memo in TextureObjectBase is its only caller.
|
||||
static Bool ComputeMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped);
|
||||
|
||||
// TextureObjectBase implementations
|
||||
Uint64 TextureObjectBase::AllocateLifetimeId() {
|
||||
return s_nextTextureLifetimeId.fetch_add(1, std::memory_order_relaxed);
|
||||
@@ -81,6 +85,7 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
m_internalFormat = format;
|
||||
++m_shapeVersion;
|
||||
++m_textureParamsVersion;
|
||||
}
|
||||
|
||||
@@ -192,6 +197,7 @@ namespace MobileGL {
|
||||
m_levelRange.y() = m_levelRange.x();
|
||||
}
|
||||
++m_textureParamsVersion;
|
||||
++m_shapeVersion;
|
||||
}
|
||||
|
||||
void TextureObjectBase::SetMaxLevel(Uint maxLevel) {
|
||||
@@ -202,6 +208,7 @@ namespace MobileGL {
|
||||
|
||||
m_levelRange.y() = maxLevel;
|
||||
++m_textureParamsVersion;
|
||||
++m_shapeVersion;
|
||||
}
|
||||
|
||||
Bool TextureObjectBase::IsImmutable() const {
|
||||
@@ -231,6 +238,17 @@ namespace MobileGL {
|
||||
return m_contentVersion;
|
||||
}
|
||||
|
||||
Bool TextureObjectBase::IsMipmapCompleteForFilterCached(Bool mipmapped) const {
|
||||
const int slot = mipmapped ? 1 : 0;
|
||||
if (m_completeMemoShapeVersion[slot] == m_shapeVersion) {
|
||||
return m_completeMemoValue[slot];
|
||||
}
|
||||
const Bool value = ComputeMipmapCompleteForFilter(this, mipmapped);
|
||||
m_completeMemoShapeVersion[slot] = m_shapeVersion;
|
||||
m_completeMemoValue[slot] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
void TextureObjectBase::BumpContentVersion() {
|
||||
++m_contentVersion;
|
||||
}
|
||||
@@ -273,10 +291,12 @@ namespace MobileGL {
|
||||
|
||||
void TextureObjectWithOneMipmap::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
MipmapInput input) {
|
||||
++m_shapeVersion;
|
||||
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||
++m_shapeVersion;
|
||||
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||
}
|
||||
|
||||
@@ -373,13 +393,18 @@ namespace MobileGL {
|
||||
|
||||
// TODO: add other texture types as needed
|
||||
|
||||
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
|
||||
if (texture == nullptr) return true;
|
||||
return texture->IsMipmapCompleteForFilterCached(mipmapped);
|
||||
}
|
||||
|
||||
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) {
|
||||
const Bool mipmapped =
|
||||
effectiveSampler != nullptr && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
|
||||
return !IsMipmapCompleteForFilter(texture, mipmapped);
|
||||
}
|
||||
|
||||
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
|
||||
static Bool ComputeMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
|
||||
if (texture == nullptr) return true;
|
||||
if (!texture->IsComplete()) return false;
|
||||
if (!mipmapped) return true;
|
||||
|
||||
@@ -55,6 +55,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
// Backends compare it against a per-resource snapshot to skip re-syncing unchanged
|
||||
// textures across draws (e.g. the block atlas bound across a whole terrain batch).
|
||||
virtual Uint64 GetContentVersion() const = 0;
|
||||
// Answers IsMipmapCompleteForFilter() from a memo. Sampling completeness is a
|
||||
// property of the texture's SHAPE - level sizes, level count, level range,
|
||||
// internal format - and never of its texel content, but every draw asks about
|
||||
// every bound texture, which made recomputing it one of the hottest things both
|
||||
// backends did (the walk plus its GetTexelSize calls measured ~8% of the render
|
||||
// thread). Shape mutations invalidate the memo; uploads do not.
|
||||
virtual Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const = 0;
|
||||
virtual Int GetSamples() const = 0;
|
||||
virtual void SetSamples(Int samples) = 0;
|
||||
virtual Bool HasFixedSampleLocations() const = 0;
|
||||
@@ -99,6 +106,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
void SetImmutableLevels(Uint levels) override;
|
||||
Uint16 GetTextureParamsVersion() const override;
|
||||
Uint64 GetContentVersion() const override;
|
||||
Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const override;
|
||||
// Bumps the content version without touching per-level storage-dirty flags. Used when the
|
||||
// set of defined mip levels grows via GPU-side mip generation (glGenerateMipmap): the level
|
||||
// set changed (so a cached sampled view's level range is stale) but no CPU data is dirty.
|
||||
@@ -124,6 +132,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
UintVec2 m_levelRange = {0, 1000};
|
||||
Uint m_immutableLevels = 0;
|
||||
Uint16 m_textureParamsVersion = 0;
|
||||
// Bumped by every mutation the completeness answer depends on - internal
|
||||
// format, level range, and the stored level set - and by nothing else, so a
|
||||
// texel upload leaves the memo below valid.
|
||||
Uint64 m_shapeVersion = 1;
|
||||
// [0] = the non-mipmapped answer, [1] = the mipmapped one. Mutable because
|
||||
// completeness is a query; a zero version means "never computed".
|
||||
mutable Uint64 m_completeMemoShapeVersion[2] = {0, 0};
|
||||
mutable Bool m_completeMemoValue[2] = {false, false};
|
||||
// Starts at 1 so a freshly-created backend resource (snapshot 0) never spuriously
|
||||
// matches before its first sync. Bumped only on dirty=true in MarkStorageDirty.
|
||||
Uint64 m_contentVersion = 1;
|
||||
|
||||
@@ -28,10 +28,12 @@ namespace MobileGL {
|
||||
|
||||
void TextureObject2DCube::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
MipmapInput input) {
|
||||
++m_shapeVersion;
|
||||
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||
}
|
||||
|
||||
void TextureObject2DCube::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||
++m_shapeVersion;
|
||||
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user