[Fix] (DirectVulkan): rewrite implicit-LOD fragment samples to explicit LOD 0 when every bound sampler is pinned to a single mip level - Adreno 650 (driver 512.502) reads outside a full-screen colour render target's allocation on its implicit-LOD sampling path and faults the GPU, which killed MC 26.2 on its own blit shader (texture(InSampler, texCoord)) between frames 344-421 on every run; this is the same driver defect the default-framebuffer blit shader already works around with textureLod, but an application's shader cannot be edited, so ForceExplicitLod0SamplePass converts OpImageSample*ImplicitLod to the explicit form at the SPIR-V level under a new CompileOptionBit that is only requested when the rewrite provably cannot move a texel (every sampler binding on a single-level view, no anisotropy, and either a LOD clamp that already pins lambda at 0 or min and mag filters that agree - an explicit LOD 0 always takes the magnification side of the min/mag decision); a single-level view now also clamps its sampler to mipmapMode NEAREST with maxLod min(maxLod, 0.25) rather than 0, since collapsing the clamp would make every fragment magnify and quietly retire the min filter; and the program's backend hash memo grows from one slot to four so a program resolved under two compile-flag sets in the same frame stops re-hashing every stage's SPIR-V once per draw

This commit is contained in:
2026-07-29 03:26:57 -04:00
parent 0ea9e6de5f
commit 992d16267c
8 changed files with 356 additions and 25 deletions
@@ -923,6 +923,206 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ProgramFactory::CompileOptionFlags m_transformFlags;
};
// Adreno 650 (driver 512.502) faults the GPU on an implicit-LOD sample of a full-screen
// colour render target: the texture unit's derivative path reads outside the image's
// allocation even though the sampler clamps LOD to 0 and the mapping is 1:1. MobileGL's
// own default-framebuffer blit shader works around it with textureLod, but an
// application's shader (Minecraft's blit.fsh is `texture(InSampler, texCoord)`) cannot be
// edited - so rewrite the sample at the SPIR-V level instead.
//
// The rewrite is only requested for draws whose every sampler binding is clamped to one
// mip level, where explicit LOD 0 is exactly what the implicit form must already produce:
// lambda' = clamp(lambda + bias, minLod, maxLod) with minLod = maxLod = 0. Bias and MinLod
// operands are therefore dropped rather than translated.
class ForceExplicitLod0SamplePass final : public spvtools::opt::Pass {
public:
const char* name() const override { return "force-explicit-lod0-sample"; }
Status Process() override {
Bool isFragment = false;
for (auto& entryPoint : get_module()->entry_points()) {
if (entryPoint.opcode() != spv::Op::OpEntryPoint) continue;
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) ==
spv::ExecutionModel::Fragment) {
isFragment = true;
break;
}
}
if (!isFragment) return Status::SuccessWithoutChange;
// Plan first, mutate second. Materializing the LOD constant is itself a module
// change, so it must not happen unless at least one rewrite is going to follow -
// otherwise the pass would grow the binary while reporting SuccessWithoutChange.
Vector<RewritePlan> plans;
for (auto& function : *get_module()) {
for (auto& block : function) {
for (auto& inst : block) {
RewritePlan plan{};
if (PlanRewrite(&inst, plan)) plans.push_back(Move(plan));
}
}
}
if (plans.empty()) return Status::SuccessWithoutChange;
const Uint32 zeroId = GetFloatZeroId();
if (zeroId == 0) return Status::SuccessWithoutChange;
for (auto& plan : plans) {
plan.operands.push_back({SPV_OPERAND_TYPE_ID, {zeroId}});
for (auto& operand : plan.trailingOperands) {
plan.operands.push_back(operand);
}
plan.instruction->SetOpcode(plan.opcode);
plan.instruction->SetInOperands(Move(plan.operands));
}
// Opcodes and operand lists changed underneath every cached analysis.
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
private:
struct RewritePlan {
spvtools::opt::Instruction* instruction = nullptr;
spv::Op opcode = spv::Op::OpNop;
// Everything up to and including the Image Operands mask; the Lod id and the
// trailing operand values are appended once the constant exists.
Vector<spvtools::opt::Operand> operands;
Vector<spvtools::opt::Operand> trailingOperands;
};
// Image Operands bits that may accompany an implicit-LOD sample, in the canonical
// ascending order SPIR-V requires the operand values to appear in.
static constexpr Uint32 kBias = 0x1;
static constexpr Uint32 kLod = 0x2;
static constexpr Uint32 kGrad = 0x4;
static constexpr Uint32 kConstOffset = 0x8;
static constexpr Uint32 kOffset = 0x10;
static constexpr Uint32 kConstOffsets = 0x20;
static constexpr Uint32 kSample = 0x40;
static constexpr Uint32 kMinLod = 0x80;
static constexpr Uint32 kKnownMask = 0xFF;
Uint32 GetFloatZeroId() {
// Reuse a 32-bit float type already in the module; a shader that samples always has
// one, and looking it up avoids depending on type-creation API details.
Uint32 floatTypeId = 0;
for (auto& inst : get_module()->types_values()) {
if (inst.opcode() == spv::Op::OpTypeFloat && inst.NumInOperands() >= 1 &&
inst.GetSingleWordInOperand(0) == 32) {
floatTypeId = inst.result_id();
break;
}
}
if (floatTypeId == 0) return 0;
const auto* floatType = context()->get_type_mgr()->GetType(floatTypeId);
if (floatType == nullptr) return 0;
const auto zeroBits = std::bit_cast<Uint32>(0.0f);
const auto* zeroConst = context()->get_constant_mgr()->GetConstant(floatType, {zeroBits});
if (zeroConst == nullptr) return 0;
auto* zeroInst = context()->get_constant_mgr()->GetDefiningInstruction(zeroConst);
return zeroInst != nullptr ? zeroInst->result_id() : 0;
}
static Bool MapOpcode(spv::Op op, spv::Op& outOpcode, Uint32& outFixedOperandCount) {
switch (op) {
case spv::Op::OpImageSampleImplicitLod:
outOpcode = spv::Op::OpImageSampleExplicitLod;
outFixedOperandCount = 2; // sampled image, coordinate
return true;
case spv::Op::OpImageSampleProjImplicitLod:
outOpcode = spv::Op::OpImageSampleProjExplicitLod;
outFixedOperandCount = 2;
return true;
case spv::Op::OpImageSampleDrefImplicitLod:
outOpcode = spv::Op::OpImageSampleDrefExplicitLod;
outFixedOperandCount = 3; // sampled image, coordinate, Dref
return true;
case spv::Op::OpImageSampleProjDrefImplicitLod:
outOpcode = spv::Op::OpImageSampleProjDrefExplicitLod;
outFixedOperandCount = 3;
return true;
default:
return false;
}
}
static Bool PlanRewrite(spvtools::opt::Instruction* inst, RewritePlan& outPlan) {
spv::Op newOpcode = spv::Op::OpNop;
Uint32 fixedCount = 0;
if (!MapOpcode(inst->opcode(), newOpcode, fixedCount)) return false;
if (inst->NumInOperands() < fixedCount) return false;
Uint32 mask = 0;
Uint32 next = fixedCount;
if (inst->NumInOperands() > fixedCount) {
mask = inst->GetSingleWordInOperand(fixedCount);
next = fixedCount + 1;
}
// An operand this pass does not model would be silently reordered or dropped, and
// Grad cannot legally accompany an implicit-LOD sample: leave such an instruction be.
if ((mask & ~kKnownMask) != 0 || (mask & kGrad) != 0) return false;
Vector<spvtools::opt::Operand> fixedOperands;
fixedOperands.reserve(fixedCount + 1);
for (Uint32 i = 0; i < fixedCount; ++i) {
fixedOperands.push_back(inst->GetInOperand(i));
}
// Collect the surviving operand values in the same ascending-bit order they were
// encoded in, so the rebuilt list stays canonical.
Uint32 keptMask = kLod;
Vector<spvtools::opt::Operand> keptOperands;
static constexpr Uint32 kOrderedBits[] = {kBias, kLod, kGrad, kConstOffset,
kOffset, kConstOffsets, kSample, kMinLod};
for (const Uint32 bit : kOrderedBits) {
if ((mask & bit) == 0) continue;
if (next >= inst->NumInOperands()) return false;
const spvtools::opt::Operand value = inst->GetInOperand(next++);
// Bias and MinLod only shift a lambda that is already clamped to 0, and any
// original Lod is replaced by the constant the caller appends.
if (bit == kBias || bit == kMinLod || bit == kLod) continue;
keptMask |= bit;
keptOperands.push_back(value);
}
fixedOperands.push_back({SPV_OPERAND_TYPE_IMAGE, {keptMask}});
outPlan.instruction = inst;
outPlan.opcode = newOpcode;
outPlan.operands = Move(fixedOperands);
outPlan.trailingOperands = Move(keptOperands);
return true;
}
};
spvtools::Optimizer::PassToken CreateForceExplicitLod0SamplePass() {
return spvtools::Optimizer::PassToken(MakeUnique<ForceExplicitLod0SamplePass>());
}
Bool TransformSpirvForExplicitLod0Sampling(const Vector<Uint>& input, Vector<Uint>& output) {
if (input.empty()) {
output.clear();
return true;
}
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
spvtools::OptimizerOptions options;
// Matches the position-fix pass: this build of spirv-tools asserts rather than
// reporting, so validation stays off in the shipping path.
options.set_run_validator(false);
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
const char* message) {
MGLOG_E("Vulkan: explicit-LOD0 pass: %s", message != nullptr ? message : "");
});
optimizer.RegisterPass(CreateForceExplicitLod0SamplePass());
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
if (!success) {
MGLOG_E("Vulkan: explicit-LOD0 sampling pass failed; keeping the original module");
output = input;
}
return success;
}
spvtools::Optimizer::PassToken CreateGlToVulkanPositionFixPass(
ProgramFactory::CompileOptionFlags transformFlags) {
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
@@ -1978,6 +2178,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
moduleSpirvs[i] = spv;
}
if ((flags & ProgramFactory::CompileOptionBit::ExplicitLod0Sampling) && shaders[i] &&
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
Vector<Uint> explicitLodSpirv;
if (TransformSpirvForExplicitLod0Sampling(moduleSpirvs[i], explicitLodSpirv)) {
moduleSpirvs[i] = Move(explicitLodSpirv);
}
}
// GL apps depend on cross-program position invariance for multi-pass equality
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
// depth its own first pass wrote); decorate Position outputs Invariant so
@@ -42,6 +42,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SurfaceRotate90 = 1 << 2,
SurfaceRotate180 = 1 << 3,
SurfaceRotate270 = 1 << 4,
// Rewrites the fragment stage's implicit-LOD image samples to explicit LOD 0.
// Only ever set for a draw whose every sampler binding is clamped to a single mip
// level, which makes the two forms produce identical texels (the implicit lambda is
// clamped into [minLod, maxLod] = [0, 0] regardless of derivatives or bias).
ExplicitLod0Sampling = 1 << 5,
};
using CompileOptionFlags = Flags<CompileOptionBit>;
using HashType = Uint64;
@@ -384,24 +384,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Uint16 samplerVersion = samplerToUse->GetVersion();
const Uint64 textureLifetimeId = texture->GetLifetimeId();
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion();
// The sampler's LOD clamp depends on how many levels the sampled view exposes, and that
// follows uploads as well as GL parameters - so it belongs in the memo key too.
const Uint32 viewLevelCount = resource->sampledLevelCount;
if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion &&
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion &&
memo.forceNearestFiltering == forceNearestFiltering) {
memo.forceNearestFiltering == forceNearestFiltering && memo.viewLevelCount == viewLevelCount) {
resolvedSampler = memo.sampler;
} else {
resolvedSampler =
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture,
forceNearestFiltering, viewLevelCount);
memo.samplerLifetimeId = samplerLifetimeId;
memo.samplerVersion = samplerVersion;
memo.textureLifetimeId = textureLifetimeId;
memo.textureParamsVersion = textureParamsVersion;
memo.forceNearestFiltering = forceNearestFiltering;
memo.viewLevelCount = viewLevelCount;
memo.sampler = resolvedSampler;
memo.valid = true;
}
} else {
resolvedSampler =
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering,
resource->sampledLevelCount);
}
outImageInfo = {
.sampler = resolvedSampler,
@@ -442,6 +446,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return outImageInfo.sampler != VK_NULL_HANDLE;
}
Bool UniformManager::ProgramSamplesOnlySingleLevelTextures(
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) {
Bool sawSampler = false;
for (Uint32 binding = 0; binding < programObj.bindingKinds.size(); ++binding) {
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
continue;
}
const auto* texture = ResolveSamplerTextureRaw(program, programObj, binding);
if (texture == nullptr) return false;
const auto& levelRange = texture->GetLevelRange();
if (levelRange.x() != levelRange.y()) return false;
// An explicit-LOD sample is a single filtered tap, so it also gives up anisotropic
// filtering - which a single-level view can still have. Resolve the sampler exactly
// the way ResolveSamplerDescriptor does and bail if anisotropy would apply.
const Int location = programObj.samplerUniformLocationByBinding[binding];
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
const auto& samplerOverride = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
const auto* effectiveSampler =
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
if (effectiveSampler == nullptr) return false;
if (effectiveSampler->GetMaxAnisotropy() > 1.0f &&
effectiveSampler->GetMinFilter() == SamplerFilterMode::Linear &&
effectiveSampler->GetMagFilter() == SamplerFilterMode::Linear) {
return false;
}
// An explicit LOD 0 makes lambda exactly 0, which is the magnification side of the
// min/mag decision. That only matches the implicit form when lambda could not have been
// positive anyway (the LOD clamp already pins it at or below 0), or when the two
// filters are the same and the choice cannot be observed.
const Float effectiveMaxLod = effectiveSampler->GetMipmapMode() == SamplerMipmapMode::None
? 0.0f
: effectiveSampler->GetMaxLod();
if (effectiveMaxLod > 0.0f && effectiveSampler->GetMinFilter() != effectiveSampler->GetMagFilter()) {
return false;
}
sawSampler = true;
}
return sawSampler;
}
Bool UniformManager::ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
@@ -69,6 +69,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
VkFormat resourceFormat, Bool useBindingFormat);
// True when the program reads at least one sampler and every one of them is bound to a
// texture whose GL level range is a single level. Such a sampler resolves to
// minLod = maxLod = 0 (see VkSamplerManager::GetOrCreateSampler), so an implicit-LOD sample
// and an explicit LOD 0 sample must read the same texel - which is what makes the
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
// only GL state, so a texture that ends up single-level for another reason (one uploaded
// level under a wide level range) merely misses the rewrite.
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
private:
struct DescriptorPoolBucket {
VkDescriptorPool handle = VK_NULL_HANDLE;
@@ -191,6 +201,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 samplerLifetimeId = 0;
Uint64 textureLifetimeId = 0;
VkSampler sampler = VK_NULL_HANDLE;
Uint32 viewLevelCount = 0;
Uint16 samplerVersion = 0;
Uint16 textureParamsVersion = 0;
Bool forceNearestFiltering = false;
@@ -51,6 +51,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
return std::min(sampler.GetMinLod(), effectiveMaxLod);
}
// A single-level view can only ever deliver the base level, but the LOD clamp must not be
// collapsed to exactly 0: both GL and Vulkan pick magFilter over minFilter from the
// *clamped* lambda, so maxLod = 0 would make every fragment magnify and quietly retire the
// min filter. 0.25 is the value VkSamplerCreateInfo's own note prescribes for emulating
// GL's non-mipmapped minification - large enough for lambda to stay positive, small enough
// that a NEAREST mip mode still rounds down to level 0. Clamped rather than assigned, so a
// texture whose GL_TEXTURE_MAX_LOD really is 0 keeps magnifying as GL says it must.
Float ResolveSingleLevelMaxLod(const MG_State::GLState::SamplerObject& sampler, Bool singleLevelView) {
const Float maxLod = ResolveEffectiveMaxLod(sampler);
return singleLevelView ? std::min(maxLod, 0.25f) : maxLod;
}
} // namespace
Bool VkSamplerManager::Initialize(const InitInfo& initInfo) {
@@ -120,11 +132,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) const {
Bool forceNearestFiltering, Bool singleLevelView) const {
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView)));
const auto minFilter = sampler.GetMinFilter();
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
@@ -138,7 +151,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
const auto wrapR = sampler.GetWrapR();
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
const auto maxLod = ResolveEffectiveMaxLod(sampler);
const auto maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
@@ -160,8 +173,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) {
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering);
Bool forceNearestFiltering, Uint32 viewLevelCount) {
// A view that exposes a single mip level has no second level to blend with, so GL's
// *_MIPMAP_* minification filters degenerate to plain filtering on the base level -
// sampling is unchanged by pinning the Vulkan sampler to NEAREST mip mode at LOD 0.
// It is not cosmetic: MobileGL backs such a view with a fully allocated mip chain whose
// tail is never written, and a LINEAR mip mode lets the texture unit issue the level+1
// fetch anyway. On Adreno that fetch lands in uninitialized UBWC pages (or past the
// allocation for a genuinely single-level image) and faults the GPU - the same failure
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
const Bool singleLevelView = viewLevelCount == 1;
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering, singleLevelView);
auto it = m_samplers.find(key);
if (it != m_samplers.end()) {
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
@@ -172,8 +194,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter());
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter());
samplerInfo.mipmapMode = forceNearestFiltering ? VK_SAMPLER_MIPMAP_MODE_NEAREST
: ToVkMipmapMode(sampler.GetMipmapMode());
samplerInfo.mipmapMode = (forceNearestFiltering || singleLevelView)
? VK_SAMPLER_MIPMAP_MODE_NEAREST
: ToVkMipmapMode(sampler.GetMipmapMode());
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
@@ -185,7 +208,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
samplerInfo.maxAnisotropy = maxAnisotropy;
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
samplerInfo.maxLod = ResolveEffectiveMaxLod(sampler);
// Must match BuildSamplerKey's resolution exactly.
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
samplerInfo.unnormalizedCoordinates = VK_FALSE;
@@ -33,9 +33,12 @@ public:
Bool Initialize(const InitInfo& initInfo);
void Shutdown();
// viewLevelCount is the mip-level count of the image view this sampler will be paired
// with; 0 means "unknown, do not narrow". See GetOrCreateSampler for why it matters.
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering = false);
Bool forceNearestFiltering = false,
Uint32 viewLevelCount = 0);
// Frame boundary hook: ages the sampler cache and destroys samplers not used
// for many frames. The key hashes continuous float state (lodBias, LOD clamps,
// anisotropy), so an app animating those would otherwise mint an unbounded
@@ -61,7 +64,7 @@ private:
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) const;
Bool forceNearestFiltering, Bool singleLevelView) const;
static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
@@ -4267,7 +4267,16 @@ void main() {
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
const auto& program = *MG_State::pGLContext->GetCurrentProgram();
ProgramFactory::CompileOptionFlags transformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform());
const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags);
const auto* programObjPtr = &m_programFactory->GetOrCreateProgram(program, transformFlags);
// Sampling a colour render target through the driver's implicit-LOD path faults the GPU on
// Adreno 650 (see ForceExplicitLod0SamplePass); ask for the explicit-LOD variant when doing
// so cannot change a texel, i.e. when every sampler this program reads is pinned to a
// single mip level.
if (UniformManager::ProgramSamplesOnlySingleLevelTextures(program, *programObjPtr)) {
transformFlags |= ProgramFactory::CompileOptionBit::ExplicitLod0Sampling;
programObjPtr = &m_programFactory->GetOrCreateProgram(program, transformFlags);
}
const auto& programObj = *programObjPtr;
// Begin command recording if not yet
if (!frame.isCommandRecording) {
@@ -334,16 +334,32 @@ namespace MobileGL::MG_State::GLState {
// draw. The memo is keyed by (backendStateVersion, flags); ResetLinkArtifacts and
// the binding setters below invalidate it by bumping m_backendStateVersion.
Bool GetBackendHashMemo(Uint flags, Uint64& outHash) const {
if (m_backendHashMemoVersion != m_backendStateVersion || m_backendHashMemoFlags != flags) {
return false;
if (m_backendHashMemoVersion != m_backendStateVersion) return false;
for (const auto& slot : m_backendHashMemoSlots) {
if (slot.valid && slot.flags == flags) {
outHash = slot.hash;
return true;
}
}
outHash = m_backendHashMemo;
return true;
return false;
}
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
m_backendHashMemo = hash;
m_backendHashMemoVersion = m_backendStateVersion;
m_backendHashMemoFlags = flags;
if (m_backendHashMemoVersion != m_backendStateVersion) {
for (auto& slot : m_backendHashMemoSlots) slot.valid = false;
m_backendHashMemoVersion = m_backendStateVersion;
m_backendHashMemoNextSlot = 0;
}
for (auto& slot : m_backendHashMemoSlots) {
if (slot.valid && slot.flags == flags) {
slot.hash = hash;
return;
}
}
auto& slot = m_backendHashMemoSlots[m_backendHashMemoNextSlot];
slot.flags = flags;
slot.hash = hash;
slot.valid = true;
m_backendHashMemoNextSlot = (m_backendHashMemoNextSlot + 1) % kBackendHashMemoSlotCount;
}
void SetUniformSamplerOrImageUnitIndex(Uint location, Int unit) {
@@ -527,10 +543,19 @@ namespace MobileGL::MG_State::GLState {
Uint32 m_backendStateVersion = 0;
// Backend-owned content-hash memo (see GetBackendHashMemo): valid only while
// m_backendStateVersion and the compile flags match the recorded values.
mutable Uint64 m_backendHashMemo = 0;
// m_backendStateVersion matches. Several slots, not one: a backend may resolve the same
// program under more than one compile-flag set within a frame (surface rotation, and the
// explicit-LOD sampling variant), and a single slot would then miss on every lookup and
// re-hash the program's whole SPIR-V once per draw.
static constexpr SizeT kBackendHashMemoSlotCount = 4;
struct BackendHashMemoSlot {
Uint64 hash = 0;
Uint flags = 0;
Bool valid = false;
};
mutable Array<BackendHashMemoSlot, kBackendHashMemoSlotCount> m_backendHashMemoSlots{};
mutable SizeT m_backendHashMemoNextSlot = 0;
mutable Uint32 m_backendHashMemoVersion = ~0u;
mutable Uint m_backendHashMemoFlags = 0;
Uint32 m_uboContentVersion = 0;
Uint32 m_linkVersion = 0;
};