[Fix] (Espryt, Magma): a partial write into a level whose upload is converted (widened RGB formats, Magma's RGB->RGBA expansion) or reshaped (Espryt 1D arrays) uploads only its dirty region - the rest of the stale shadow no longer lands over GPU-drawn texels

This commit is contained in:
2026-09-29 04:28:22 -04:00
parent 568090f09f
commit 8966d5c294
2 changed files with 82 additions and 52 deletions
+62 -36
View File
@@ -3090,7 +3090,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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 shadow
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)
@@ -4376,33 +4376,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<Uint8> packedUploadData;
uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), texelSize,
uploadData, byteSize, &glType, packedUploadData);
// Leaves `uploadData` pointing at its own buffer when it fires, which
// is exactly what takes the sub-rect fast path below out of play: that
// path strides into the SHADOW, and the widened texels are four
// components wide where the shadow's are one or two.
// 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(stateTextureObject->GetTarget(), texelSize);
// Sub-rect upload: when only a region of the level changed (a
// 16x16 sprite in a 1024x512 atlas, the per-frame lightmap) and
// the shadow bytes go to the driver unconverted, upload just that
// region with UNPACK_ROW_LENGTH striding into the level shadow.
// Conversion fallbacks rewrite the whole level into a fresh
// buffer, so they stay on the full-level path, as do targets
// whose backend upload size differs from the shadow's texel size.
// 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.
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.
const Bool layersAsRows = stateTextureObject->GetTarget() == TextureTarget::Texture1DArray;
const auto dirtyRegion = textureMipmapObject->GetStorageDirtyRegion(uploadTarget, level);
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
static_cast<SizeT>(texelSize.y()) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
const Bool subRectEligible =
uploadData == mipData && !dirtyRegion.Empty() &&
!dirtyRegion.CoversWholeLevel(texelSize) && texelCount > 0 &&
byteSize % texelCount == 0 && uploadSize.x() == texelSize.x() &&
uploadSize.y() == texelSize.y() &&
std::max(uploadSize.z(), 1) == std::max(texelSize.z(), 1);
const SizeT bpp = subRectEligible ? byteSize / texelCount : 0;
!dirtyRegion.Empty() && !dirtyRegion.CoversWholeLevel(texelSize) && 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 SizeT bpp = subRectEligible ? uploadBytes / texelCount : 0;
const IntVec3 regionSize = {dirtyRegion.hi.x() - dirtyRegion.lo.x(),
dirtyRegion.hi.y() - dirtyRegion.lo.y(),
dirtyRegion.hi.z() - dirtyRegion.lo.z()};
@@ -4418,7 +4435,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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 shadow as the box
// 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.
@@ -4439,12 +4456,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
dirtyRectCount = 0;
}
}
const auto rectShadowPtr = [&](const MG_State::GLState::MipmapDirtyRegion& rect) {
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;
};
// 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
@@ -4469,7 +4501,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (SizeT r = 0; r < dirtyRectCount; ++r) {
const auto& rect = dirtyRects[r];
stagingBlocks[r] = {
rectShadowPtr(rect),
rectUploadPtr(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)),
@@ -4528,7 +4560,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<GLsizei>(rect.hi.y() - rect.lo.y()), glFormat,
glType,
ringStaged ? UnpackRingPixelOffset(stagingBlocks[r].offset)
: static_cast<const void*>(rectShadowPtr(rect)));
: static_cast<const void*>(rectUploadPtr(rect)));
}
// The surrounding ScopedDefaultUnpackState shadow says 0.
if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
@@ -4560,19 +4592,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (subRectEligible && dirtyRectCount >= 2) {
if (!ringStaged) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, texelSize.y());
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight);
}
for (SizeT r = 0; r < dirtyRectCount; ++r) {
const auto& rect = dirtyRects[r];
const auto box = ToBackendBox(dirtyRects[r]);
g_GLESFuncs.glTexSubImage3D(
glUploadTarget, static_cast<GLint>(level), rect.lo.x(),
rect.lo.y(), rect.lo.z(),
static_cast<GLsizei>(rect.hi.x() - rect.lo.x()),
static_cast<GLsizei>(rect.hi.y() - rect.lo.y()),
static_cast<GLsizei>(rect.hi.z() - rect.lo.z()), glFormat,
glType,
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*>(rectShadowPtr(rect)));
: static_cast<const void*>(rectUploadPtr(dirtyRects[r])));
}
if (!ringStaged) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
@@ -4581,14 +4609,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
} else if (subRectEligible) {
if (!ringStaged) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, texelSize.y());
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight);
}
const auto box = ToBackendBox(dirtyRegion);
g_GLESFuncs.glTexSubImage3D(
glUploadTarget, static_cast<GLint>(level), dirtyRegion.lo.x(),
dirtyRegion.lo.y(), dirtyRegion.lo.z(),
static_cast<GLsizei>(regionSize.x()),
static_cast<GLsizei>(regionSize.y()),
static_cast<GLsizei>(regionSize.z()), glFormat, glType,
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) {
@@ -2835,9 +2835,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDeviceSize offset = 0;
// Sub-region upload (a small sprite in a big atlas): only the dirty box
// is staged and copied. texelSize keeps the LEVEL extent - the staging
// row copy needs it for the shadow's stride. Plain color formats only;
// the RGB-expand and depth(+stencil) conversion passes rewrite whole
// levels and stay full-size.
// row copy needs it for the stride. Color formats only (an RGB-expanded
// level is cut from its expanded bytes); the depth(+stencil) conversion
// passes rewrite whole levels and stay full-size.
Bool subRegion = false;
IntVec3 regionLo = {0, 0, 0};
IntVec3 regionSize = {0, 0, 0};
@@ -2900,19 +2900,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
uploadItem.source = source;
uploadItem.offset = stagingSize;
uploadItem.uploadByteSize = byteSize;
if (!formatInfo.expandRgbToRgba &&
GetAspectMaskForFormat(outResource.format) == VK_IMAGE_ASPECT_COLOR_BIT) {
// The RGB -> RGBA expansion rewrites the level texel for texel into a tight buffer
// of its own, so it runs first and the dirty region below is taken from whichever
// bytes are staged. Uploading an expanded level WHOLE put the shadow's stale texels
// over whatever the GPU had drawn into the rest of it.
if (formatInfo.expandRgbToRgba) {
const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo,
uploadItem.expandedData);
MOBILEGL_ASSERT(expanded,
"UploadDirtyMipLevels: failed to expand RGB textureId=%d target=%s level=%u to RGBA staging data",
mipmapTexture.GetExternalIndex(),
MG_Util::ConvertTextureUploadTargetToString(target).c_str(), level);
uploadItem.uploadByteSize = uploadItem.expandedData.size();
}
const SizeT levelBytes = uploadItem.uploadByteSize;
if (GetAspectMaskForFormat(outResource.format) == VK_IMAGE_ASPECT_COLOR_BIT) {
const auto region = mipmapTexture.GetStorageDirtyRegion(target, level);
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
static_cast<SizeT>(texelSize.y()) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (!region.Empty() && !region.CoversWholeLevel(texelSize) && texelCount > 0 &&
byteSize % texelCount == 0) {
levelBytes % texelCount == 0) {
uploadItem.subRegion = true;
uploadItem.regionLo = region.lo;
uploadItem.regionSize = {region.hi.x() - region.lo.x(), region.hi.y() - region.lo.y(),
region.hi.z() - region.lo.z()};
uploadItem.texelBytes = byteSize / texelCount;
uploadItem.texelBytes = levelBytes / texelCount;
uploadItem.uploadByteSize = static_cast<SizeT>(uploadItem.regionSize.x()) *
static_cast<SizeT>(uploadItem.regionSize.y()) *
static_cast<SizeT>(uploadItem.regionSize.z()) *
@@ -2953,15 +2966,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
}
if (formatInfo.expandRgbToRgba) {
const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo,
uploadItem.expandedData);
MOBILEGL_ASSERT(expanded,
"UploadDirtyMipLevels: failed to expand RGB textureId=%d target=%s level=%u to RGBA staging data",
mipmapTexture.GetExternalIndex(),
MG_Util::ConvertTextureUploadTargetToString(target).c_str(), level);
uploadItem.uploadByteSize = uploadItem.expandedData.size();
}
uploadItems.push_back(Move(uploadItem));
if (!uploadItems.back().expandedData.empty()) {
uploadItems.back().source = uploadItems.back().expandedData.data();