diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index b57de618..bece1681 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -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(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 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 operands; + Vector 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(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 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 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()); + } + + Bool TransformSpirvForExplicitLod0Sampling(const Vector& input, Vector& 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(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 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 diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h index 7b4cb290..1ae8e8c8 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h @@ -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; using HashType = Uint64; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 2c068bdb..d1aabb2d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -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& outTexture) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index 82187acf..3011b405 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -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; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp index b0eabcc1..b4127289 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp @@ -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; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h index 5ec83059..8cbedff0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.h @@ -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); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 62ddda8f..54a26963 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -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) { diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index 82bbfe3a..6a3cc360 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -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 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; };