mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 12:18:30 +09:00
135 lines
8.3 KiB
C++
135 lines
8.3 KiB
C++
// MobileGL - MobileGL/MG_State/GLState/TextureState/MipmapStorage.h
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
#pragma once
|
|
#include <algorithm>
|
|
|
|
#include "TextureEnum.h"
|
|
#include "MG_Util/Types.h"
|
|
#include "MG_Util/Math/VectorTypes.h"
|
|
#include "TextureTypes.h"
|
|
|
|
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);
|
|
}
|
|
SizeT TexelCount() const {
|
|
if (Empty()) return 0;
|
|
return static_cast<SizeT>(hi.x() - lo.x()) * static_cast<SizeT>(hi.y() - lo.y()) *
|
|
static_cast<SizeT>(hi.z() - lo.z());
|
|
}
|
|
};
|
|
|
|
class MipmapStorage {
|
|
public:
|
|
SizeT GetLevelCount() const;
|
|
void AllocateLevel(Uint level, MipmapInput input);
|
|
// Discard every level at or above levelCount. AllocateLevel never shrinks, so this
|
|
// is the only way a chain gets shorter - use it where the caller defines the whole
|
|
// level set (glTexStorage*, mip regeneration, atlas respecification).
|
|
void TruncateToLevelCount(SizeT levelCount);
|
|
void UpdateSubData(Uint level, DataPtr input);
|
|
void* MapData(Uint level);
|
|
IntVec3 GetTexelSize(Uint level) const;
|
|
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;
|
|
|
|
// Behind the union box, the level keeps up to kMaxDirtyRects pairwise
|
|
// disjoint rects recording WHERE the writes actually landed. A frame of
|
|
// ~100 scattered sprite updates in a big atlas has a union box that
|
|
// covers nearly the whole level while the touched texels are ~5% of it;
|
|
// the union box stays the source of truth (every write funnels through
|
|
// MarkDirty/MarkDirtyRegion into BOTH representations), backends OPT IN
|
|
// to the list purely as an upload-size refinement. 96 slots because the
|
|
// pattern this exists for is Minecraft's ~100 sprites/frame: a 16-slot
|
|
// list forced into far-apart merges was measured at >90% of the union
|
|
// box's area on exactly that pattern, i.e. worthless. Inserts merge any
|
|
// touching/overlapping rect (cascading, so the list stays disjoint);
|
|
// when full, the incoming rect folds into the neighbour whose box grows
|
|
// least and the list degrades gracefully toward the union box.
|
|
static constexpr SizeT kMaxDirtyRects = 96;
|
|
// Copies the level's dirty rects into outRects and returns how many were
|
|
// written. 0 means "upload the union box instead" and covers every
|
|
// reason at once: tracking unavailable, a single rect (identical to the
|
|
// union box by construction), more rects than maxRects, or a summed
|
|
// area so close to the union box's that one big upload beats many small
|
|
// ones (fewer driver calls wins when the bytes are nearly equal).
|
|
SizeT GetDirtyRects(Uint level, MipmapDirtyRegion* outRects, SizeT maxRects) 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
|
|
// glGetCompressedTexImage to return the image *as stored*, and no backend here has a
|
|
// BC/ETC codec, so a re-encode could never be byte-exact; at the same time m_data has
|
|
// to keep the "width * height * bytes-per-texel" layout that the backend upload
|
|
// sizing, glGenerateMipmap's bytes-per-texel division and the pixel-store packer all
|
|
// divide by. Two parallel vectors, one invariant preserved. Call order is
|
|
// AllocateLevel then SetCompressedImage - AllocateLevel clears the tag, so a plain
|
|
// glTexImage2D over the level un-compresses it.
|
|
void SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size);
|
|
// GL_NONE when the level is not stored compressed.
|
|
GLenum GetCompressedFormat(Uint level) const;
|
|
SizeT GetCompressedByteSize(Uint level) const;
|
|
const void* MapCompressedData(Uint level) const;
|
|
|
|
// The compressed internalformat the application ASKED for, which is not the same
|
|
// question as the one above: the six generic GL_COMPRESSED_* enums let the
|
|
// implementation choose, MobileGL chooses uncompressed storage, and the level is
|
|
// deliberately left untagged so GL_TEXTURE_COMPRESSED keeps answering false and
|
|
// glGetCompressedTexImage is not handed a blob nothing ever compressed. The entry
|
|
// points that must refuse a compressed image outright (glClearTexImage /
|
|
// glClearTexSubImage, GL 4.6 core 8.19) still need to know, so the request is
|
|
// recorded separately. Set right after AllocateLevel, which clears it.
|
|
void SetRequestedCompressedFormat(Uint level, GLenum internalFormat);
|
|
// GL_NONE when the level was not requested with a compressed internalformat.
|
|
GLenum GetRequestedCompressedFormat(Uint level) const;
|
|
|
|
protected:
|
|
// Insert one clamped, non-empty write box, keeping the list disjoint
|
|
// and bounded (see kMaxDirtyRects).
|
|
void InsertDirtyRect(Uint level, MipmapDirtyRegion incoming);
|
|
|
|
Vector<IntVec3> m_texelSizes;
|
|
Vector<Vector<Uint8>> m_data;
|
|
Vector<bool> m_isDirty;
|
|
Vector<MipmapDirtyRegion> m_dirtyRegions;
|
|
// Per level, the disjoint rect list behind m_dirtyRegions' union box.
|
|
// An EMPTY list is the common resting state and always means "the union
|
|
// box is the whole story" - clean levels, whole-level dirties and
|
|
// respecifies all just clear it, so plain full-level uploads never pay
|
|
// a heap allocation; the first scattered MarkDirtyRegion on an
|
|
// already-dirty level seeds the list from the union box accumulated so
|
|
// far and refines from there.
|
|
Vector<Vector<MipmapDirtyRegion>> m_dirtyRects;
|
|
Vector<Vector<Uint8>> m_compressedData;
|
|
Vector<GLenum> m_compressedFormats;
|
|
Vector<GLenum> m_requestedCompressedFormats;
|
|
};
|
|
} // namespace GLState
|
|
} // namespace MG_State
|
|
} // namespace MobileGL
|