Files
MobileGL/MobileGL/MG_Backend/DirectGLES/ManagersTextureSync.cpp
T

2320 lines
167 KiB
C++

// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersTextureSync.cpp
// 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
#include "ManagersInternal.h"
namespace MobileGL::MG_Backend::DirectGLES {
namespace TextureImpl {
// --- Unpack-ring staging (see BufferImpl::UnpackRingAvailable) ---------------
// One rectangular (or whole-level) region of a level shadow, repacked TIGHTLY
// into the persistently-mapped unpack PBO. The upload that follows passes the
// returned ring offset with UNPACK_ROW_LENGTH / UNPACK_IMAGE_HEIGHT left at the
// surrounding ScopedDefaultUnpackState's 0, so the driver reads exactly the
// bytes staged here - strictly fewer than the ROW_LENGTH-strided client-pointer
// upload it replaces, which made the driver walk the whole level's stride.
//
// `blocks` describes one or more source regions to stage back-to-back into a
// SINGLE ring allocation; the ring offset of block i lands in blocks[i].offset.
// Deliberately without default member initializers: call sites declare a
// kMaxDirtyRects-sized array of these per dirty level and fill only the entries
// they use, so the type stays trivially default-constructible and the
// declaration costs nothing on the levels that never reach the ring.
struct UnpackStagingBlock {
const Uint8* src; // top-left texel of the region in the level's upload bytes
SizeT rowBytes; // bytes per region row (region width * bpp)
SizeT rows; // region height
SizeT slices; // region depth (1 for 2D)
SizeT srcRowStride; // level row pitch
SizeT srcSliceStride; // level slice pitch
SizeT offset; // out: byte offset into the ring store
};
// False (nothing staged, ring untouched) whenever the caller must keep the
// client-pointer path: ring unavailable, a degenerate block, or a total that
// overflows / exceeds the ring's cap.
static Bool StageBlocksIntoUnpackRing(UnpackStagingBlock* blocks, SizeT blockCount) {
if (blocks == nullptr || blockCount == 0) return false;
if (!BufferImpl::UnpackRingAvailable()) return false;
const SizeT maxBytes = BufferImpl::UnpackRingMaxBytes();
SizeT total = 0;
for (SizeT i = 0; i < blockCount; ++i) {
const UnpackStagingBlock& b = blocks[i];
if (b.src == nullptr || b.rowBytes == 0 || b.rows == 0 || b.slices == 0) return false;
// Every factor is bounded by the level's own byte size, but the
// multiplications are on SizeT: check each against the cap instead of
// trusting a product that could have wrapped.
if (b.rowBytes > maxBytes || b.rows > maxBytes / b.rowBytes) return false;
const SizeT sliceBytes = b.rowBytes * b.rows;
if (b.slices > maxBytes / sliceBytes) return false;
const SizeT blockBytes = sliceBytes * b.slices;
if (blockBytes > maxBytes - total) return false;
total += blockBytes;
}
SizeT base = 0;
if (!BufferImpl::UnpackRingAllocate(total, base)) return false;
// AFTER the allocation: growing the ring replaces the store and its map.
auto* ringBytes = static_cast<Uint8*>(BufferImpl::UnpackRingMappedPtr());
if (ringBytes == nullptr) return false;
SizeT cursor = base;
for (SizeT i = 0; i < blockCount; ++i) {
UnpackStagingBlock& b = blocks[i];
b.offset = cursor;
Uint8* dst = ringBytes + cursor;
// Each block's size is a whole number of texels, so consecutive block
// offsets stay multiples of the pixel type's size just as `base` is.
if (b.rowBytes == b.srcRowStride && b.rowBytes * b.rows == b.srcSliceStride) {
Memcpy(dst, b.src, b.rowBytes * b.rows * b.slices); // region IS the level
} else {
for (SizeT z = 0; z < b.slices; ++z) {
const Uint8* srcSlice = b.src + z * b.srcSliceStride;
if (b.rowBytes == b.srcRowStride) {
Memcpy(dst, srcSlice, b.rowBytes * b.rows); // full-width rows
dst += b.rowBytes * b.rows;
continue;
}
for (SizeT y = 0; y < b.rows; ++y) {
Memcpy(dst, srcSlice + y * b.srcRowStride, b.rowBytes);
dst += b.rowBytes;
}
}
}
cursor += b.rowBytes * b.rows * b.slices;
}
return true;
}
// The `pixels` argument of a PBO-sourced glTexSubImage: a byte offset dressed
// up as a pointer.
static const void* UnpackRingPixelOffset(SizeT offset) {
return reinterpret_cast<const void*>(static_cast<std::uintptr_t>(offset));
}
static Uint GetNormFallbackComponentCount(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::R16:
case TextureInternalFormat::R16Snorm:
return 1;
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RG16:
case TextureInternalFormat::RG16Snorm:
return 2;
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGB16:
case TextureInternalFormat::RGB10: // stored as RGB16 (UNorm16 shadow)
case TextureInternalFormat::RGB12: // stored as RGB16 (UNorm16 shadow)
case TextureInternalFormat::RGB16Snorm:
return 3;
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::RGBA16:
case TextureInternalFormat::RGBA12: // stored as RGBA16 (UNorm16 shadow)
case TextureInternalFormat::RGBA16Snorm:
return 4;
default:
return 0;
}
}
static Bool IsSnormFallbackFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::R16Snorm:
case TextureInternalFormat::RG16Snorm:
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGBA16Snorm:
return true;
default:
return false;
}
}
static Bool IsNorm8FallbackFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGBA8Snorm:
return true;
default:
return false;
}
}
// The bit pattern of 1.0 in an upload component type: what a format without alpha reads
// back as, and therefore what the synthetic fourth channel of a widened render target has
// to hold. Integer components carry the integer one, not a saturated field - and since
// GL_BYTE/GL_SHORT/GL_UNSIGNED_BYTE/GL_UNSIGNED_SHORT serve both classes, `integerData`
// is what decides, not the type.
static Bool GetUploadComponentOneBits(GLenum uploadType, Bool integerData, Uint8* outOneBits,
SizeT* outComponentSize) {
switch (uploadType) {
case GL_BYTE: {
const Int8 one = integerData ? Int8(1) : Int8(0x7F);
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_UNSIGNED_BYTE: {
const Uint8 one = integerData ? Uint8(1) : Uint8(0xFF);
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_SHORT: {
const Int16 one = integerData ? Int16(1) : Int16(0x7FFF);
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_UNSIGNED_SHORT: {
const Uint16 one = integerData ? Uint16(1) : Uint16(0xFFFF);
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_HALF_FLOAT: {
const Uint16 one = 0x3C00; // half 1.0
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_FLOAT: {
const Float one = 1.0f;
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_INT: {
const Int32 one = 1;
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
case GL_UNSIGNED_INT: {
const Uint32 one = 1;
Memcpy(outOneBits, &one, sizeof(one));
*outComponentSize = sizeof(one);
return true;
}
default:
return false;
}
}
// A three-channel format widened to four to keep a colour attachment renderable (see
// NormalizePixelFormat) is described to the driver as a four-component transfer, so the
// three-component client data has to be repacked with an alpha of 1.0 - otherwise the
// driver walks three texels' worth of data per four-texel row and the image shears.
// `componentCount` is the SOURCE component count and `byteSize` the source's size, so this
// runs after any type conversion (which keeps the component count) has already happened.
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data,
SizeT byteSize, GLenum uploadType, Vector<Uint8>& widenedData,
Bool integerData, Uint32 alphaOneCodeOverride) {
Uint8 oneBits[8] = {};
SizeT componentSize = 0;
// One and two source components as well as three: the image-format widening carries
// GL_R8UI in a GL_RGBA8UI and GL_RG32F in a GL_RGBA32F (see
// TextureImpl::GetImageBindableStorageWidening), and their surplus channels take the
// same values the three-channel case gives its single added one - zeroes, and the
// format's implied 1 in alpha.
if (componentCount == 0 || componentCount > 3 || data == nullptr || byteSize == 0 ||
!GetUploadComponentOneBits(uploadType, integerData, oneBits, &componentSize)) {
return data;
}
// ...except where the carrier holds CODES of a normalized value (GL_R16 in a
// GL_RGBA16UI), where the transfer type says GL_UNSIGNED_SHORT and neither of that
// type's two "ones" is right: the integer 1 is a code for 1/65535 and the saturated
// 0xFFFF is only right for the UNSIGNED 16-bit formats, not the signed ones, whose
// saturated code is 0x7FFF. The caller passes the channel's own maximum instead.
// Written through a value of the component's own width rather than as the low
// `componentSize` bytes of the Uint32, so the encoding does not turn on the host's
// byte order.
if (alphaOneCodeOverride != 0u) {
if (componentSize == sizeof(Uint16)) {
const auto one = static_cast<Uint16>(alphaOneCodeOverride);
Memcpy(oneBits, &one, sizeof(one));
} else if (componentSize == sizeof(Uint32)) {
Memcpy(oneBits, &alphaOneCodeOverride, sizeof(alphaOneCodeOverride));
} else if (componentSize == sizeof(Uint8)) {
const auto one = static_cast<Uint8>(alphaOneCodeOverride);
Memcpy(oneBits, &one, sizeof(one));
}
}
const SizeT srcTexelBytes = componentSize * componentCount;
// Sized from the level, never from the source: the driver reads a full
// width*height*depth*4 components for the transfer it was handed, so a source that
// somehow holds fewer texels must still leave a full destination behind (its tail
// reads as transparent black with the format's implied opaque alpha) rather than a
// short buffer the driver would run off the end of.
const SizeT texelCount = static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (texelCount == 0) {
return data;
}
const SizeT copyTexelCount = std::min(texelCount, byteSize / srcTexelBytes);
widenedData.assign(texelCount * componentSize * 4, 0);
const auto* src = static_cast<const Uint8*>(data);
Uint8* dst = widenedData.data();
for (SizeT i = 0; i < texelCount; ++i, dst += componentSize * 4) {
if (i < copyTexelCount) {
Memcpy(dst, src, srcTexelBytes);
src += srcTexelBytes;
}
// ALWAYS at component 3, never at `componentCount`: GL's implied 1 is the ALPHA
// channel, and a one- or two-component source leaves the channels between it and
// alpha at the zero `assign` already wrote. For three components the two
// expressions coincide, which is what this used to be written as.
Memcpy(dst + componentSize * 3, oneBits, componentSize);
}
return widenedData.data();
}
static const void* PrepareNormFloatFallbackUpload(TextureInternalFormat format, const IntVec3& texelSize,
const void* data, SizeT byteSize, GLenum uploadType,
Vector<Float>& convertedData) {
const Uint componentCount = GetNormFallbackComponentCount(format);
if (componentCount == 0 || uploadType != GL_FLOAT || data == nullptr || byteSize == 0) {
return data;
}
const SizeT texelCount = static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 0));
const SizeT componentTotal = texelCount * static_cast<SizeT>(componentCount);
const SizeT sourceComponentSize = IsNorm8FallbackFormat(format) ? sizeof(Int8) : sizeof(Uint16);
const SizeT sourceComponentTotal = byteSize / sourceComponentSize;
if (componentTotal == 0 || sourceComponentTotal == 0) {
return nullptr;
}
convertedData.assign(componentTotal, 0.0f);
const SizeT copyComponentTotal = std::min(componentTotal, sourceComponentTotal);
if (IsNorm8FallbackFormat(format)) {
const Int8* src = static_cast<const Int8*>(data);
constexpr Float invMaxSnorm8 = 1.0f / 127.0f;
for (SizeT i = 0; i < copyComponentTotal; ++i) {
convertedData[i] = std::max(static_cast<Float>(src[i]) * invMaxSnorm8, -1.0f);
}
} else if (IsSnormFallbackFormat(format)) {
const Int16* src = static_cast<const Int16*>(data);
constexpr Float invMaxSnorm16 = 1.0f / 32767.0f;
for (SizeT i = 0; i < copyComponentTotal; ++i) {
convertedData[i] = std::max(static_cast<Float>(src[i]) * invMaxSnorm16, -1.0f);
}
} else {
const Uint16* src = static_cast<const Uint16*>(data);
constexpr Float invMaxUnorm16 = 1.0f / 65535.0f;
for (SizeT i = 0; i < copyComponentTotal; ++i) {
convertedData[i] = static_cast<Float>(src[i]) * invMaxUnorm16;
}
}
return convertedData.data();
}
// The two shadow -> upload conversions a fallback storage format can need, in order:
// the component type first (SNORM/UNORM shadows into the float the fallback stores), then
// the component count (three-channel client data into a four-channel widened render
// target). They compose: GL_RGB8_SNORM on a driver with no renderable three-channel
// format becomes GL_RGBA16F, so its Int8x3 shadow is converted to Float x3 and then
// repacked as Float x4 with alpha 1.0.
//
// Both scratch buffers belong to the caller so they outlive the returned pointer; the
// return value is `data` itself whenever neither conversion applies, which is what the
// sub-rect upload fast path tests for.
static const void* PrepareFallbackUpload(TextureInternalFormat format, TextureTarget target,
const IntVec3& texelSize, const void* data, SizeT byteSize,
GLenum uploadType, Vector<Float>& convertedData,
Vector<Uint8>& widenedData) {
const void* uploadData =
PrepareNormFloatFallbackUpload(format, texelSize, data, byteSize, uploadType, convertedData);
// The component-count switch first: it rules out every format that cannot be widened
// (which is nearly all of them, including GL_RGBA8) without touching the capability
// cache, so an ordinary atlas upload does not pay for a per-level cache lookup.
const Uint componentCount = GetWidenableClientComponentCount(format);
if (componentCount == 0 || !TextureImpl::BackendTextureFormatAddsAlpha(format, target)) {
return uploadData;
}
// The type conversion above rewrites the level into `convertedData` at four bytes per
// component while keeping the component count, so the widening's source size is that
// buffer's, not the shadow's.
const SizeT uploadByteSize = (!convertedData.empty() && uploadData == convertedData.data())
? convertedData.size() * sizeof(Float)
: byteSize;
return PrepareChannelWidenedUpload(componentCount, texelSize, uploadData, uploadByteSize, uploadType,
widenedData, IsIntegerWidenableFormat(format));
}
// One channel of a packed r11f_g11f_b10f word as a float. The two 11-bit channels are
// e5m6 and the 10-bit one e5m5 - IEEE-shaped but UNSIGNED, so there is no sign bit to
// read and the exponent bias is the 15 a 5-bit exponent always carries.
static Float DecodePackedUnsignedFloat(Uint32 bits, Uint mantissaBits) {
const Uint32 mantissaScale = 1u << mantissaBits;
const Uint32 mantissa = bits & (mantissaScale - 1u);
const Uint32 exponent = bits >> mantissaBits;
if (exponent == 0u) {
// Subnormal, and zero with it: no implied leading 1, and the exponent is the
// smallest NORMAL one rather than the encoded 0.
return std::ldexp(static_cast<Float>(mantissa) / static_cast<Float>(mantissaScale), -14);
}
if (exponent == 31u) {
return mantissa == 0u ? std::numeric_limits<Float>::infinity()
: std::numeric_limits<Float>::quiet_NaN();
}
return std::ldexp(1.0f + static_cast<Float>(mantissa) / static_cast<Float>(mantissaScale),
static_cast<Int>(exponent) - 15);
}
// The r11f_g11f_b10f shadow decoded into the GL_RGBA / GL_FLOAT level its GL_RGBA16F
// carrier is uploaded as. Alpha is the 1 GL defines for a format that has no alpha
// channel, which is the same constant the shader-side mask writes, so a texel this
// function produced and a texel an imageStore produced are indistinguishable.
//
// Sized from the LEVEL, not the source, for the reason PrepareChannelWidenedUpload is:
// the driver reads a full width*height*depth*4 floats for the transfer it was handed.
static const void* PreparePackedFloatWidenedUpload(const IntVec3& texelSize, const void* data, SizeT byteSize,
Vector<Uint8>& widenedData) {
constexpr SizeT kSourceTexelBytes = sizeof(Uint32);
if (data == nullptr || byteSize < kSourceTexelBytes) {
return data;
}
const SizeT texelCount = static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (texelCount == 0) {
return data;
}
const SizeT copyTexelCount = std::min(texelCount, byteSize / kSourceTexelBytes);
widenedData.assign(texelCount * 4u * sizeof(Float), 0);
const auto* src = static_cast<const Uint8*>(data);
auto* dst = reinterpret_cast<Float*>(widenedData.data());
for (SizeT i = 0; i < texelCount; ++i, dst += 4) {
Float rgb[3] = {0.0f, 0.0f, 0.0f};
if (i < copyTexelCount) {
Uint32 packed = 0;
// Through a memcpy rather than a Uint32 read of `src`: the shadow is a byte
// buffer with no alignment promise of its own.
Memcpy(&packed, src + i * kSourceTexelBytes, sizeof(packed));
rgb[0] = DecodePackedUnsignedFloat(packed & 0x7FFu, 6u);
rgb[1] = DecodePackedUnsignedFloat((packed >> 11u) & 0x7FFu, 6u);
rgb[2] = DecodePackedUnsignedFloat((packed >> 22u) & 0x3FFu, 5u);
}
dst[0] = rgb[0];
dst[1] = rgb[1];
dst[2] = rgb[2];
dst[3] = 1.0f;
}
return widenedData.data();
}
// The rgb10_a2 / rgb10_a2ui shadow split into the four GL_UNSIGNED_SHORT channel CODES its
// GL_RGBA16UI carrier is uploaded as. GL_UNSIGNED_INT_2_10_10_10_REV puts the FIRST
// component in the LOW bits (that is what REV means), so red is bits 0-9, green 10-19,
// blue 20-29 and alpha 30-31.
//
// The same split serves both formats: an rgb10_a2ui channel's code IS its value, and an
// rgb10_a2 channel's code is the numerator of value = code / (2^b - 1) that the shader-side
// unpack divides out. Neither is scaled here - the carrier holds the format's own bits.
//
// Sized from the LEVEL, not the source, for the reason PrepareChannelWidenedUpload is: the
// driver reads a full width*height*depth*4 shorts for the transfer it was handed.
const void* PreparePackedIntWidenedUpload(const IntVec3& texelSize, const void* data, SizeT byteSize,
Vector<Uint8>& widenedData) {
constexpr SizeT kSourceTexelBytes = sizeof(Uint32);
if (data == nullptr || byteSize < kSourceTexelBytes) {
return data;
}
const SizeT texelCount = static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (texelCount == 0) {
return data;
}
const SizeT copyTexelCount = std::min(texelCount, byteSize / kSourceTexelBytes);
widenedData.assign(texelCount * 4u * sizeof(Uint16), 0);
const auto* src = static_cast<const Uint8*>(data);
auto* dst = reinterpret_cast<Uint16*>(widenedData.data());
for (SizeT i = 0; i < texelCount; ++i, dst += 4) {
Uint32 packed = 0;
if (i < copyTexelCount) {
// Through a memcpy rather than a Uint32 read of `src`: the shadow is a byte
// buffer with no alignment promise of its own.
Memcpy(&packed, src + i * kSourceTexelBytes, sizeof(packed));
}
dst[0] = static_cast<Uint16>(packed & 0x3FFu);
dst[1] = static_cast<Uint16>((packed >> 10u) & 0x3FFu);
dst[2] = static_cast<Uint16>((packed >> 20u) & 0x3FFu);
dst[3] = static_cast<Uint16>((packed >> 30u) & 0x3u);
}
return widenedData.data();
}
// The transfer half of the image-format widening: an image-bindable texture whose ES
// storage was widened to a core carrier is described to the driver as a four-component
// transfer, so its narrower client data has to be repacked the same way the three-channel
// colour-renderable widening repacks its own.
//
// Three shapes, because the carriers come in three kinds. Most of them keep the frontend
// format's component TYPE and only add channels, so padding the shadow out to four
// components is the whole conversion. The two PACKED formats do not: their shadow is one
// 32-bit word per texel, so the word has to be split - into four floats for
// r11f_g11f_b10f's GL_RGBA16F, into four shorts for rgb10_a2ui's GL_RGBA16UI. Reading such
// a word as components of the carrier's type - what the repack below would do - takes
// twelve or sixteen bytes from a four-byte texel and shears the level, which is what the
// allFormats LOAD walkers see and the STORE ones do not (a store overwrites every texel
// the upload got wrong).
//
// Composes with PrepareFallbackUpload rather than replacing it, and the composition is a
// no-op by construction: none of the widened formats is one GetWidenableClientComponentCount
// reports a count for, and the SNORM shadow-to-float conversion only fires for a GL_FLOAT
// transfer type, which the widened triple never picks for the two SNORM8 formats. So the
// shadow reaches this untouched and one conversion is all that runs.
static const void* PrepareImageWidenedUpload(const TextureImpl::ImageBindableStorageWidening& widening,
const IntVec3& texelSize, const void* data, SizeT byteSize,
Vector<Uint8>& widenedData) {
if (!widening || widening.SourceChannels == 0 || widening.SourceChannels > 4) {
return data;
}
switch (widening.SourceEncoding) {
case TextureImpl::ImageWidenSourceEncoding::PackedFloat11f11f10f:
return PreparePackedFloatWidenedUpload(texelSize, data, byteSize, widenedData);
case TextureImpl::ImageWidenSourceEncoding::PackedInt2101010Rev:
return PreparePackedIntWidenedUpload(texelSize, data, byteSize, widenedData);
case TextureImpl::ImageWidenSourceEncoding::Components:
break;
}
if (widening.SourceChannels == 4) {
return data;
}
return PrepareChannelWidenedUpload(widening.SourceChannels, texelSize, data, byteSize, widening.Type,
widenedData, widening.IntegerData,
widening.CarriesNormalizedCodes() ? widening.ChannelMax[3] : 0u);
}
// Overwrites the (internal format, format, type) triple GenerateTextureFormatInfo chose
// with the widened carrier's. Deliberately unconditional on anything but the widening
// itself: whatever renderability fallback the triple carried, an image the driver refuses
// to bind is useless, so the image constraint wins.
static void ApplyImageBindableStorageWidening(const TextureImpl::ImageBindableStorageWidening& widening,
GLenum* inOutInternalFormat, GLenum* inOutFormat,
GLenum* inOutType) {
if (!widening) {
return;
}
if (inOutInternalFormat) *inOutInternalFormat = widening.InternalFormat;
if (inOutFormat) *inOutFormat = widening.Format;
if (inOutType) *inOutType = widening.Type;
}
// RGB565/RGB5_A1 shadow data is stored as 8-bit unorm; uploading it as GL_UNSIGNED_BYTE
// leaves the 8-bit -> 5/6-bit requantization to the driver, whose rounding direction is
// implementation-defined: Adreno rounds to nearest (lossless round trip) but Mali floors,
// drifting mid-range texels one 5-bit step down and failing the KHR-GL3x
// pixelstoragemodes.teximage3d rgb565/rgb5a1 1/32-eps checks. Repack the shadow rows into
// the packed 16-bit client type with round-to-nearest instead - that recovers the original
// 5/6-bit values exactly (the shadow expansion round(v * 255 / max) is injective), so the
// driver stores them verbatim with no requantization left to its discretion. 4-bit formats
// (RGBA4) are exempt: their 8-bit expansion (v * 17) is exact under either rounding.
// Always retargets *inOutType for these formats (even for null data) so every upload of a
// level uses the same client type.
static const void* PreparePackedNormUpload(TextureInternalFormat format, const IntVec3& texelSize,
const void* data, SizeT byteSize, GLenum* inOutType,
Vector<Uint8>& packedData) {
if (format != TextureInternalFormat::RGB5 && format != TextureInternalFormat::RGB5A1) {
return data;
}
// With the storage widened to 8-bit-per-channel (the packed16 field-order quirk)
// there is no driver requantization left for the repack to pre-empt - the shadow's
// UNorm8 bytes ARE the stored bytes - and the packed 16-bit client type this leg
// retargets to is not a legal upload for a GL_RGB8/GL_RGBA8 store at all.
if (TextureImpl::UsesWidenedPacked16NormStorage(format)) {
return data;
}
const Bool hasAlpha = format == TextureInternalFormat::RGB5A1;
const GLenum packedType = hasAlpha ? GL_UNSIGNED_SHORT_5_5_5_1 : GL_UNSIGNED_SHORT_5_6_5;
// Idempotent across a region's level loop: glType is shared, so later levels arrive with
// the already-retargeted packed type and must still be converted.
if (*inOutType != GL_UNSIGNED_BYTE && *inOutType != packedType) {
return data;
}
*inOutType = packedType;
if (data == nullptr || byteSize == 0) {
return data;
}
const SizeT srcPixelBytes = hasAlpha ? 4 : 3;
const SizeT texelCount = std::min(static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 1)),
byteSize / srcPixelBytes);
packedData.resize(texelCount * sizeof(Uint16));
const Uint8* src = static_cast<const Uint8*>(data);
auto* dst = reinterpret_cast<Uint16*>(packedData.data());
for (SizeT i = 0; i < texelCount; ++i, src += srcPixelBytes) {
const Uint32 r = (static_cast<Uint32>(src[0]) * 31u + 127u) / 255u;
const Uint32 b = (static_cast<Uint32>(src[2]) * 31u + 127u) / 255u;
if (hasAlpha) {
const Uint32 g = (static_cast<Uint32>(src[1]) * 31u + 127u) / 255u;
dst[i] = static_cast<Uint16>((r << 11) | (g << 6) | (b << 1) | (src[3] >= 128 ? 1u : 0u));
} else {
const Uint32 g = (static_cast<Uint32>(src[1]) * 63u + 127u) / 255u;
dst[i] = static_cast<Uint16>((r << 11) | (g << 5) | b);
}
}
return packedData.data();
}
// "Some level of this texture holds an image", which is all the sync gate below actually
// needs to know. Deliberately weaker than ITextureObject::IsComplete(): that predicate also
// answers whether the texture SAMPLES as complete, so it must keep rejecting a chain with
// undefined lower levels - but such a texture still has to be uploaded, or the level that
// IS defined never reaches the driver at all.
static Bool HasAnyDefinedMipmapLevel(const MG_State::GLState::ITextureObject* stateTextureObject) {
const auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject);
if (mipmapObject == nullptr) return false;
const auto levelCount = mipmapObject->GetMipmapLevelCount();
for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) {
for (Uint level = 0; level < levelCount; ++level) {
const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level);
if (levelTexelSize.x() > 0 && levelTexelSize.y() > 0 && levelTexelSize.z() > 0) {
return true;
}
}
}
return false;
}
// True while this twin has NO noted handle, i.e. it is the monolith-glue half and the frontend
// object below is the only thing that can answer. A macro and not a member function because the
// pull build has no such member at all and this has to fold to a constant there (D-P: the pull
// build's preprocessed text may not gain a call).
#define MGB_TEXTURE_HANDLE_ARM_OFF(twin) (MG_Pipe::MGPipeHandleIsNull((twin).PushedSyncHandle()))
// The ~20 GetExternalIndex() reads this family makes in LOG LINES (scout G-S2-4). The pull
// expansion is the pre-P5e expression with one pair of parentheses around the receiver.
#define MGB_TEXTURE_DIAG_NAME(rec, obj) (TextureDiagName((obj), (rec)))
// The storage KIND, from the descriptor whenever one was resolved - the backstop arm of the
// storage switch used to ask the frontend object for a value it had just read from the record.
#define MGB_TEXTURE_STORAGE_KIND(rec, obj) \
((rec) != nullptr ? static_cast<TextureStorageType>((rec)->Desc.StorageKind) : (obj)->GetStorageType())
// P4a (D-D5), the dirty-ownership inversion, applied at the two places the upload loops
// below actually ask: "does this (uploadTarget, level) owe an upload" and "it has been
// uploaded". ARCHITECTURE.md §6 puts the rect model and the emission cursor on the
// CLIENT, which clears its own flags AT EMISSION - so on the handle arm the frontend's
// IsStorageDirty is already false for every level the applier accepted, and asking it
// would upload nothing at all. The applier's pending set is what these two read instead;
// it is server-side state, it survives this function's bail arms (an incomplete texture
// returns early, a multisample target refreshes and skips), and an entry is consumed
// ONLY where the level actually uploaded.
//
// THE UPLOAD-TARGET ENCODING is static_cast<Uint16>(TextureUploadTarget), which is what
// package B must emit into MGPSubData::Target's cube-face half; it is recorded as a
// deviation because the contract left it unstated.
//
// MACROS AND NOT LAMBDAS, and that is a G1 requirement rather than a style choice:
// SyncMipmapsToBackend already holds nine ErrorLopper lambdas whose mangled names are
// ...::$_N BY POSITION, so inserting two more renumbered every one of them - measured as
// eight symbols added, eight removed and four std::function thunks resized in the PULL
// build, whose symbol set P4a may not move by one byte (D-P). A macro expands to the
// pre-P4a expression exactly when MOBILEGL_PIPE_PUSH is off, so the pull build's
// preprocessed text, and therefore its object code, is unchanged. Both are #undef'd
// immediately after the function.
// P5c (tx): THE READS THE FOUR UPLOAD ARMS MAKE, re-sourced. With an active transport the
// apply thread may not name the client's TextureObjectMipmap at all (rule E), so on the
// handle arm:
//
// * the level TEXELS come from the server's staged-texture store, adopted at apply time
// (MGB_LEVEL_TEXELS - Fatal{StageSnapshotTooNarrow} when no record covered the level,
// which is the data-correctness refusal of the texture half);
// * the per-level EXTENT and DEFINED-NESS come from the same store (fed by the sub-data
// adoption and the respecify hook; {0,0,0} for a level nothing defined, which is exactly
// GetMipmapTexelSize's answer for one);
// * the level BYTE SIZE is the adopted run's length;
// * the texture TARGET and the UPLOAD-TARGET list come from the descriptor.
//
// Every macro keeps the P4a discipline: the non-disaggregated expansion is the original
// frontend read, character for character modulo one pair of parentheses, so the pull and
// push builds compile exactly what they compiled before tx, and every disaggregated arm
// falls back to the frontend read when there is no active transport (the legacy arm, and
// monolith). MGB_STAGED_TEXTURE_LIVE is the runtime discriminator; pushedStorage/pushedRes
// are the function's own locals. All are #undef'd with the rest after the function.
#define MGB_STAGED_TEXTURE_LIVE (pushedStorage != nullptr && MG_Record::ServerStagedTexture().Holds())
#define MGB_TEXTURE_TARGET(obj) \
(MGB_STAGED_TEXTURE_LIVE ? BufferImpl::StagedTextureTargetForPipeTarget(pushedStorage->Desc.Target) \
: (obj)->GetTarget())
#define MGB_UPLOAD_TARGETS(obj) \
(MGB_STAGED_TEXTURE_LIVE ? BufferImpl::StagedUploadTargetsForPipeTarget(pushedStorage->Desc.Target) \
: (obj)->GetUploadTargets())
#define MGB_LEVEL_TEXEL_SIZE(obj, tgt, lvl) \
(MGB_STAGED_TEXTURE_LIVE ? MG_Record::ServerStagedTexture().LevelExtentOrUndefined( \
MG_Record::StagedTextureStore::KeyForHandle(pushedRes), static_cast<Uint16>(tgt), \
static_cast<Uint16>(lvl)) \
: (obj)->GetMipmapTexelSize(tgt, lvl))
#define MGB_LEVEL_BYTE_SIZE(obj, tgt, lvl) \
(MGB_STAGED_TEXTURE_LIVE \
? MG_Record::ServerStagedTexture().LevelByteSize(MG_Record::StagedTextureStore::KeyForHandle(pushedRes), \
static_cast<Uint16>(tgt), static_cast<Uint16>(lvl)) \
: (obj)->GetMipmapByteSize(tgt, lvl))
#define MGB_LEVEL_TEXELS(obj, tgt, lvl, site) \
(MGB_STAGED_TEXTURE_LIVE ? MG_Record::ServerStagedTexture().RequireLevelBytes( \
MG_Record::StagedTextureStore::KeyForHandle(pushedRes), static_cast<Uint16>(tgt), \
static_cast<Uint16>(lvl), site) \
: (obj)->MapMipmapData(tgt, lvl))
// P5c (tx): the disaggregated pair adds ONE term and ONE clear to the P4a shapes - the
// staged-texture store's GPU-dirty mark (T5: a level the GPU generated dirties the SERVER's
// shadow, and the pending set cannot see it). The mark is never set on Espryt today - its
// GenerateMipmap fills the levels on the driver - so the term is inert here and is the
// contract-shaped answer (§2.2's last row) rather than a hot-path cost: one m_any acquire
// load when the store is empty.
#define MGB_LEVEL_NEEDS_UPLOAD(obj, tgt, lvl) \
(pushedStorage != nullptr \
? (FindPipeTextureUpload(*pushedStorage, static_cast<Uint16>(tgt), static_cast<Uint16>(lvl)) != nullptr || \
MG_Record::ServerStagedTexture().IsLevelGpuDirty(MG_Record::StagedTextureStore::KeyForHandle(pushedRes), \
static_cast<Uint16>(tgt), static_cast<Uint16>(lvl))) \
: (obj)->IsStorageDirty(tgt, lvl))
// Re-resolves the record itself, so it is safe after any amount of driver work - and it
// invalidates any PendingUpload* taken earlier for THIS texture, which is why every such pointer
// is used and dropped inside one level's iteration.
#define MGB_LEVEL_UPLOAD_DONE(obj, tgt, lvl) \
do { \
if (pushedStorage != nullptr) { \
ConsumePipeTextureUpload(pushedRes, static_cast<Uint16>(tgt), static_cast<Uint16>(lvl)); \
if (MGB_STAGED_TEXTURE_LIVE) { \
MG_Record::ServerStagedTexture().MarkLevelGpuDirty( \
MG_Record::StagedTextureStore::KeyForHandle(pushedRes), static_cast<Uint16>(tgt), \
static_cast<Uint16>(lvl), false); \
} \
} else { \
(obj)->MarkStorageDirty(tgt, lvl, false); \
} \
} while (0)
// THE STORAGE SHAPE, and on the handle arm it is the DESCRIPTOR's (C.3's d2: "the
// m_prevTextureInfo probe re-keyed onto the resource record's Serial AND Desc"; review
// M-2). v1 moved the parameters and the uploads and left the allocation reading the
// frontend object, which put TWO AUTHORITIES on one value inside one twin -
// SyncTextureParamsToBackend already answers MGB_TEXPARAM_FORMAT from
// Desc.InternalFormat while this function allocated from GetFormat(). In monolith they
// agree, so no lane can expose the split; under split the descriptor is all there is.
//
// WHAT THE DESCRIPTOR ANSWERS: internal format, base extent, level count, sample count,
// fixed sample locations, immutability, storage kind and the buffer-texture window. What
// it does NOT answer, and what still comes from the frontend shadow, is the PER-LEVEL
// extent and the level BYTES - MGPResourceDesc carries one base extent, the per-level
// sizes are derived from it, and the texels themselves ride on sub-data. A level the
// descriptor's Levels claims and the shadow has not defined reads back {0,0,0} and the
// per-level loops skip it, which is the same arm they take for a sparse chain today.
//
// AND Desc.HasDefinedContent IS DELIBERATELY NOT READ HERE (review N-8, and the
// enumeration above exists to be exhaustive, so the omission is stated rather than
// left to be noticed). c0e made it storage-defining and the BUFFER family reads it
// three times, because a buffer's content is ONE fact about the whole resource. A
// texture's is not: content is per (uploadTarget, level) and its authority is the
// pending set (D-D5), which is per level and survives a bail. A whole-resource
// "content is undefined" would say nothing the per-level loops do not already answer
// by finding no level to upload, and acting on it would mean throwing away levels the
// set still owes. The field stays a respecify CLASSIFIER (it moves the descriptor, so
// wire refuses to call such a respecify metadata-only) and nothing more on this arm.
//
// MACROS, NOT LOCALS, FOR THE SAME REASON AS THE PAIR ABOVE (DV-9, and it was measured):
// routing these through locals resized SyncTextureParamsToBackend by +9 bytes in the
// PULL build and P4a's admitted-resize set is EMPTY. Each takes the object it would
// otherwise have been spelled on, so the pull expansion is the pre-P4a expression with
// one pair of parentheses around the receiver. All are #undef'd with the pair above.
#define MGB_STORAGE_FORMAT(obj) \
(pushedStorage != nullptr ? static_cast<TextureInternalFormat>(pushedStorage->Desc.InternalFormat) \
: (obj)->GetFormat())
#define MGB_STORAGE_BASE_SIZE(obj) \
(pushedStorage != nullptr \
? IntVec3{static_cast<Int>(pushedStorage->Desc.Width), static_cast<Int>(pushedStorage->Desc.Height), \
static_cast<Int>(pushedStorage->Desc.Depth)} \
: (obj)->GetBaseSize())
#define MGB_STORAGE_LEVELS(obj) \
(pushedStorage != nullptr ? static_cast<Uint>(pushedStorage->Desc.Levels) : (obj)->GetMipmapLevelCount())
#define MGB_STORAGE_SAMPLES(obj) \
(pushedStorage != nullptr ? static_cast<Int>(pushedStorage->Desc.Samples) : (obj)->GetSamples())
#define MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(obj) \
(pushedStorage != nullptr ? pushedStorage->Desc.FixedSampleLocations != 0 : (obj)->HasFixedSampleLocations())
#define MGB_STORAGE_IMMUTABLE(obj) \
(pushedStorage != nullptr ? pushedStorage->Desc.Immutable != 0 : (obj)->IsImmutable())
#define MGB_STORAGE_KIND(obj) \
(pushedStorage != nullptr ? static_cast<TextureStorageType>(pushedStorage->Desc.StorageKind) \
: (obj)->GetStorageType())
void BackendTextureObject::SyncMipmapsToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
// P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin;
// see the twin note in SyncBuiltinSamplerToBackend. P5e (fix1): "a handle was noted"
// is not on its own the statement that the record arm will be SELECTED - the view
// test below and RequireImageBindableStorage's transition are transport-gated - so
// the check is the one shared refusal and a noted handle on an arm that cannot serve
// it aborts by name rather than falling into the frontend reads.
if (!stateTextureObject && MGB_TEXTURE_NULL_FRONTEND_REFUSED(*this, "SyncMipmapsToBackend")) {
MGLOG_E_ONCE("State texture object is null, cannot sync to backend.");
return;
}
// P4a (D-B3/D-D5): on the handle arm the record IS the state this function reads.
// P5e (tx2) hoists the resolution to the TOP of the function, because the view test
// below needs it before anything else does; nothing else about it moves.
//
// The record pointer stays valid for the whole function: only a create/respecify for
// a NEW slot grows MGPipeApplierState::TextureResources, and nothing this function
// calls emits one. Pointers INTO record->PendingUploads do not - the consume is a
// swap-and-pop - so every one of them is taken, used and dropped inside one level's
// iteration, and never held across a consume.
const MG_Pipe::MGPipeResourceRecord* pushedStorage = nullptr;
MG_Pipe::MGPipeHandle pushedRes = MG_Pipe::kMGPipeNullHandle;
pushedStorage = ResolveOwnRecord(stateTextureObject);
pushedRes = !MG_Pipe::MGPipeHandleIsNull(m_pushedSyncHandle)
? m_pushedSyncHandle
: (pushedStorage != nullptr ? pushedStorage->Desc.Resource : MG_Pipe::kMGPipeNullHandle);
if (pushedStorage == nullptr) {
MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its "
"storage and uploads cannot be driven from the pushed descriptor "
"(handle {%u, %u})",
TextureDiagName(stateTextureObject, pushedStorage), pushedRes.Slot, pushedRes.Gen);
return;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// SHARED IMAGES: ahead of the view test and of the staged-store and serial gates below,
// none of which knows the texels belong to an image - the staged store holds none of
// them, and the storage path would allocate fresh, empty storage over the image.
if (pushedStorage != nullptr && (pushedStorage->SharedImageId != 0 || m_sharedImageId != 0) &&
SyncSharedImageStorage(*pushedStorage)) {
// EVERY use, not only the attach (the draw-clean gates never pass a bound image):
// a write that landed since the last one is waited for on this context's GPU
// before the command this sync is for, and the session's frame notes the read.
if (m_sharedImage != nullptr) SharedImageImpl::AcquireForSampling(m_sharedImage);
ReplaySharedImageUploads(*pushedStorage);
// A YUV image is converted into the name at the frame's first use.
if (m_sharedImage != nullptr && SharedImageImpl::IsYuv(m_sharedImage))
SharedImageImpl::ConvertYuvForSampling(m_sharedImage, GetBackendTextureId(),
pushedStorage->Desc.Width, pushedStorage->Desc.Height);
return;
}
#endif
// P5e (tx2), CONTRACT-P5E §5.2 (scout G-S2-2): THE VIEW TEST MOVES BEHIND THE
// RECORD. `IsTextureView()` was the one frontend read this function made BEFORE it
// had resolved anything at all, and Desc.ViewOf is the carrier - it names the storage
// owner BY HANDLE, so both the question and its answer are server-owned.
//
// Ruling 1's arm: the push-monolith build keeps the frontend test below token for
// token, because there the two answers are the same answer and the verify comparator
// needs the frontend arm.
if (pushedStorage != nullptr && !MG_Pipe::MGPipeHandleIsNull(pushedStorage->Desc.ViewOf)) {
SyncTextureViewToBackendByRecord(pushedRes, *pushedStorage, stateTextureObject);
return;
}
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// P4a (D-B3/D-D5): on the handle arm the record IS the state this function reads.
// Resolved once, here, because three things below want it - the cheap gate, the
// per-level dirty questions and the union box / rect list / strides the staging
// planner takes its shape from.
//
// The record pointer stays valid for the whole function: only a create/respecify
// for a NEW slot grows MGPipeApplierState::TextureResources, and nothing this
// function calls emits one. Pointers INTO record->PendingUploads do not - the
// consume is a swap-and-pop - so every one of them is taken, used and dropped
// inside one level's iteration, and never held across a consume.
if (pushedStorage != nullptr) {
// THE METADATA RESPECIFY, HONOURED (ID-18 M4, review M-2). BindMask and
// ImageBindableHint are STICKY facts the client discovers AFTER allocation - a
// texture first bound as a shader image - and an IMMUTABLE texture never has a
// later redefinition to carry them, so they arrive on a respecify that restates
// the storage the resource already has. The applier classifies such a record as a
// metadata update: no reallocation ack, no pending-upload clear, Serial moved.
// THE TWIN'S HALF IS TO RE-DERIVE ITS STORAGE FLAGS FROM THE NEW MASK AND
// RECREATE ONLY WHERE THE BACKEND NEEDS THE FLAG AT CREATION - and image
// bindability is exactly such a flag on this backend: the carrier may be channel-
// widened, which is a different allocation, not a different binding.
//
// RequireImageBindableStorage IS that path and it is idempotent, so this is the
// whole of the re-derivation: it sets the sticky flag, clears m_isInitialized so
// the storage below is regenerated, re-arms every defined level in BOTH dirty
// models (see its own comment) so the widened carrier is replayed rather than
// allocated empty, and forces the parameter resync the widening's swizzle needs.
// Arriving on the CREATE instead - the common case once B publishes the hint -
// costs nothing: m_isInitialized is already false and there are no levels to
// replay.
const Bool pushedWantsImageBindableStorage =
pushedStorage->Desc.ImageBindableHint != 0 ||
(pushedStorage->Desc.BindMask & MG_Pipe::kMGPipeBindShaderImage) != 0;
if (pushedWantsImageBindableStorage && !m_imageBindableStorageRequired) {
// P5e (tx2), ruling 1's arm: under a transport the re-dirty half is a NAMED
// refusal at its entry (§5.2) instead of a walk of the client's level shadows;
// the push-monolith build keeps the frontend transition exactly as it is.
RequireImageBindableStorageByHandle(pushedRes, *pushedStorage);
}
// ONE compare replaces the whole cheap-gate trio AND the content version. The
// applier bumps Serial on every respecify and every sub-data it applies to this
// resource, which is exactly the union those four keys covered - without the
// coarse behaviour the sampling-resolution generation had, where any texture's
// shape churn re-opened every other texture's gate.
//
// The pending set is the second half and it is not redundant: a previous sync
// may have BAILED (an incomplete texture returns early, a multisample target
// refreshes and skips) with the serial already stamped, and the set is what
// survives that. Empty means every level this record ever declared has been
// consumed by a sync that actually uploaded it.
if (m_isInitialized && m_syncedResourceSerial != 0 && m_syncedResourceSerial == pushedStorage->Serial &&
pushedStorage->PendingUploads.empty()) {
return;
}
}
// First-level clean gate (see the member comment): three version compares and no
// virtual shape walk. Every mutation the slower probe below would catch bumps one of
// the keys - shape via the context's sampling-resolution generation (coarse: any
// texture's shape churn re-opens every gate, which only costs a fall-through to the
// probe), CPU pixels via the content version, samples/fixed-locations via the params
// version - and backend-side storage resets clear m_isInitialized. Restricted to
// Mipmap storage like the probe fast path: a buffer texture's backing store can move
// without any of these keys noticing.
//
// LEGACY ARM ONLY from P4a on: the memos it reads are the pre-handle ones and the
// record above answers the same question in one compare.
if (pushedStorage == nullptr)
if (m_isInitialized && m_syncedShapeContextId != 0 && MG_Pipe::gPipeInputs->IsLive() &&
m_syncedShapeContextId == MG_Pipe::gPipeInputs->GetTextureContextId() &&
m_syncedShapeGeneration == MG_Pipe::gPipeInputs->GetSamplingResolutionGeneration() &&
m_syncedContentVersion == stateTextureObject->GetContentVersion() &&
m_syncedShapeParamsVersion == stateTextureObject->GetTextureParamsVersion() &&
stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) {
return;
}
MGLOG_D("Syncing texture mipmaps with backend ID %u to backend for state ID %u", m_backendTextureId,
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
GLenum target = ConvertTextureTargetToBackendGLEnum(MGB_TEXTURE_TARGET(stateTextureObject));
auto targetInternal = MGB_TEXTURE_TARGET(stateTextureObject);
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
if (!IsSupportedTextureTarget(targetInternal)) {
MGLOG_E_ONCE(" Texture target %s is not supported, skipping.",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
return;
}
// The texture needs to be regenerated completely with glTexImage* calls if:
// 1. Not initialized
// 2. InternalFormat changed
// 3. Size changed
// 4. Mipmap levels changed
// IsComplete() is the sampling predicate, and it calls a chain whose lower levels are
// undefined incomplete - which is what a top-down build (upload level N, then level 0)
// and ARB_clear_texture's conformance cases both produce. Bailing out on that shape
// left the backend name with no levels whatsoever, so the level that WAS defined could
// never be sampled or read back. Sync whenever some level holds an image; the per-level
// loops below skip the degenerate ones individually.
// P5c (gt, CONTRACT-P5C §6 layer 1): on the staged arm the same question is answered
// out of the SERVER's staged-texture store, never out of the frontend object - the
// store's Defined-ness (fed by the respecify hook and the sub-data adoption) IS "some
// level holds an image", and IsComplete()'s sampling half is redundant with it: a
// complete mipmap chain has a defined level 0, and the gate's whole job is to let an
// incomplete-but-partly-defined chain through. The one divergence is deliberate and
// stated: a chain whose EVERY level is 0x0 is complete-by-quirk on the frontend (the
// "0x0 in last level" relaxation, TextureObject.cpp) and undefined here - and syncing
// it would upload nothing either way, because every per-level loop below skips a
// {0,0,0} level individually. A BUFFER texture never has staged levels, so its gate
// is the descriptor's format - exactly what TextureObjectBase::IsComplete() reduces
// to for that storage kind.
if (MGB_STAGED_TEXTURE_LIVE) {
const auto& stagedDesc = pushedStorage->Desc;
Bool anyDefined = false;
if (static_cast<TextureStorageType>(stagedDesc.StorageKind) == TextureStorageType::Buffer) {
// A buffer texture has no staged levels; its gate is the descriptor's
// format, which is what TextureObjectBase::IsComplete() reduces to here.
anyDefined =
static_cast<TextureInternalFormat>(stagedDesc.InternalFormat) != TextureInternalFormat::Unknown;
} else {
auto& stagedStore = MG_Record::ServerStagedTexture();
const Uint64 stagedKey = MG_Record::StagedTextureStore::KeyForHandle(pushedRes);
for (const auto& uploadTarget : BufferImpl::StagedUploadTargetsForPipeTarget(stagedDesc.Target)) {
for (Uint32 level = 0; level < stagedDesc.Levels; ++level) {
if (stagedStore.IsLevelDefined(stagedKey, static_cast<Uint16>(uploadTarget),
static_cast<Uint16>(level))) {
anyDefined = true;
break;
}
}
if (anyDefined) break;
}
}
if (!anyDefined) {
MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
return;
}
} else if (!stateTextureObject->IsComplete() && !HasAnyDefinedMipmapLevel(stateTextureObject.get())) {
MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.",
stateTextureObject->GetExternalIndex());
return;
}
// Fast path: a fully-synced mipmap texture is the common per-draw case.
// SyncNeccessaryTextures re-syncs every bound texture each draw, and the
// scratch Bind below targets the temp unit - which sequential distinct
// textures thrash, forcing a real glBindTexture per texture per draw. When
// nothing needs uploading, skip the bind + upload machinery entirely;
// BindCurrentTextures() re-establishes the real sampling bindings regardless.
// The content-version stamp short-circuits before any shape probing: it
// bumps on every CPU-side pixel mutation, so an unchanged stamp plus an
// unchanged shape means no level can be dirty. Shape stays a separate
// compare because a NULL-data glTexImage changes it without touching the
// content version.
//
// LEGACY ARM ONLY from P4a on: on the handle arm the record's Serial plus an empty
// pending set is the same statement in one compare, made above and BEFORE the
// IsComplete()/shape walk rather than after it - so this probe would only re-derive
// an answer already given, out of the very frontend state the arm exists to stop
// reading.
if (pushedStorage == nullptr)
if (m_isInitialized && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap &&
m_syncedContentVersion != 0 && m_syncedContentVersion == stateTextureObject->GetContentVersion()) {
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(stateTextureObject.get());
const auto probeBaseSize = stateTextureObject->GetBaseSize();
StateTextureBasicInfo probe = {stateTextureObject->GetFormat(),
static_cast<SizeT>(probeBaseSize.x()),
static_cast<SizeT>(probeBaseSize.y()),
static_cast<SizeT>(probeBaseSize.z()),
static_cast<SizeT>(mipmapObject->GetMipmapLevelCount()),
0,
stateTextureObject->GetSamples(),
stateTextureObject->HasFixedSampleLocations()};
if (probe == m_prevTextureInfo) {
MGLOG_D("Texture ID %u already fully synced, skipping scratch bind + upload.",
m_backendTextureId);
// The probe just proved "fully synced" from the real state, so the cheap
// gate may be (re)stamped here: the coarse generation only ever goes stale
// from OTHER textures' churn, and this draw re-validated this one.
if (MG_Pipe::gPipeInputs->IsLive()) {
m_syncedShapeContextId = MG_Pipe::gPipeInputs->GetTextureContextId();
m_syncedShapeGeneration = MG_Pipe::gPipeInputs->GetSamplingResolutionGeneration();
m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion();
}
return;
}
}
Bind(target);
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
const auto baseSize = MGB_STORAGE_BASE_SIZE(stateTextureObject);
StateTextureBasicInfo currentTextureInfo = {MGB_STORAGE_FORMAT(stateTextureObject),
static_cast<SizeT>(baseSize.x()),
static_cast<SizeT>(baseSize.y()),
static_cast<SizeT>(baseSize.z()),
0,
0,
MGB_STORAGE_SAMPLES(stateTextureObject),
MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(stateTextureObject)};
switch (MGB_STORAGE_KIND(stateTextureObject)) {
case TextureStorageType::Mipmap: {
auto* textureMipmapObject =
static_cast<MG_State::GLState::TextureObjectMipmap*>(stateTextureObject.get());
const auto mipmapCount = MGB_STORAGE_LEVELS(textureMipmapObject);
currentTextureInfo.mipmapLevels = mipmapCount;
Bool needsRegeneration = !m_isInitialized || (currentTextureInfo != m_prevTextureInfo);
if (needsRegeneration && m_backendStorageImmutable) {
RecreateBackendTexture();
Bind(target);
}
// Only a texture that is actually image-bound pays for the widening: it doubles
// or quadruples the storage, and RequireImageBindableStorage is sticky, so a
// texture that is merely sampled keeps its narrow format for life. See
// TextureImpl::GetImageBindableStorageWidening for what widens and why.
const TextureImpl::ImageBindableStorageWidening imageWidening =
m_imageBindableStorageRequired
? TextureImpl::GetImageBindableStorageWidening(MGB_STORAGE_FORMAT(textureMipmapObject))
: TextureImpl::ImageBindableStorageWidening{};
const Bool canAppendMipmaps =
m_isInitialized && !m_imageBindableStorageRequired && !MGB_STORAGE_IMMUTABLE(stateTextureObject) &&
currentTextureInfo.internalFormat == m_prevTextureInfo.internalFormat &&
currentTextureInfo.width == m_prevTextureInfo.width &&
currentTextureInfo.height == m_prevTextureInfo.height &&
currentTextureInfo.depth == m_prevTextureInfo.depth &&
currentTextureInfo.bufferExternalIndex == m_prevTextureInfo.bufferExternalIndex &&
currentTextureInfo.samples == m_prevTextureInfo.samples &&
currentTextureInfo.fixedSampleLocations == m_prevTextureInfo.fixedSampleLocations &&
currentTextureInfo.mipmapLevels > m_prevTextureInfo.mipmapLevels &&
!TextureImpl::IsMultisampleTextureTarget(targetInternal);
MGLOG_D("%s: Got texture info: %dx%dx%d, mips %d, format %s", __func__, baseSize.x(), baseSize.y(),
baseSize.z(), mipmapCount,
MG_Util::ConvertTextureInternalFormatToString(MGB_STORAGE_FORMAT(textureMipmapObject)).c_str());
if (canAppendMipmaps) {
MGLOG_D("Texture mip count increased for backend ID %u, appending levels %zu..%zu",
m_backendTextureId, m_prevTextureInfo.mipmapLevels, mipmapCount - 1);
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureMipmapObject), &glInternalFormat,
&glFormat, &glType, targetInternal);
const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject);
ScopedDefaultUnpackState unpackState;
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = m_prevTextureInfo.mipmapLevels; level < mipmapCount; ++level) {
auto levelTexelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level);
// A level the application never defined reads back as {0, 0, 0}; now that a
// sparse chain is synced rather than skipped whole, leave those undefined on
// the driver instead of giving the name a 0x0 image at that index.
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 || levelTexelSize.z() <= 0) {
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
continue;
}
auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level);
bool levelDirty = MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData =
(levelDirty && levelByteSize != 0)
? MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level, "append-mips")
: nullptr;
Vector<Float> convertedUploadData;
Vector<Uint8> widenedUploadData;
const void* uploadData = PrepareFallbackUpload(
MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, levelTexelSize, pData,
levelByteSize, glType, convertedUploadData, widenedUploadData);
Vector<Uint8> packedUploadData;
uploadData =
PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), levelTexelSize,
uploadData, levelByteSize, &glType, packedUploadData);
DebugImpl::ErrorLopper::Clear();
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
const IntVec3 uploadSize =
GetBackendUploadSize(MGB_TEXTURE_TARGET(stateTextureObject), levelTexelSize);
switch (MapToBackendTextureTarget(MGB_TEXTURE_TARGET(stateTextureObject))) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexImage2D(glUploadTarget, static_cast<GLint>(level),
(GLint)glInternalFormat, static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()), 0, glFormat, glType,
uploadData);
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
// ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly
// like a 2D array whose depth is 6 * the cube count.
case TextureTarget::TextureCubeMapArray:
g_GLESFuncs.glTexImage3D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), 0, glFormat, glType, uploadData);
break;
default:
MGLOG_E_ONCE("Unhandled texture target %s", MG_Util::ConvertTextureTargetToString(
MGB_TEXTURE_TARGET(stateTextureObject))
.c_str());
break;
}
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__,
glUploadTarget, glInternalFormat, glFormat, glType,
pData](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s. glTexImage*: target=%s, internalformat=%s, format=%s, "
"type=%s, pixels=%p",
func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(),
MG_Util::ConvertGLEnumToString(glUploadTarget).c_str(),
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
MG_Util::ConvertGLEnumToString(glFormat).c_str(),
MG_Util::ConvertGLEnumToString(glType).c_str(), pData);
});
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
}
}
needsRegeneration = false;
}
if (needsRegeneration) {
MGLOG_D("Texture state changed significantly or not initialized, regenerating texture with ID: %u",
m_backendTextureId);
// A REDEFINITION IN PLACE TAKES THE SAME GENERATION A RE-MINT TAKES (P4a fable
// seam F-3, the pre-handle half). Mutable driver storage is redefined on the
// SAME id below, so unlike RecreateBackendTexture nothing moves the FBO twins'
// memo: the frontend framebuffer versions do not see a texture's respecify
// and the id did not change. An attached texture whose format moved to one
// with the same carrier (GL_SRGB8 -> GL_SRGB8_ALPHA8 on a driver that widens
// the first) therefore kept the framebuffer's alpha-widening mask, and every
// draw into it stayed masked. The first definition is not a redefinition and
// bumps nothing; a redefinition that went through RecreateBackendTexture above
// has already bumped.
// PUSH BUILDS ONLY. This is Espryt code the pull build would share, and G1
// keeps the pull library byte-identical to the P4a baseline (the bump resized
// this function and the renderbuffer twin's SyncToBackend: 0/0/2/0). So the
// pull build carries the pre-P4a hole until these lines land on dev on their
// own; every arm of a push build has the fix.
if (m_isInitialized && !m_backendStorageImmutable) {
++FramebufferImpl::g_attachmentBackendIdGeneration;
}
// Regenerate all mipmap levels
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureMipmapObject), &glInternalFormat,
&glFormat, &glType, targetInternal);
ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType);
const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject);
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
DebugImpl::ErrorLopper::Clear();
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
// The frontend validates against the count MobileGL advertises, which can
// exceed what the driver takes for this format (Adreno: GL_MAX_SAMPLES 4,
// GL_MAX_INTEGER_SAMPLES 1). Clamp the ES call - and only the ES call:
// stateTextureObject keeps the requested count so GL_TEXTURE_SAMPLES and
// framebuffer completeness still report what the application asked for.
const auto backendSamples = static_cast<GLsizei>(ClampSamplesToBackendSupport(
GetFormatCapabilityTargetIndex(targetInternal), MGB_STORAGE_FORMAT(textureMipmapObject),
glFormat, static_cast<Int>(MGB_STORAGE_SAMPLES(stateTextureObject))));
// ES 3.1 8.19 requires width/height (and depth, for the array target) >= 1,
// so a degenerate size has nothing to allocate and must not reach the
// driver. The frontend deallocates such an image rather than defining it
// (GL 4.6 core 8.8), so this is belt and braces for any path that still
// syncs one.
const Bool hasAllocatableSize =
baseSize.x() >= 1 && baseSize.y() >= 1 &&
(targetInternal != TextureTarget::Texture2DMultisampleArray || baseSize.z() >= 1);
if (!hasAllocatableSize) {
MGLOG_D("Skipping multisample storage for texture %u: degenerate size (%d, %d, %d)",
m_backendTextureId, baseSize.x(), baseSize.y(), baseSize.z());
} else {
switch (targetInternal) {
case TextureTarget::Texture2DMultisample:
g_GLESFuncs.glTexStorage2DMultisample(
target, backendSamples, glInternalFormat, static_cast<GLsizei>(baseSize.x()),
static_cast<GLsizei>(baseSize.y()),
MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(stateTextureObject) ? GL_TRUE : GL_FALSE);
break;
case TextureTarget::Texture2DMultisampleArray:
g_GLESFuncs.glTexStorage3DMultisample(
target, backendSamples, glInternalFormat, static_cast<GLsizei>(baseSize.x()),
static_cast<GLsizei>(baseSize.y()), static_cast<GLsizei>(baseSize.z()),
MGB_STORAGE_FIXED_SAMPLE_LOCATIONS(stateTextureObject) ? GL_TRUE : GL_FALSE);
break;
default:
MOBILEGL_ASSERT(false, "Unexpected multisample target: %d",
static_cast<Int>(targetInternal));
break;
}
m_backendStorageImmutable = true;
}
// The one storage branch that cleared the ES error queue without ever
// draining it again, so anything this call raised was left for an
// unrelated later query to trip over. Paired with its two siblings now.
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__, target,
glInternalFormat, backendSamples](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s. glTexStorage*Multisample: target=%s, internalformat=%s, "
"samples=%d",
func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(),
MG_Util::ConvertGLEnumToString(target).c_str(),
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
static_cast<Int>(backendSamples));
});
for (const auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
}
}
} else if (MGB_STORAGE_IMMUTABLE(stateTextureObject) || m_imageBindableStorageRequired) {
DebugImpl::ErrorLopper::Clear();
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
const IntVec3 storageSize = GetBackendUploadSize(targetInternal, baseSize);
switch (MapToBackendTextureTarget(targetInternal)) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexStorage2D(target, static_cast<GLsizei>(mipmapCount), glInternalFormat,
static_cast<GLsizei>(storageSize.x()),
static_cast<GLsizei>(storageSize.y()));
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
case TextureTarget::TextureCubeMapArray:
g_GLESFuncs.glTexStorage3D(target, static_cast<GLsizei>(mipmapCount), glInternalFormat,
static_cast<GLsizei>(storageSize.x()),
static_cast<GLsizei>(storageSize.y()),
static_cast<GLsizei>(storageSize.z()));
break;
default:
MGLOG_E_ONCE("Unhandled immutable texture target %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
break;
}
DebugImpl::ErrorLopper::Loop(
[file = __FILE__, line = __LINE__, func = __func__, target, glInternalFormat](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s. glTexStorage*: target=%s, internalformat=%s", func,
file, line, MG_Util::ConvertGLEnumToString(err).c_str(),
MG_Util::ConvertGLEnumToString(target).c_str(),
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
});
m_backendStorageImmutable = true;
ScopedDefaultUnpackState unpackState;
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level);
const bool levelDirty =
MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level);
if (levelDirty && levelByteSize != 0) {
auto levelTexelSize =
MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData =
MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level, "immutable-regen");
Vector<Float> convertedUploadData;
Vector<Uint8> widenedUploadData;
const void* uploadData = PrepareFallbackUpload(
MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, levelTexelSize, pData,
levelByteSize, glType, convertedUploadData, widenedUploadData);
Vector<Uint8> packedUploadData;
uploadData =
PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), levelTexelSize,
uploadData, levelByteSize, &glType, packedUploadData);
Vector<Uint8> imageWidenedUploadData;
uploadData = PrepareImageWidenedUpload(imageWidening, levelTexelSize, uploadData,
levelByteSize, imageWidenedUploadData);
DebugImpl::ErrorLopper::Clear();
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
const IntVec3 uploadSize = GetBackendUploadSize(targetInternal, levelTexelSize);
switch (MapToBackendTextureTarget(targetInternal)) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexSubImage2D(glUploadTarget, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(uploadSize.x()),
static_cast<GLsizei>(uploadSize.y()), glFormat,
glType, uploadData);
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
case TextureTarget::TextureCubeMapArray:
g_GLESFuncs.glTexSubImage3D(
glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), glFormat, glType, uploadData);
break;
default:
break;
}
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__,
glUploadTarget, glFormat, glType, pData](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s. glTexSubImage*: target=%s, format=%s, "
"type=%s, pixels=%p",
func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(),
MG_Util::ConvertGLEnumToString(glUploadTarget).c_str(),
MG_Util::ConvertGLEnumToString(glFormat).c_str(),
MG_Util::ConvertGLEnumToString(glType).c_str(), pData);
});
}
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
}
}
} else {
m_backendStorageImmutable = false;
ScopedDefaultUnpackState unpackState;
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
auto levelTexelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level);
// See the append-mips loop: an undefined level stays undefined on the
// driver rather than becoming a 0x0 image.
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 || levelTexelSize.z() <= 0) {
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
continue;
}
auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level);
bool levelDirty = MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData =
(levelDirty && levelByteSize != 0)
? MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level, "mutable-regen")
: nullptr;
Vector<Float> convertedUploadData;
Vector<Uint8> widenedUploadData;
const void* uploadData = PrepareFallbackUpload(
MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, levelTexelSize, pData,
levelByteSize, glType, convertedUploadData, widenedUploadData);
Vector<Uint8> packedUploadData;
uploadData =
PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), levelTexelSize,
uploadData, levelByteSize, &glType, packedUploadData);
MGLOG_D("%s: target: %s: syncing mip %d: %dx%dx%d, byteSize = %d, pData = %p, "
"levelDirty = %s",
__func__, MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
level, levelTexelSize.x(), levelTexelSize.y(), levelTexelSize.z(),
levelByteSize, pData, levelDirty ? "true" : "false");
DebugImpl::ErrorLopper::Clear();
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
auto textureTarget = MGB_TEXTURE_TARGET(stateTextureObject);
const IntVec3 uploadSize = GetBackendUploadSize(textureTarget, levelTexelSize);
switch (MapToBackendTextureTarget(textureTarget)) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap: {
g_GLESFuncs.glTexImage2D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()), 0,
glFormat, glType, uploadData);
break;
}
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
case TextureTarget::TextureCubeMapArray: {
g_GLESFuncs.glTexImage3D(
glUploadTarget, static_cast<GLint>(level), (GLint)glInternalFormat,
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), 0, glFormat, glType, uploadData);
break;
}
default: {
MGLOG_E_ONCE("Unhandled texture target %s",
MG_Util::ConvertTextureTargetToString(textureTarget).c_str());
}
}
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__,
glUploadTarget, glInternalFormat, glFormat, glType,
pData](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s. glTexImage*: target=%s, internalformat=%s, "
"format=%s, type=%s, pixels=%p",
func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(),
MG_Util::ConvertGLEnumToString(glUploadTarget).c_str(),
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
MG_Util::ConvertGLEnumToString(glFormat).c_str(),
MG_Util::ConvertGLEnumToString(glType).c_str(), pData);
});
MGLOG_D("Regenerated mipmap level %d for texture with ID: %u", level,
m_backendTextureId);
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
}
}
}
m_isInitialized = true;
}
{ // Update all dirty mipmap levels
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject);
for (const auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
if (MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level)) {
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
}
}
}
break;
}
const auto mipmapCount = MGB_STORAGE_LEVELS(textureMipmapObject);
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureMipmapObject), &glInternalFormat,
&glFormat, &glType, targetInternal);
// The storage this level is being written into was widened when it was minted
// (see above), so the transfer pair has to describe the carrier here too - ES
// requires glTexSubImage's `format` to match the storage's base internal
// format, so a GL_RG upload into a GL_RGBA32F image is GL_INVALID_OPERATION.
ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType);
const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject);
ScopedDefaultUnpackState unpackState;
// Per-level scratch of the exact-footprint path, hoisted to reuse capacity.
Vector<MG_State::GLState::MipmapDirtyRegion> dirtyFootprint;
Vector<UnpackStagingBlock> stagingBlockOverflow;
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
if (!MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level)) {
continue;
}
auto byteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level);
if (byteSize == 0) {
MGLOG_D("Mipmap level %d has no data, skipping update.", level);
continue;
}
if (level > 0)
MGLOG_D("%s: Updating dirty mip %d for texture ID %u, size: %dx%d, "
"byteSize: %d",
__func__, level, m_backendTextureId,
MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level).x(),
MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level).y(), byteSize);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
DebugImpl::ErrorLopper::Loop(
[file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s", func, file, line,
MG_Util::ConvertGLEnumToString(err).c_str());
});
auto texelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level);
const void* mipData =
MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level, "dirty-level");
Vector<Float> convertedUploadData;
Vector<Uint8> widenedUploadData;
const void* uploadData = PrepareFallbackUpload(
MGB_STORAGE_FORMAT(textureMipmapObject), targetInternal, texelSize, mipData, byteSize,
glType, convertedUploadData, widenedUploadData);
Vector<Uint8> packedUploadData;
uploadData = PreparePackedNormUpload(MGB_STORAGE_FORMAT(textureMipmapObject), texelSize,
uploadData, byteSize, &glType, packedUploadData);
// Leaves `uploadData` pointing at its own buffer when it fires, with
// texels four components wide where the shadow's are one or two.
Vector<Uint8> imageWidenedUploadData;
uploadData = PrepareImageWidenedUpload(imageWidening, texelSize, uploadData, byteSize,
imageWidenedUploadData);
const IntVec3 uploadSize =
GetBackendUploadSize(MGB_TEXTURE_TARGET(stateTextureObject), texelSize);
// Sub-rect upload: when only a region of the level changed (a
// 16x16 sprite in a 1024x512 atlas, the per-frame lightmap), upload
// just that region with UNPACK_ROW_LENGTH striding into the level.
// Not an optimization alone: the shadow never sees a GPU write, so
// the texels OUTSIDE the region may be stale there and must not go
// up over what the GPU drew. Every conversion above rewrites the
// whole level texel for texel into a tight buffer of its own, so the
// region is taken from whichever buffer `uploadData` now names, at
// that buffer's bytes per texel.
//
// THE SAME ON EVERY ARM (P11 M). `mipData` is the frontend shadow on
// the pull and push-monolith arms and the server's staged level on the
// split arms - the same tight client layout either way - and the
// conversions run on whichever side uploads, so the handle arm takes
// its region from the converted buffer exactly as the shadow arm
// does. What it must NOT take from the record then is the carried
// pitch and offset (below): those index the client's layout, not the
// converted one.
SizeT uploadBytes = 0;
if (uploadData == mipData) {
uploadBytes = byteSize;
} else if (!imageWidenedUploadData.empty() && uploadData == imageWidenedUploadData.data()) {
uploadBytes = imageWidenedUploadData.size();
} else if (!packedUploadData.empty() && uploadData == packedUploadData.data()) {
uploadBytes = packedUploadData.size();
} else if (!widenedUploadData.empty() && uploadData == widenedUploadData.data()) {
uploadBytes = widenedUploadData.size();
} else if (!convertedUploadData.empty() && uploadData == convertedUploadData.data()) {
uploadBytes = convertedUploadData.size() * sizeof(Float);
}
// A 1D array's ES image is a 2D array one texel high with the layers
// in depth, while the shadow keeps the layers as rows: same texels in
// the same order, so a region maps across by moving its y to z. (The
// target is the descriptor's on the split arms: the server may not
// name the client's texture object.)
const Bool layersAsRows =
MGB_TEXTURE_TARGET(stateTextureObject) == TextureTarget::Texture1DArray;
// P4a (D-D6, and ARCHITECTURE.md §9.1's ONE item moved out of the
// Espryt do-not-touch list): the DECISION below is unchanged and
// stays on the server - it is the side that pays the GPU cost, and
// the union-box-versus-N-rects choice is worth 16x the bytes on one
// axis and +6 ms/frame of Mali job cost on the other. What moves is
// the SOURCE of the shape and of the strides.
//
// On the handle arm the box and the rect list come from the record
// the client emitted; on the legacy arm they come from the frontend's
// own rect model, exactly as before. Both spellings feed the same
// three staging shapes and the same StageBlocksIntoUnpackRing, which
// is byte-identical (G5).
const MG_Pipe::MGPipeResourceRecord::PendingUpload* pendingUpload =
pushedStorage != nullptr
? FindPipeTextureUpload(*pushedStorage, static_cast<Uint16>(uploadTarget),
static_cast<Uint16>(level))
: nullptr;
const auto dirtyRegion = [&]() -> MG_State::GLState::MipmapDirtyRegion {
if (pendingUpload == nullptr) {
// P5c (tx): a level this arm owes with NO pending upload behind it
// was dirtied by the GPU (T5), and the dirty answer is the
// server's own mark on the staged shadow - the whole level,
// because a generation touches all of it. The client is never
// asked (§2.2's last row).
if (MGB_STAGED_TEXTURE_LIVE) {
const IntVec3 gpuExtent =
MG_Record::ServerStagedTexture().LevelExtentOrUndefined(
MG_Record::StagedTextureStore::KeyForHandle(pushedRes),
static_cast<Uint16>(uploadTarget), static_cast<Uint16>(level));
return MG_State::GLState::MipmapDirtyRegion{IntVec3{0, 0, 0}, gpuExtent};
}
return textureMipmapObject->GetStorageDirtyRegion(uploadTarget, level);
}
// MGPBox is {origin, extent}; MipmapDirtyRegion is {lo, hi}. The
// conversion is the whole difference between the two spellings.
const auto& box = pendingUpload->UnionBox;
return MG_State::GLState::MipmapDirtyRegion{IntVec3{box.X, box.Y, box.Z},
IntVec3{box.X + static_cast<Int32>(box.W),
box.Y + static_cast<Int32>(box.H),
box.Z + static_cast<Int32>(box.D)}};
}();
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
static_cast<SizeT>(texelSize.y()) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
// Whether a box of the level can be addressed at all: whole texels in
// the upload bytes, and an ES image shaped like the shadow.
const Bool regionsAddressable =
!dirtyRegion.Empty() && texelCount > 0 && uploadBytes > 0 &&
uploadBytes % texelCount == 0 &&
(layersAsRows ? std::max(texelSize.z(), 1) == 1
: uploadSize.x() == texelSize.x() && uploadSize.y() == texelSize.y() &&
std::max(uploadSize.z(), 1) == std::max(texelSize.z(), 1));
const Bool subRectEligible = regionsAddressable && !dirtyRegion.CoversWholeLevel(texelSize);
const SizeT bpp = regionsAddressable ? uploadBytes / texelCount : 0;
const IntVec3 regionSize = {dirtyRegion.hi.x() - dirtyRegion.lo.x(),
dirtyRegion.hi.y() - dirtyRegion.lo.y(),
dirtyRegion.hi.z() - dirtyRegion.lo.z()};
// STRIDES ARE CARRIED, NEVER INFERRED (D-D3), once there is a record
// to carry them: a sub-rect's rows are not contiguous in the level
// shadow, so each MGPSubRegion states the level's row and slice pitch
// and 0 means "tightly packed", which is what a whole-level region
// sends. The legacy arm keeps computing them from the level extent
// and the bytes-per-texel it just derived - the same two numbers,
// from the other side of the wire.
// m-2: THE PITCH IS THE LEVEL'S (D-D3), so every region of one level
// states the same one - and v1 read Regions.front() and would have
// strided the rest at the first one's pitch, silently, if a record
// ever disagreed. This is the assert the review asked for rather than
// a per-region loop feeding per-region strides: a record whose regions
// disagree is corrupt, not a shape the upload planner should learn.
// On disagreement the carried pair is DROPPED and the level's own
// extent times bpp is used, which is what the legacy arm computes.
Bool pendingStridesAgree = true;
if (pendingUpload != nullptr && !pendingUpload->Regions.empty()) {
const auto& firstRegion = pendingUpload->Regions.front();
for (const auto& region : pendingUpload->Regions) {
if (region.SrcRowStride == firstRegion.SrcRowStride &&
region.SrcSliceStride == firstRegion.SrcSliceStride) {
continue;
}
pendingStridesAgree = false;
MGLOG_E_ONCE("MGPipe: texture %u level %u carries regions with two "
"different level pitches (%u/%u and %u/%u) - a pitch is "
"the LEVEL's, so the carried pair is dropped and the "
"extent is used instead",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject),
static_cast<Uint>(level), firstRegion.SrcRowStride,
firstRegion.SrcSliceStride, region.SrcRowStride,
region.SrcSliceStride);
break;
}
}
// P11 M: THE CARRIED PITCH AND OFFSET INDEX THE CLIENT'S LAYOUT - the
// level shadow, or the server's staged copy of it, which is `mipData`.
// A converted level's region is cut from the conversion's own tight
// buffer (above), whose texels are wider, so there the record's numbers
// name the wrong bytes and the pitch is derived from the upload
// buffer's bpp instead - which is what the shadow arm does.
const Bool carriedLayoutApplies = uploadData == mipData;
const SizeT carriedRowBytes =
(pendingUpload != nullptr && pendingStridesAgree && carriedLayoutApplies &&
!pendingUpload->Regions.empty() && pendingUpload->Regions.front().SrcRowStride != 0)
? static_cast<SizeT>(pendingUpload->Regions.front().SrcRowStride)
: 0;
const SizeT carriedSliceBytes =
(pendingUpload != nullptr && pendingStridesAgree && carriedLayoutApplies &&
!pendingUpload->Regions.empty() && pendingUpload->Regions.front().SrcSliceStride != 0)
? static_cast<SizeT>(pendingUpload->Regions.front().SrcSliceStride)
: 0;
const SizeT levelRowBytes =
carriedRowBytes != 0 ? carriedRowBytes : static_cast<SizeT>(texelSize.x()) * bpp;
const SizeT levelSliceBytes = carriedSliceBytes != 0
? carriedSliceBytes
: static_cast<SizeT>(texelSize.y()) * levelRowBytes;
// N-5: THE SAME DISCIPLINE FOR SrcOffset AS FOR THE STRIDES, and the
// asymmetry it removes was the review's point: the strides got a loud
// refusal in the commit that started reading them while the offset -
// which decides the ADDRESS glTexSubImage reads (w*bpp x h x d) bytes
// from - was consumed raw. The cross-check is free, because
// rectUploadPtr four lines down computes exactly the same number from
// the region origin and the two pitches. A record whose regions
// disagree with their own origins is corrupt, so on disagreement the
// carried offsets are DROPPED - every use site falls back to the
// derived pointer, which is what the legacy arm sends - and one named
// line says so. Gated on subRectEligible because bpp is 0 otherwise
// and the regions are not read at all, and on carriedLayoutApplies
// because a converted level never reads them (MGB_RECT_SRC_PTR).
//
// FIX A2 IS THE SECOND LEGITIMATE DISAGREEMENT, and the fallback is
// exactly right for it: a level too large to stage whole crosses as
// several slab records (TextureEmit.h's MGPipeForEachTextureSlab) whose
// regions carry offsets into THEIR OWN run rather than into the level
// shadow. Every piece of the level is merged into this one entry, the
// entry's rects are then read out of the SERVER'S WHOLE LEVEL IMAGE -
// which is what the derived pointer names - and those piece-relative
// offsets are precisely the ones that must not be used here.
//
// GATED ON regionsAddressable, NOT subRectEligible: the per-region arm
// for a level the GPU may have written (m_driverMayHoldGpuWrites below)
// reads MGB_RECT_SRC_PTR too, and it runs exactly when the union box is
// the whole level - which is what two or more slab pieces always add up
// to. Checked only under subRectEligible, a damage rect split across
// pieces kept the later pieces' run-relative offsets and read their
// rows from the top of the level image (KWin's wl_shm window textures:
// the panel repeated down the desktop). bpp is valid whenever the
// regions are addressable.
Bool pendingOffsetsAgree = true;
if (pendingUpload != nullptr && regionsAddressable && carriedLayoutApplies) {
for (const auto& region : pendingUpload->Regions) {
const SizeT derivedOffset = static_cast<SizeT>(region.Z) * levelSliceBytes +
static_cast<SizeT>(region.Y) * levelRowBytes +
static_cast<SizeT>(region.X) * bpp;
if (static_cast<SizeT>(region.SrcOffset) == derivedOffset) continue;
pendingOffsetsAgree = false;
MGLOG_E_ONCE("MGPipe: texture %u level %u carries a region whose SrcOffset "
"%llu is not its own origin (%d,%d,%d) at the level's pitch "
"(%llu expected) - the carried offsets are dropped and every "
"rect is read from the shadow instead. Expected once for a "
"level cut into stage-chunk pieces, whose regions index their "
"own run; otherwise this names a record whose regions and "
"origins disagree",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject),
static_cast<Uint>(level),
static_cast<unsigned long long>(region.SrcOffset), region.X, region.Y,
region.Z, static_cast<unsigned long long>(derivedOffset));
break;
}
}
const Uint8* regionPtr = static_cast<const Uint8*>(uploadData) +
static_cast<SizeT>(dirtyRegion.lo.z()) * levelSliceBytes +
static_cast<SizeT>(dirtyRegion.lo.y()) * levelRowBytes +
static_cast<SizeT>(dirtyRegion.lo.x()) * bpp;
// Scatter refinement behind the union box: ~100 sprite
// writes into an atlas leave a box that spans nearly the
// whole level while the touched texels are a few percent of
// it. The storage's bounded rect list recovers the true
// footprint; each rect is uploaded with the same
// ROW_LENGTH striding into the level as the box
// path. The storage only hands the list out when its
// summed area is materially smaller than the box (0
// otherwise), so the extra calls always move fewer bytes.
MG_State::GLState::MipmapDirtyRegion
dirtyRects[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
SizeT dirtyRectCount = 0;
if (subRectEligible) {
if (pendingUpload != nullptr) {
// RegionCount == 0 is LEGAL and means "the union box is the
// whole story" (D-D3), which is the same statement the
// frontend's GetStorageDirtyRects makes by returning 0 - so
// both arms reach the box branch below by the same route.
//
// P11 M2: the record's list is the level's EXACT footprint now
// (TextureEmit.h), which the storage's own rect list only
// covered, and the storage withheld that list - "0 rects" - when
// it held more rects than it keeps or when they added up to 3/4
// of the union box (one driver call on nearly the same bytes).
// Both cut-offs are this side's to apply now, so the box-or-rects
// choice on a texture the GPU has not written is what it was.
// (Once the GPU may have written it, the footprint below uploads
// every region exactly, whatever this chose.) Never a partial
// list: a truncated list is dropped writes.
SizeT summedTexels = 0;
for (const auto& region : pendingUpload->Regions) {
summedTexels += static_cast<SizeT>(region.W) * static_cast<SizeT>(region.H) *
static_cast<SizeT>(std::max<Uint32>(region.D, 1));
}
const Bool listWithheld = pendingUpload->Regions.size() < 2 ||
pendingUpload->Regions.size() >
MG_State::GLState::MipmapStorage::kMaxDirtyRects ||
summedTexels * 4 >= dirtyRegion.TexelCount() * 3;
dirtyRectCount = listWithheld ? 0 : pendingUpload->Regions.size();
for (SizeT r = 0; r < dirtyRectCount; ++r) {
const auto& region = pendingUpload->Regions[r];
dirtyRects[r] = MG_State::GLState::MipmapDirtyRegion{
IntVec3{region.X, region.Y, region.Z},
IntVec3{region.X + static_cast<Int32>(region.W),
region.Y + static_cast<Int32>(region.H),
region.Z + static_cast<Int32>(region.D)}};
}
} else
// tx: with an active transport the rect list is the record's (the
// pendingUpload arm above) or nothing - the server's GPU-dirty mark
// is whole-level and has no scatter refinement to hand out, and the
// frontend's rect model is not this side's to read.
if (!MGB_STAGED_TEXTURE_LIVE)
dirtyRectCount = textureMipmapObject->GetStorageDirtyRects(
uploadTarget, level, dirtyRects,
MG_State::GLState::MipmapStorage::kMaxDirtyRects);
// The scatter refinement pays only on the client-pointer path,
// where fewer bytes mean less driver-side copying. Through the
// unpack ring every glTexSubImage is a GPU copy job (Mali), so
// ~100 sprite rects become ~100 jobs whose fixed cost dwarfs
// the union box's extra bytes - measured +6 ms/frame of GPU
// time in MC's animated-atlas ticks. One box, one job.
if (BufferImpl::UnpackRingAvailable()) {
dirtyRectCount = 0;
}
}
// Everything above - the union box, the ring's one-box rule, the rect
// list with its merges, fold and 3/4 cut-off - may upload shadow texels
// the client did not write, which is only harmless while the driver
// still holds those texels as they were last uploaded. Once it may hold
// GPU writes, the level goes up as exactly the texels written, one box
// (one ring job) each, whatever that costs: more boxes, and a union box
// that spans the whole level no longer means a whole-level upload.
const MG_State::GLState::MipmapDirtyRegion* uploadRects = dirtyRects;
SizeT uploadRectCount = dirtyRectCount;
if (m_driverMayHoldGpuWrites && regionsAddressable) {
if (pendingUpload != nullptr) {
// The handle arm's footprint is the record's region list, all
// of it and in order (MGB_RECT_SRC_PTR indexes the regions by
// position); a box-only entry is its union box.
dirtyFootprint.clear();
for (const auto& region : pendingUpload->Regions) {
dirtyFootprint.push_back(MG_State::GLState::MipmapDirtyRegion{
IntVec3{region.X, region.Y, region.Z},
IntVec3{region.X + static_cast<Int32>(region.W),
region.Y + static_cast<Int32>(region.H),
region.Z + static_cast<Int32>(region.D)}});
}
} else
// With an active transport and no record behind the level, the
// server's own whole-level GPU-dirty mark is the whole story (the
// same reason the rect list above is not read here).
if (MGB_STAGED_TEXTURE_LIVE) {
dirtyFootprint.clear();
} else
textureMipmapObject->GetStorageDirtyFootprint(uploadTarget, level,
dirtyFootprint);
// One box is the union box itself, which the branches below already
// upload exactly (as the whole level when it is the whole level).
if (dirtyFootprint.size() >= 2) {
uploadRects = dirtyFootprint.data();
uploadRectCount = dirtyFootprint.size();
} else {
uploadRectCount = 0;
}
}
const Bool perRectUpload = regionsAddressable && uploadRectCount >= 2;
const auto rectUploadPtr = [&](const MG_State::GLState::MipmapDirtyRegion& rect) {
return static_cast<const Uint8*>(uploadData) +
static_cast<SizeT>(rect.lo.z()) * levelSliceBytes +
static_cast<SizeT>(rect.lo.y()) * levelRowBytes +
static_cast<SizeT>(rect.lo.x()) * bpp;
};
// m-1 / D-D3, "carried, never inferred": MGPSubRegion::SrcOffset is the byte offset of that
// region's origin into the level shadow - the same number the lambda above derives from the
// origin and the two strides - and on the handle arm it is READ rather than re-derived, after
// the cross-check above has agreed that the two spellings say the same thing (N-5). It is read
// only while carriedLayoutApplies (uploadData == mipData), so the base the offset is relative to
// is the level shadow itself and never a conversion buffer: since dev's 8966d5c2 a converted
// level is sub-rect eligible too, and its rects are cut at the conversion's bpp (rectUploadPtr).
//
// THREE USE SITES, AND THE FIRST OF THEM IS UNREACHABLE (N-5's second half, and the correction
// to what esprytobj-v2.md m-1 claimed): dirtyRectCount is forced to 0 whenever
// UnpackRingAvailable(), and the staging-block loop that takes MGB_RECT_SRC_PTR(r, rect) first
// requires `ringUsable && dirtyRectCount >= 2`. The two conditions cannot both hold. That shape
// is pre-existing (the ring/scatter decision, not this package's), so the loop is left as it
// stands and the count is corrected here instead of the loop being deleted underneath it.
//
// A MACRO AND NOT A SECOND LAMBDA, for DV-9's measured reason: this function's nine ErrorLopper
// lambdas are mangled ...::$_N BY POSITION, so one more renumbers every one of them and moves
// the PULL build's symbol set, whose admitted-change set is EMPTY. The pull expansion is the
// pre-P4a call with one pair of parentheses. #undef'd with the rest.
#define MGB_RECT_SRC_PTR(idx, rect) \
((pendingUpload != nullptr && pendingOffsetsAgree && carriedLayoutApplies && \
static_cast<SizeT>(idx) < pendingUpload->Regions.size()) \
? static_cast<const Uint8*>(uploadData) + \
static_cast<SizeT>(pendingUpload->Regions[static_cast<SizeT>(idx)].SrcOffset) \
: rectUploadPtr(rect))
// A shadow-grid box as the ES image addresses it, and the image height
// that strides the source to match; they differ only for a 1D array
// (layersAsRows), whose rows are ES layers of a one-texel-high image.
struct BackendBox {
GLint x, y, z;
GLsizei w, h, d;
};
const auto ToBackendBox = [&](const MG_State::GLState::MipmapDirtyRegion& rect) {
const GLsizei w = static_cast<GLsizei>(rect.hi.x() - rect.lo.x());
const GLsizei h = static_cast<GLsizei>(rect.hi.y() - rect.lo.y());
const GLsizei d = static_cast<GLsizei>(rect.hi.z() - rect.lo.z());
return layersAsRows ? BackendBox{rect.lo.x(), 0, rect.lo.y(), w, 1, h}
: BackendBox{rect.lo.x(), rect.lo.y(), rect.lo.z(), w, h, d};
};
const GLint imageHeight = layersAsRows ? 1 : texelSize.y();
// Unpack-ring staging plan, decided ONCE for whichever branch
// below runs: either every glTexSubImage of this level sources
// from the ring or none does, so the pixel-unpack binding is
// toggled exactly once per level. The plan's three shapes line
// up 1:1 with the switch's three branches.
//
// Regions are repacked TIGHTLY (row length = the region's own
// width), which is why the ring path issues no glPixelStorei at
// all: the surrounding ScopedDefaultUnpackState already holds
// ROW_LENGTH/IMAGE_HEIGHT at 0, which is exactly what a tight
// block wants. It also stages strictly fewer bytes than the
// client-pointer path makes the driver walk, which strides over
// the whole level width.
UnpackStagingBlock stagingBlockArray[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
UnpackStagingBlock* stagingBlocks = stagingBlockArray;
SizeT stagingBlockCount = 0;
const Bool ringUsable = BufferImpl::UnpackRingAvailable();
if (!ringUsable) {
// Nothing to plan: every branch below keeps the client
// pointer and its ROW_LENGTH striding, unchanged.
} else if (perRectUpload) {
// Only an exact footprint outgrows the array (see uploadRects).
if (uploadRectCount > MG_State::GLState::MipmapStorage::kMaxDirtyRects) {
stagingBlockOverflow.resize(uploadRectCount);
stagingBlocks = stagingBlockOverflow.data();
}
for (SizeT r = 0; r < uploadRectCount; ++r) {
const auto& rect = uploadRects[r];
stagingBlocks[r] = {MGB_RECT_SRC_PTR(r, rect),
static_cast<SizeT>(rect.hi.x() - rect.lo.x()) * bpp,
static_cast<SizeT>(rect.hi.y() - rect.lo.y()),
static_cast<SizeT>(std::max(rect.hi.z() - rect.lo.z(), 1)),
levelRowBytes,
levelSliceBytes,
0};
}
stagingBlockCount = uploadRectCount;
} else if (subRectEligible) {
stagingBlocks[0] = {regionPtr,
static_cast<SizeT>(regionSize.x()) * bpp,
static_cast<SizeT>(regionSize.y()),
static_cast<SizeT>(std::max(regionSize.z(), 1)),
levelRowBytes,
levelSliceBytes,
0};
stagingBlockCount = 1;
} else if (uploadData == mipData && texelCount > 0 && byteSize % texelCount == 0) {
// Whole level, shadow bytes verbatim: `byteSize` is exactly
// what the driver would have read from the client pointer,
// and the shadow is already tightly packed. A CONVERTED
// level stays on the pointer path - its buffer's length is
// the conversion's, not the shadow's, so nothing here can
// size the driver's read of it. The whole-texels check is
// the same sanity the sub-rect path applies, and it is what
// makes "the shadow's bytes per texel == the transfer's"
// hold. (A 1D array's upload size permutes height and depth,
// but the texel count and the tight byte order are the same,
// so the flat copy still describes what the driver reads.)
stagingBlocks[0] = {
static_cast<const Uint8*>(uploadData), static_cast<SizeT>(byteSize), 1, 1,
static_cast<SizeT>(byteSize), static_cast<SizeT>(byteSize), 0};
stagingBlockCount = 1;
}
const Bool ringStaged =
stagingBlockCount > 0 && StageBlocksIntoUnpackRing(stagingBlocks, stagingBlockCount);
if (ringStaged) {
BufferImpl::BindPixelUnpackBufferId(BufferImpl::UnpackRingBufferId());
}
// Ring-sourced or not: see NoteInPlaceTextureUpload.
NoteInPlaceTextureUpload();
if (MG_Util::PipeStats::Enabled()) {
// One emission per (upload target, level) that ships texels;
// the switch below turns it into either one union-box job or
// dirtyRectCount rect jobs. The box/rect split is counted
// separately from the bytes on purpose: SSIM is blind to it
// and the +6 ms/frame Mali cliff was a shape regression, not
// a byte regression (plan section 7.3).
const Bool rectShape = subRectEligible && dirtyRectCount >= 2;
Uint64 shippedBytes = 0;
if (rectShape) {
for (SizeT r = 0; r < dirtyRectCount; ++r) {
const auto& rect = dirtyRects[r];
shippedBytes += static_cast<Uint64>(rect.hi.x() - rect.lo.x()) *
static_cast<Uint64>(rect.hi.y() - rect.lo.y()) *
static_cast<Uint64>(std::max(rect.hi.z() - rect.lo.z(), 1)) *
static_cast<Uint64>(bpp);
}
} else if (subRectEligible) {
shippedBytes =
static_cast<Uint64>(regionSize.x()) * static_cast<Uint64>(regionSize.y()) *
static_cast<Uint64>(std::max(regionSize.z(), 1)) * static_cast<Uint64>(bpp);
} else {
shippedBytes = static_cast<Uint64>(byteSize);
}
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageTexture, shippedBytes);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadEmissions, 1);
MG_Util::PipeStats::AddCalls(
rectShape ? MG_Util::PipeStats::CallClass::TextureUploadRectEmissions
: MG_Util::PipeStats::CallClass::TextureUploadBoxEmissions,
1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadJobs,
rectShape ? static_cast<Uint64>(dirtyRectCount) : 1u);
}
switch (MapToBackendTextureTarget(MGB_TEXTURE_TARGET(stateTextureObject))) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
if (perRectUpload) {
if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
for (SizeT r = 0; r < uploadRectCount; ++r) {
const auto& rect = uploadRects[r];
g_GLESFuncs.glTexSubImage2D(
glUploadTarget, static_cast<GLint>(level), rect.lo.x(), rect.lo.y(),
static_cast<GLsizei>(rect.hi.x() - rect.lo.x()),
static_cast<GLsizei>(rect.hi.y() - rect.lo.y()), glFormat, glType,
ringStaged ? UnpackRingPixelOffset(stagingBlocks[r].offset)
: static_cast<const void*>(MGB_RECT_SRC_PTR(r, rect)));
}
// The surrounding ScopedDefaultUnpackState shadow says 0.
if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
} else if (subRectEligible) {
if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
g_GLESFuncs.glTexSubImage2D(
glUploadTarget, static_cast<GLint>(level), dirtyRegion.lo.x(),
dirtyRegion.lo.y(), static_cast<GLsizei>(regionSize.x()),
static_cast<GLsizei>(regionSize.y()), glFormat, glType,
ringStaged ? UnpackRingPixelOffset(stagingBlocks[0].offset)
: static_cast<const void*>(regionPtr));
// The surrounding ScopedDefaultUnpackState shadow says 0.
if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
} else {
g_GLESFuncs.glTexSubImage2D(
glUploadTarget, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
glFormat, glType,
ringStaged ? UnpackRingPixelOffset(stagingBlocks[0].offset) : uploadData);
}
break;
case TextureTarget::Texture3D:
case TextureTarget::Texture2DArray:
// ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly
// like a 2D array whose depth is 6 * the cube count.
case TextureTarget::TextureCubeMapArray:
if (perRectUpload) {
if (!ringStaged) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight);
}
for (SizeT r = 0; r < uploadRectCount; ++r) {
const auto box = ToBackendBox(uploadRects[r]);
g_GLESFuncs.glTexSubImage3D(
glUploadTarget, static_cast<GLint>(level), box.x, box.y, box.z, box.w,
box.h, box.d, glFormat, glType,
ringStaged ? UnpackRingPixelOffset(stagingBlocks[r].offset)
: static_cast<const void*>(MGB_RECT_SRC_PTR(r, uploadRects[r])));
}
if (!ringStaged) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
}
} else if (subRectEligible) {
if (!ringStaged) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight);
}
const auto box = ToBackendBox(dirtyRegion);
g_GLESFuncs.glTexSubImage3D(glUploadTarget, static_cast<GLint>(level), box.x, box.y,
box.z, box.w, box.h, box.d, glFormat, glType,
ringStaged
? UnpackRingPixelOffset(stagingBlocks[0].offset)
: static_cast<const void*>(regionPtr));
if (!ringStaged) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
}
} else {
g_GLESFuncs.glTexSubImage3D(
glUploadTarget, static_cast<GLint>(level), 0, 0, 0,
static_cast<GLsizei>(uploadSize.x()), static_cast<GLsizei>(uploadSize.y()),
static_cast<GLsizei>(uploadSize.z()), glFormat, glType,
ringStaged ? UnpackRingPixelOffset(stagingBlocks[0].offset) : uploadData);
}
break;
default:
MGLOG_E_ONCE("Unhandled texture target %s", MG_Util::ConvertTextureTargetToString(
MGB_TEXTURE_TARGET(stateTextureObject))
.c_str());
break;
}
if (ringStaged) {
// Back to the resting unbound state every other upload site
// in this file assumes (their BindPixelUnpackBufferId(0) is
// meant to stay a shadow no-op).
BufferImpl::BindPixelUnpackBufferId(0);
}
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
}
}
}
break;
}
case TextureStorageType::Buffer: {
auto* textureBufferObject =
static_cast<MG_State::GLState::TextureObjectBuffer*>(stateTextureObject.get());
// P5e (tx2), scout R5 RESOLVED. THE BACKING BUFFER IS THE DESCRIPTOR'S, AND SO IS
// ITS WINDOW - `Desc.BufferForTexBuffer` names the store and `Desc.BufOffset /
// BufSize` name the range, so the frontend binding slot below is not read at all
// on the handle arm.
//
// The one thing that argument had to answer is D-A4's `frontend` parameter of
// EnsureBufferResourceForHandle, which existed for the emulated-persistent-map
// question. The E note's reading is confirmed at this head: that function's three
// frontend reads (Managers.cpp's MappedData / SyncPersistentMappedRange /
// HasDefinedContent) are all inside its MONOLITH arm - under an active transport
// it is client-free (kimi audit row 29) - so `nullptr` is not a hole, it is the
// whole of the split answer, and it is what §4.2's Buffer row already prescribes
// (`EnsureBufferResourceForHandle(nullptr, h)`).
//
// WHOLE-VS-RANGE likewise stops asking the buffer object for its size: the
// sentinel kMGPipeWholeBuffer IS "the whole store", which is the question
// `rangeSize == buffer->GetSize()` was spelling the long way round.
MG_Pipe::MGPipeHandle pushedTexBuffer = MG_Pipe::kMGPipeNullHandle;
if (pushedStorage != nullptr) {
pushedTexBuffer = pushedStorage->Desc.BufferForTexBuffer;
if (MG_Pipe::MGPipeHandleIsNull(pushedTexBuffer)) {
MGLOG_E_ONCE("MGPipe: buffer texture %u's descriptor names no backing buffer on "
"the handle arm, so it cannot be backed (handle {%u, %u})",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), pushedRes.Slot,
pushedRes.Gen);
return;
}
}
// P5e (fix1 follow-up, ID-110): the third conjunct is the one fix1's report
// asked for a ruling about. This arm hands the backing-buffer lookup
// kNoFrontendBuffer and leans on the record answering; it was unreachable
// under monolith only because every writer of a handle-note is
// transport-gated - safe by a chain rather than by its own statement, which
// is exactly the shape that cost the monolith arm 137 scenarios. Under a
// transport the conjunct is already implied by `pushedStorage != nullptr`, so
// this changes no behaviour and closes no door; it says the thing instead of
// depending on it.
const Bool texBufferByRecord = pushedStorage != nullptr && MGB_TEXTURE_HANDLE_ARM_OFF(*this) == false;
static const SharedPtr<MG_State::GLState::BufferObject> kNoFrontendBuffer{};
auto& buffer = texBufferByRecord ? kNoFrontendBuffer
: textureBufferObject->GetBufferBindingSlot().GetBoundObject();
if (!buffer && !texBufferByRecord) {
MGLOG_D("Texture buffer object with ID: %u has no bound buffer, skipping sync.",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
return;
}
auto bufferIndex = buffer ? buffer->GetExternalIndex() : 0u;
// The memo key is the HANDLE's slot on the handle arm: a GL name is never an
// identity (section 4.2.1), and the record's Serial covers a slot recycled at a
// new generation because a respecify moves it.
currentTextureInfo.bufferExternalIndex =
pushedStorage != nullptr ? static_cast<Uint>(pushedTexBuffer.Slot) : bufferIndex;
Bool needsRegeneration = !m_isInitialized || (currentTextureInfo != m_prevTextureInfo);
// Need to sync texture buffer if not synced yet
auto* backendBufferResource = BufferImpl::EnsureBufferResourceForHandle(buffer, pushedTexBuffer);
if (!backendBufferResource || backendBufferResource->id == 0) {
MGLOG_E_ONCE("Failed to sync backing buffer for texture buffer with ID: %u",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
return;
}
// Bind buffer to texture
auto backendId = backendBufferResource->id;
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureBufferObject), &glInternalFormat,
&glFormat, &glType, TextureTarget::TextureBuffer);
// The view half of the buffer-image SPLIT. A buffer texture has no storage of its
// own to widen, but the VIEW its format describes can be re-described one
// component at a time over the same bytes - rg32f over N texels is r32f over 2N -
// and WidenImageFormatsPass rewrites every access to subscript it that way. Only
// for a texture that is actually image-bound: a sampled-only buffer texture keeps
// the format the application asked for (see GetImageBindableBufferSplitFormat).
//
// The split goes on a SEPARATE name (m_bufferImageSplitViewId), not on this one.
// Re-describing the application's own texture also re-describes what a
// samplerBuffer reading it sees, and the sampler side is not subscript-rewritten -
// so texelFetch(s, i) started returning component 2i of the base view instead of
// texel i. rg32f is a legal SAMPLED buffer-texture format in ES 3.2; only the
// IMAGE binding needs the split, so only the image binding's name carries it.
const GLenum bufferImageSplitFormat =
m_imageBindableStorageRequired
? TextureImpl::GetImageBindableBufferSplitFormat(MGB_STORAGE_FORMAT(textureBufferObject))
: GL_UNKNOWN_MGL;
if (needsRegeneration) {
// Desktop GL has had buffer textures core since 3.1 and MobileGL advertises a
// 4.x context, so glTexBuffer is a legal call the app may make on any driver -
// but ES only gained them in 3.2, and g_GLESFuncs.glTexBuffer is simply null
// below that without EXT/OES_texture_buffer. Calling it was an unconditional
// null dereference. There is no conformant way to refuse the call (it is valid
// in the context MobileGL claims), so the texture is left unbacked and the
// reason is stated once per object, latched by the flag below. It was parked
// at MGLOG_I while the level ordering compiled MGLOG_W out of INFO builds;
// W is the correct level and now survives there.
if (!AreBufferTexturesSupported()) {
if (m_bufferTextureUnsupportedReported) {
break;
}
m_bufferTextureUnsupportedReported = true;
MGLOG_W("Texture buffer %u cannot be backed: this ES driver has no buffer "
"textures (%s). Every draw sampling it will read zero and every "
"shader declaring a samplerBuffer will fail to compile. MobileGL "
"still advertises GL_MAX_TEXTURE_BUFFER_SIZE = %d because an "
"OpenGL 4.x context may not report 0.",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), GetBufferTextureTierName(),
g_GLESCapabilities.MaxTextureBufferSize);
break;
}
MGLOG_D("Texture state changed significantly or not initialized, regenerating texture buffer with "
"ID: %u, buffer ID: %u, buffer size: %zu, format: %s",
m_backendTextureId, backendId, buffer ? buffer->GetSize() : rangeSize,
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
// A texture that names a window of the buffer needs the range form; the
// whole-buffer forms report offset 0 and the buffer's current size, which
// glTexBuffer expresses more directly (and works where the range entry point
// is absent).
// The WINDOW is carried: MGPResourceDesc::BufOffset / BufSize, with
// kMGPipeWholeBuffer (~0) meaning "the whole store, resolved live" - which is
// why the whole-buffer arm still asks the BUFFER for its size. That is the
// buffer family's own object (P3a's, not this record's), and resolving it
// here is exactly what the sentinel is documented to mean.
const SizeT rangeOffset = pushedStorage != nullptr
? static_cast<SizeT>(pushedStorage->Desc.BufOffset)
: textureBufferObject->GetBufferRangeOffset();
// P5e (tx2): "the whole store" is the SENTINEL on the handle arm, not a size
// compare against a frontend BufferObject this arm may not name. The monolith
// arm keeps resolving the sentinel live, exactly as its comment says.
const Bool wholeBufferRange =
pushedStorage != nullptr
? (pushedStorage->Desc.BufOffset == 0 &&
pushedStorage->Desc.BufSize == MG_Pipe::kMGPipeWholeBuffer)
: (textureBufferObject->GetBufferRangeOffset() == 0 &&
textureBufferObject->GetBufferRangeSizeInBytes() == buffer->GetSize());
const SizeT rangeSize = pushedStorage == nullptr
? textureBufferObject->GetBufferRangeSizeInBytes()
: (pushedStorage->Desc.BufSize == MG_Pipe::kMGPipeWholeBuffer
? (buffer ? buffer->GetSize() : 0u)
: static_cast<SizeT>(pushedStorage->Desc.BufSize));
// Through CallTexBuffer/CallTexBufferRange rather than g_GLESFuncs directly:
// the unsuffixed entry points are the ES 3.2 core spelling, and a driver
// whose buffer textures come from EXT/OES_texture_buffer exports the
// suffixed ones instead. The dispatchers pick whichever this tier ships.
if (wholeBufferRange) {
CallTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId);
} else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, glInternalFormat, backendId,
static_cast<GLintptr>(rangeOffset),
static_cast<GLsizeiptr>(rangeSize))) {
MGLOG_W_ONCE("Texture buffer %u names a sub-range but the driver has no "
"glTexBufferRange; binding the whole buffer instead",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
CallTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId);
}
DebugImpl::ErrorLopper::Loop(
[file = __FILE__, line = __LINE__, func = __func__, glInternalFormat, backendId](GLenum err) {
MGLOG_D("%s(%s:%d) glTexBuffer(format=%s, buffer=%u) ES error: %s", func, file, line,
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(), backendId,
MG_Util::ConvertGLEnumToString(err).c_str());
});
// The image half of the SPLIT, on its own name over the same buffer. Minted
// lazily - only a texture that is both image-bound AND holds a format with no
// ESSL image spelling ever gets one - and re-pointed here, in the same
// regeneration gate as the view above, so the two never describe different
// buffers or different windows of one.
if (bufferImageSplitFormat != GL_UNKNOWN_MGL) {
if (m_bufferImageSplitViewId == 0) {
g_GLESFuncs.glGenTextures(1, &m_bufferImageSplitViewId);
}
if (m_bufferImageSplitViewId == 0) {
MGLOG_E_ONCE("Failed to generate the buffer-image split view for texture %u; "
"its image binding will read the unsplit view.",
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
} else {
g_GLESFuncs.glBindTexture(GL_TEXTURE_BUFFER, m_bufferImageSplitViewId);
if (wholeBufferRange) {
CallTexBuffer(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId);
} else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId,
static_cast<GLintptr>(rangeOffset),
static_cast<GLsizeiptr>(rangeSize))) {
CallTexBuffer(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId);
}
// The raw bind above went behind Bind()'s shadow, which tracks objects
// rather than names: leaving it claiming THIS object is bound would
// make the next Bind(GL_TEXTURE_BUFFER) a no-op and leave the split
// view bound in the application texture's place.
g_boundTexturesCache[g_activeTextureUnit]
[static_cast<SizeT>(TextureTarget::TextureBuffer)] = nullptr;
}
}
}
break;
}
default:
// TextureStorageType is {Mipmap, Buffer}, both handled above, so this is a
// backstop for a state object that grew a new storage kind. Skipping the upload
// renders wrong; throwing unwinds through the C GL ABI and kills the process.
MGLOG_E_ONCE("DirectGLES texture sync: no upload path for storage type %d on texture %u; "
"skipping this sync",
static_cast<int>(MGB_TEXTURE_STORAGE_KIND(pushedStorage, stateTextureObject)),
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject));
break;
}
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
m_prevTextureInfo = currentTextureInfo;
if (pushedStorage != nullptr) {
// The handle arm's one stamp. Deliberately AFTER the switch, at the same
// instant the legacy arm stamps its four keys, so a bail arm that returned
// early leaves it untouched and the next sync re-runs - which is the property
// the pending set exists to make safe (D-D5).
//
// The record pointer was resolved before any driver work and TextureResources
// has not been grown since (nothing this function calls emits a create), so
// re-reading Serial through it is sound; the PendingUploads entries it named
// have been consumed one at a time by MGB_LEVEL_UPLOAD_DONE.
m_syncedResourceSerial = pushedStorage->Serial;
return;
}
// Everything dirty at entry is uploaded (or provably has no bytes to
// upload); stamp the version so per-draw re-syncs short-circuit until
// the next CPU-side mutation.
m_syncedContentVersion = stateTextureObject->GetContentVersion();
// Same instant, so the cheap gate's keys describe exactly this synced state.
// Only Mipmap storage may arm it - the gate refuses other storage types anyway,
// but a stale trio must not linger on an object that later switches type.
if (MG_Pipe::gPipeInputs->IsLive() && stateTextureObject->GetStorageType() == TextureStorageType::Mipmap) {
m_syncedShapeContextId = MG_Pipe::gPipeInputs->GetTextureContextId();
m_syncedShapeGeneration = MG_Pipe::gPipeInputs->GetSamplingResolutionGeneration();
m_syncedShapeParamsVersion = stateTextureObject->GetTextureParamsVersion();
} else {
m_syncedShapeContextId = 0;
}
}
#undef MGB_LEVEL_NEEDS_UPLOAD
#undef MGB_LEVEL_UPLOAD_DONE
#undef MGB_STORAGE_FORMAT
#undef MGB_STORAGE_BASE_SIZE
#undef MGB_STORAGE_LEVELS
#undef MGB_STORAGE_SAMPLES
#undef MGB_STORAGE_FIXED_SAMPLE_LOCATIONS
#undef MGB_STORAGE_IMMUTABLE
#undef MGB_STORAGE_KIND
#undef MGB_TEXTURE_TARGET
#undef MGB_UPLOAD_TARGETS
#undef MGB_LEVEL_TEXEL_SIZE
#undef MGB_LEVEL_BYTE_SIZE
#undef MGB_LEVEL_TEXELS
#undef MGB_STAGED_TEXTURE_LIVE
// P5e (tx2), CONTRACT-P5E §5.2: THIS TWIN'S OWN RECORD, by the handle the caller noted on
// the twin before it resolved it; the client allocator is never probed.
const MG_Pipe::MGPipeResourceRecord* BackendTextureObject::ResolveOwnRecord(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) const {
(void)stateTextureObject;
// P13: a twin is only ever reached by handle, and the handle is noted on it first.
if (MG_Pipe::MGPipeHandleIsNull(m_pushedSyncHandle)) return nullptr;
return PipeTextureRecordForHandle(m_pushedSyncHandle);
}
} // namespace TextureImpl
} // namespace MobileGL::MG_Backend::DirectGLES