mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 14:48:32 +09:00
[Perf] (MG_State, MG_Impl, MG_Backend): stop paying per draw and per upload for work already known
A per-draw CPU profile of a real Minecraft frame (perf on the render thread, which sits at 100% of one core on both backends) said the deficit is translation overhead, not the GPU, and named where it goes. This removes the largest items it found, on both backends and in the shared frontend they both feed. The single biggest one was not translation at all: IsBackendContextCurrentOnThisThread called eglGetCurrentContext on every invocation, and glvnd answers that with a getpid() fork check - a real syscall. The predicate sits two and three deep in every draw (the deferred-release drain, the global-UBO ring availability check, and the ring allocation), so it accounted for 16.3% of the render thread. EGL is still the ground truth, but re-verifying it once per thread per frame catches an external migration at the next frame boundary rather than the next call, which recovers the same bookkeeping. Texture uploads now carry a dirty region instead of a per-level flag. Minecraft animates atlas sprites with 16x16 glTexSubImage2D calls into a 1024x512 atlas and respecifies the lightmap every frame; a per-level flag turned each of those into a full-level re-upload - about 3.6 MB a frame of texels nobody changed. MipmapStorage accumulates the written box, Espryt uploads it with UNPACK_ROW_LENGTH striding into the level shadow, and Magma stages just that box. The box is a union, not a range list: repeated writes to one level widen it and it degrades to exactly the old whole-level upload, which is the honest worst case. glBufferData(NULL) is the orphaning idiom, and the backend was answering it by uploading the stale CPU shadow - turning a rename the driver does for free into a full synchronized upload. BufferObject now records that a NULL respecify leaves the store undefined, and the upload is skipped until content is actually written. The rest are smaller and of a kind: the deferred-release queue is probed without taking its mutex, the UBO ring waits on the frame fence that frees the space it needs instead of draining the whole pipeline with glFinish at the size cap, VAO binds go through a shadow so a draw's second bind of the same object does not reach the driver, the per-draw clean-texture probe short-circuits on the content version before rebuilding shape info, glUniform drops byte-identical writes (which otherwise dirty the whole UBO for the next draw), re-binding the texture or VAO a slot already holds no longer bumps the generation counters a backend fast path is keyed on, and the texture validators stopped taking shared_ptr by value. On Magma: descriptor-set reuse keeps four entries instead of one, because draws alternating between two programs - the chunk/entity ping-pong - thrashed a single slot into a full re-allocate and re-write every draw; a DynamicDraw buffer whose contents survive two frame boundaries is promoted to resident storage instead of being re-copied into the per-frame arena forever; and sampled-read barriers name only the shader stages whose device feature is enabled, which also removes a latent VUID violation (ALL_GRAPHICS names geometry and tessellation stages a device need not have). Measured with the Minecraft rig (render distance 32, p50 fps, same machine, single sample each): vanilla 1.21.1 Espryt 10.8 -> 36.3 and Magma 31.3 -> 44.6; 26.2 snapshot Magma 114.5 -> 210.5. Fabric+Sodium moved inside noise on Magma (854 -> 766) with the native baseline itself moving 838 -> 1031 between the two sessions, so treat that cell as unresolved rather than a regression measured. Unit tests 421/421. The CTS A/B was not run: these numbers and the test suite are the whole of the evidence, and a conformance regression would not have been caught here.
This commit is contained in:
@@ -41,6 +41,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
void BufferObject::NotifySubData(SizeT offset, SizeT size) {
|
||||
++m_changeSerial;
|
||||
if (size == 0) return;
|
||||
m_hasDefinedContent = true;
|
||||
if (g_bufferBackendOps && g_bufferBackendOps->SubData) {
|
||||
g_bufferBackendOps->SubData(*this, offset, size);
|
||||
}
|
||||
@@ -49,12 +50,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
void BufferObject::NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess) {
|
||||
++m_changeSerial;
|
||||
if (range.start >= range.end) return;
|
||||
m_hasDefinedContent = true;
|
||||
if (g_bufferBackendOps && g_bufferBackendOps->FlushMappedRange) {
|
||||
g_bufferBackendOps->FlushMappedRange(*this, range, appAccess);
|
||||
}
|
||||
}
|
||||
|
||||
void BufferObject::NotifyContentWrite(SizeT offset, SizeT size) {
|
||||
m_hasDefinedContent = true;
|
||||
if (m_resource.IsGpuResident()) {
|
||||
// The write already landed in coherent GPU memory; the backend has no separate
|
||||
// copy to sync. Only bump the serial so cached transient slices invalidate.
|
||||
@@ -71,6 +74,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
if (data && size > 0) {
|
||||
Memcpy(m_resource.Bytes(), data, size);
|
||||
}
|
||||
// A NULL-data respecify (the orphaning idiom) leaves the store undefined;
|
||||
// record that so backends skip uploading the stale shadow bytes.
|
||||
m_hasDefinedContent = (data != nullptr) || size == 0;
|
||||
m_isImmutableStorage = false;
|
||||
m_storageFlags = 0;
|
||||
NotifyRespecify();
|
||||
@@ -89,6 +95,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
} else if (size > 0) {
|
||||
Memset(m_resource.Bytes(), 0, size);
|
||||
}
|
||||
m_hasDefinedContent = true;
|
||||
m_isImmutableStorage = true;
|
||||
m_storageFlags = storageFlags;
|
||||
NotifyRespecify();
|
||||
@@ -175,6 +182,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void BufferObject::MarkGpuWritten() {
|
||||
m_hasDefinedContent = true;
|
||||
m_gpuWritePending = true;
|
||||
}
|
||||
|
||||
@@ -332,6 +340,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
return m_changeSerial;
|
||||
}
|
||||
|
||||
Bool BufferObject::HasDefinedContent() const {
|
||||
return m_hasDefinedContent;
|
||||
}
|
||||
|
||||
const SharedPtr<BackendBufferResource>& BufferObject::GetBackendResource() const {
|
||||
return m_resource.Backend();
|
||||
}
|
||||
|
||||
@@ -188,6 +188,11 @@ namespace MobileGL {
|
||||
// Monotonic counter bumped on every shadow mutation; backends use it to
|
||||
// validate cached transient slices.
|
||||
Uint64 GetChangeSerial() const;
|
||||
// False after a NULL-data (re)specification until the first content
|
||||
// write: the app's orphaning idiom (glBufferData with nullptr) leaves
|
||||
// the store undefined, so backends may (re)allocate GPU storage without
|
||||
// uploading the stale CPU shadow.
|
||||
Bool HasDefinedContent() const;
|
||||
|
||||
const SharedPtr<BackendBufferResource>& GetBackendResource() const;
|
||||
void SetBackendResource(SharedPtr<BackendBufferResource> resource);
|
||||
@@ -213,6 +218,8 @@ namespace MobileGL {
|
||||
Bool m_isImmutableStorage = false;
|
||||
GLbitfield m_storageFlags = 0;
|
||||
Uint64 m_changeSerial = 0;
|
||||
// See HasDefinedContent().
|
||||
Bool m_hasDefinedContent = true;
|
||||
// Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed.
|
||||
Bool m_gpuWritePending = false;
|
||||
Range1D m_mappedRange;
|
||||
|
||||
@@ -111,7 +111,9 @@ namespace MobileGL {
|
||||
TextureUnit& GetTextureUnitObject(Int unit);
|
||||
ImageTextureBinding& GetImageTextureBinding(Int unit);
|
||||
const ImageTextureBinding& GetImageTextureBinding(Int unit) const;
|
||||
void NoteTextureUnitTouched(Int unit) { m_textureState.NoteUnitTouched(unit); }
|
||||
void NoteTextureUnitTouched(Int unit, Bool bindingChanged = true) {
|
||||
m_textureState.NoteUnitTouched(unit, bindingChanged);
|
||||
}
|
||||
Int GetMaxTouchedTextureUnit() const { return m_textureState.GetMaxTouchedUnit(); }
|
||||
// Monotonic counter bumped whenever a texture bind/unbind/delete changes which
|
||||
// texture is bound at a unit; lets a backend skip re-resolving an unchanged
|
||||
|
||||
@@ -27,11 +27,23 @@ namespace MobileGL {
|
||||
m_texelSizes.reserve(std::bit_ceil(requiredLevelCount));
|
||||
m_texelSizes.resize(requiredLevelCount);
|
||||
m_isDirty.resize(requiredLevelCount, false);
|
||||
m_dirtyRegions.resize(requiredLevelCount);
|
||||
m_compressedData.resize(requiredLevelCount);
|
||||
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
|
||||
}
|
||||
|
||||
m_texelSizes[level] = input.texelSize;
|
||||
// A respecified level invalidates any pending sub-region: its extents were
|
||||
// measured against the old size. If the level is still flagged dirty the
|
||||
// pending upload widens to the whole (new) level.
|
||||
if (level < m_dirtyRegions.size()) {
|
||||
m_dirtyRegions[level] =
|
||||
m_isDirty[level]
|
||||
? MipmapDirtyRegion{IntVec3{0, 0, 0},
|
||||
IntVec3{input.texelSize.x(), input.texelSize.y(),
|
||||
std::max(input.texelSize.z(), 1)}}
|
||||
: MipmapDirtyRegion{};
|
||||
}
|
||||
auto& data = m_data[level];
|
||||
data.resize(input.byteSize, 0);
|
||||
|
||||
@@ -81,6 +93,7 @@ namespace MobileGL {
|
||||
m_data.resize(levelCount);
|
||||
m_texelSizes.resize(levelCount);
|
||||
m_isDirty.resize(levelCount);
|
||||
m_dirtyRegions.resize(levelCount);
|
||||
m_compressedData.resize(levelCount);
|
||||
m_compressedFormats.resize(levelCount);
|
||||
}
|
||||
@@ -95,7 +108,7 @@ namespace MobileGL {
|
||||
const Uint8* src = static_cast<const Uint8*>(input.data);
|
||||
// Clamp so a size mismatch can never write past the allocation.
|
||||
Memcpy(levelData.data(), src, std::min(input.size, levelData.size()));
|
||||
m_isDirty[level] = true;
|
||||
MarkDirty(level, true); // whole-level write: dirty region covers everything
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,12 +133,51 @@ namespace MobileGL {
|
||||
void MipmapStorage::MarkDirty(Uint level, bool dirty) {
|
||||
MOBILEGL_ASSERT(level < m_isDirty.size(), "MarkDirty: level out of range");
|
||||
m_isDirty[level] = dirty;
|
||||
if (level < m_dirtyRegions.size()) {
|
||||
if (dirty) {
|
||||
const IntVec3 size = level < m_texelSizes.size() ? m_texelSizes[level] : IntVec3{0, 0, 0};
|
||||
m_dirtyRegions[level] = {IntVec3{0, 0, 0},
|
||||
IntVec3{size.x(), size.y(), std::max(size.z(), 1)}};
|
||||
} else {
|
||||
m_dirtyRegions[level] = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MipmapStorage::IsDirty(Uint level) const {
|
||||
MOBILEGL_ASSERT(level < m_isDirty.size(), "IsDirty: level out of range");
|
||||
return m_isDirty[level];
|
||||
}
|
||||
|
||||
void MipmapStorage::MarkDirtyRegion(Uint level, IntVec3 offset, IntVec3 size) {
|
||||
MOBILEGL_ASSERT(level < m_isDirty.size(), "MarkDirtyRegion: level out of range");
|
||||
const IntVec3 levelSize = level < m_texelSizes.size() ? m_texelSizes[level] : IntVec3{0, 0, 0};
|
||||
MipmapDirtyRegion incoming;
|
||||
incoming.lo = {std::max(offset.x(), 0), std::max(offset.y(), 0), std::max(offset.z(), 0)};
|
||||
incoming.hi = {std::min(offset.x() + size.x(), levelSize.x()),
|
||||
std::min(offset.y() + size.y(), levelSize.y()),
|
||||
std::min(offset.z() + std::max(size.z(), 1), std::max(levelSize.z(), 1))};
|
||||
if (incoming.Empty()) return;
|
||||
if (level < m_dirtyRegions.size()) {
|
||||
MipmapDirtyRegion& region = m_dirtyRegions[level];
|
||||
if (m_isDirty[level] && !region.Empty()) {
|
||||
region.lo = {std::min(region.lo.x(), incoming.lo.x()),
|
||||
std::min(region.lo.y(), incoming.lo.y()),
|
||||
std::min(region.lo.z(), incoming.lo.z())};
|
||||
region.hi = {std::max(region.hi.x(), incoming.hi.x()),
|
||||
std::max(region.hi.y(), incoming.hi.y()),
|
||||
std::max(region.hi.z(), incoming.hi.z())};
|
||||
} else {
|
||||
region = incoming;
|
||||
}
|
||||
}
|
||||
m_isDirty[level] = true;
|
||||
}
|
||||
|
||||
MipmapDirtyRegion MipmapStorage::GetDirtyRegion(Uint level) const {
|
||||
if (level >= m_dirtyRegions.size()) return {};
|
||||
return m_dirtyRegions[level];
|
||||
}
|
||||
} // namespace GLState
|
||||
} // namespace MG_State
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
|
||||
#include "TextureEnum.h"
|
||||
#include "MG_Util/Types.h"
|
||||
#include "MG_Util/Math/VectorTypes.h"
|
||||
@@ -15,6 +17,22 @@
|
||||
namespace MobileGL {
|
||||
namespace MG_State {
|
||||
namespace GLState {
|
||||
// Texel-space bounding box of the shadow bytes a backend has not uploaded
|
||||
// yet, [lo, hi) per axis. Cleared (all zero) while the level is clean. A
|
||||
// box, not a range list: repeated sub-image writes union into one region,
|
||||
// which stays exact for the per-frame "small sub-rect of a big atlas"
|
||||
// pattern this exists for, and degrades to the old full-level upload as
|
||||
// the union grows.
|
||||
struct MipmapDirtyRegion {
|
||||
IntVec3 lo{0, 0, 0};
|
||||
IntVec3 hi{0, 0, 0};
|
||||
Bool Empty() const { return hi.x() <= lo.x() || hi.y() <= lo.y() || hi.z() <= lo.z(); }
|
||||
Bool CoversWholeLevel(const IntVec3& levelSize) const {
|
||||
return lo.x() <= 0 && lo.y() <= 0 && lo.z() <= 0 && hi.x() >= levelSize.x() &&
|
||||
hi.y() >= levelSize.y() && hi.z() >= std::max(levelSize.z(), 1);
|
||||
}
|
||||
};
|
||||
|
||||
class MipmapStorage {
|
||||
public:
|
||||
SizeT GetLevelCount() const;
|
||||
@@ -29,6 +47,12 @@ namespace MobileGL {
|
||||
SizeT GetByteSize(Uint level) const;
|
||||
void MarkDirty(Uint level, bool dirty);
|
||||
bool IsDirty(Uint level) const;
|
||||
// Union a sub-image write's box into the level's pending region and set the
|
||||
// dirty flag. MarkDirty keeps its meaning: true covers the whole level,
|
||||
// false clears the region along with the flag.
|
||||
void MarkDirtyRegion(Uint level, IntVec3 offset, IntVec3 size);
|
||||
// Meaningful only while IsDirty(level).
|
||||
MipmapDirtyRegion GetDirtyRegion(Uint level) const;
|
||||
|
||||
// The bytes an application handed to glCompressedTexImage*, kept verbatim beside the
|
||||
// (uncompressed) texel shadow rather than in place of it. GL 4.6 core 8.11 requires
|
||||
@@ -49,6 +73,7 @@ namespace MobileGL {
|
||||
Vector<IntVec3> m_texelSizes;
|
||||
Vector<Vector<Uint8>> m_data;
|
||||
Vector<bool> m_isDirty;
|
||||
Vector<MipmapDirtyRegion> m_dirtyRegions;
|
||||
Vector<Vector<Uint8>> m_compressedData;
|
||||
Vector<GLenum> m_compressedFormats;
|
||||
};
|
||||
|
||||
@@ -74,6 +74,16 @@ namespace MobileGL {
|
||||
return m_storage[targetIndex].IsDirty(level);
|
||||
}
|
||||
|
||||
void MarkDirtyRegion(Uint targetIndex, Uint level, IntVec3 offset, IntVec3 size) {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "MarkDirtyRegion: target invalid");
|
||||
m_storage[targetIndex].MarkDirtyRegion(level, offset, size);
|
||||
}
|
||||
|
||||
MipmapDirtyRegion GetDirtyRegion(Uint targetIndex, Uint level) const {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetDirtyRegion: target invalid");
|
||||
return m_storage[targetIndex].GetDirtyRegion(level);
|
||||
}
|
||||
|
||||
void SetCompressedImage(Uint targetIndex, Uint level, GLenum internalFormat, const void* data,
|
||||
SizeT size) {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "SetCompressedImage: target invalid");
|
||||
|
||||
@@ -301,6 +301,18 @@ namespace MobileGL {
|
||||
return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
IntVec3 offset, IntVec3 size) {
|
||||
++m_contentVersion;
|
||||
m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset,
|
||||
size);
|
||||
}
|
||||
|
||||
MipmapDirtyRegion TextureObjectWithOneMipmap::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat, const void* data, SizeT size) {
|
||||
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||
|
||||
@@ -150,6 +150,20 @@ namespace MobileGL::MG_State::GLState {
|
||||
virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0;
|
||||
virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0;
|
||||
virtual Bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
|
||||
// Sub-image variant of MarkStorageDirty(..., true): backends may then upload
|
||||
// only the accumulated region instead of the whole level. The base fallback
|
||||
// keeps whole-level semantics for storage classes that do not track regions.
|
||||
virtual void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
|
||||
IntVec3 size) {
|
||||
(void)offset;
|
||||
(void)size;
|
||||
MarkStorageDirty(uploadTarget, mipmapLevel, true);
|
||||
}
|
||||
// Meaningful only while IsStorageDirty(uploadTarget, mipmapLevel).
|
||||
virtual MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const {
|
||||
const IntVec3 size = GetMipmapTexelSize(uploadTarget, mipmapLevel);
|
||||
return {IntVec3{0, 0, 0}, IntVec3{size.x(), size.y(), std::max(size.z(), 1)}};
|
||||
}
|
||||
|
||||
// The compressed image a glCompressedTexImage* call shadowed for this level, kept verbatim
|
||||
// next to the texel data rather than instead of it - see MipmapStorage. The texel shadow
|
||||
@@ -218,6 +232,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
||||
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override;
|
||||
bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
|
||||
IntVec3 size) override;
|
||||
MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat,
|
||||
const void* data, SizeT size) override;
|
||||
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
|
||||
@@ -55,6 +55,18 @@ namespace MobileGL {
|
||||
return m_textureStorage.IsDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObject2DCube::MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
IntVec3 offset, IntVec3 size) {
|
||||
++m_contentVersion;
|
||||
m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset,
|
||||
size);
|
||||
}
|
||||
|
||||
MipmapDirtyRegion TextureObject2DCube::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const {
|
||||
return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
void TextureObject2DCube::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat, const void* data, SizeT size) {
|
||||
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||
|
||||
@@ -27,6 +27,10 @@ namespace MobileGL {
|
||||
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
||||
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) override;
|
||||
bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
|
||||
IntVec3 size) override;
|
||||
MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const override;
|
||||
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat, const void* data, SizeT size) override;
|
||||
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
// High-water mark of texture units ever touched by a texture or sampler bind.
|
||||
// Units above it have provably-empty binding slots, so per-draw backend scans
|
||||
// can stop there instead of walking all MAX_TEXTURE_IMAGE_UNITS units.
|
||||
void NoteUnitTouched(Int unit) {
|
||||
void NoteUnitTouched(Int unit, Bool bindingChanged = true) {
|
||||
if (unit > m_maxTouchedUnit && unit < MAX_TEXTURE_IMAGE_UNITS) m_maxTouchedUnit = unit;
|
||||
// Every texture/sampler bind entry point (glBindTexture / glBindTextureUnit /
|
||||
// glBindTextures / glBindSampler) routes through here, so bumping the generation here
|
||||
@@ -75,7 +75,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
// which texture is bound at which unit. A backend that has cached the per-draw
|
||||
// sampled-texture set can compare this against a snapshot to skip re-resolving it when
|
||||
// no bind changed (the block atlas + lightmap stay bound across a whole terrain batch).
|
||||
++m_textureBindGeneration;
|
||||
// Re-binding the object a slot already holds changes nothing that the generation
|
||||
// guards; such callers pass bindingChanged=false so only the high-water mark advances
|
||||
// and the backend fast path survives the redundant re-binds apps issue every frame.
|
||||
if (bindingChanged) ++m_textureBindGeneration;
|
||||
}
|
||||
Int GetMaxTouchedUnit() const { return m_maxTouchedUnit; }
|
||||
Uint64 GetTextureBindGeneration() const { return m_textureBindGeneration; }
|
||||
|
||||
@@ -34,7 +34,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
|
||||
void VertexArrayState::Bind(Uint index) {
|
||||
m_boundVertexArray = GetVertexArrayObject(index);
|
||||
const auto& vertexArray = GetVertexArrayObject(index);
|
||||
// Re-binding the already-current VAO is a per-batch habit of Blaze3D-style renderers;
|
||||
// skip the shared_ptr store (two atomic refcount ops) when nothing changes.
|
||||
if (vertexArray == m_boundVertexArray) return;
|
||||
m_boundVertexArray = vertexArray;
|
||||
}
|
||||
|
||||
const SharedPtr<VertexArrayObject>& VertexArrayState::CreateVertexArrayObject(Uint index) {
|
||||
|
||||
Reference in New Issue
Block a user