[Fix] (MG_State, DirectVulkan): apply the incomplete-texture rule on Magma too

The completeness rule itself is GL's, not a backend's, so it now reads as one question
both backends ask - SamplesAsIncompleteTexture(texture, effective sampler) - and each
answers in whatever way it already expresses "nothing is bound at this sampler".
DirectGLES leaves the native target unbound; Magma has a fallback texture for exactly
that case and now routes an incomplete texture to it.

The fallback's texel had never been written, so it read whatever its freshly allocated
storage held. GL is specific here: an incomplete texture - and a sampler with nothing
bound - reads (0, 0, 0, 1). It says so now, which is what makes
KHR-GL40.texture_gather.incomplete-texture-last-comp (it gathers the alpha) meaningful
rather than accidentally right.
This commit is contained in:
BZLZHH
2026-08-04 13:14:36 -04:00
parent 28c3cfc1d6
commit 44ee6b66b3
4 changed files with 27 additions and 3 deletions
@@ -1621,9 +1621,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& effectiveSampler = textureUnit.GetSamplerObject()
? textureUnit.GetSamplerObject()
: textureObject->GetSamplerObject();
const Bool mipmappedFilter =
effectiveSampler && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
if (!MG_State::GLState::IsMipmapCompleteForFilter(textureObject.get(), mipmappedFilter)) {
if (MG_State::GLState::SamplesAsIncompleteTexture(textureObject.get(),
effectiveSampler.get())) {
continue;
}
@@ -266,6 +266,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto& samplerOverride = textureUnit.GetSamplerObject();
const auto preferredTarget = programObj.samplerTextureTargetByBinding[binding];
SharedPtr<MG_State::GLState::ITextureObject> fallbackHolder;
// A texture that fails the completeness rules for the filter in effect reads
// (0, 0, 0, 1), which is exactly what the fallback texture holds - so it takes the
// same route as a sampler with nothing bound.
if (texture != nullptr &&
MG_State::GLState::SamplesAsIncompleteTexture(
texture, samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get())) {
texture = nullptr;
}
if (texture == nullptr) {
fallbackHolder = GetFallbackTexture(preferredTarget);
texture = fallbackHolder.get();
@@ -769,6 +777,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8);
fallbackTexture->AllocateStorage(TextureUploadTarget::Texture2D, 0,
{.texelSize = {1, 1, 1}, .byteSize = 4});
// (0, 0, 0, 1): what GL reads from a texture that is not complete, and the only
// sensible answer for a sampler with nothing bound.
static Uint8 kOpaqueBlackTexel[4] = {0, 0, 0, 255};
fallbackTexture->UpdateMipmapSubData(TextureUploadTarget::Texture2D, 0,
{kOpaqueBlackTexel, sizeof(kOpaqueBlackTexel)});
fallbackTexture->MarkStorageDirty(TextureUploadTarget::Texture2D, 0, true);
m_fallbackTexture2D = fallbackTexture;
}
@@ -348,6 +348,12 @@ namespace MobileGL {
// TODO: add other texture types as needed
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) {
const Bool mipmapped =
effectiveSampler != nullptr && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None;
return !IsMipmapCompleteForFilter(texture, mipmapped);
}
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) {
if (texture == nullptr) return true;
if (!texture->IsComplete()) return false;
@@ -164,6 +164,12 @@ namespace MobileGL::MG_State::GLState {
// IsComplete() already covers. Sampling an incomplete texture returns (0, 0, 0, 1).
Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped);
// The rule above asked as the backends need it: does a lookup on this texture read
// (0, 0, 0, 1) instead of its contents? `effectiveSampler` is the sampler object bound
// to the unit when there is one, otherwise the texture's own. A backend answers yes by
// routing the texture to whatever it already uses for "nothing is bound there".
Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler);
inline const TextureObjectMipmap* AsMipmapTexture(const ITextureObject* texture) {
return (texture && texture->GetStorageType() == TextureStorageType::Mipmap)
? static_cast<const TextureObjectMipmap*>(texture)