mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc4cd980f2 | ||
|
|
992d16267c |
@@ -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);
|
||||
|
||||
@@ -587,6 +587,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_allocator = initInfo.allocator;
|
||||
m_commandPool = initInfo.commandPool;
|
||||
m_graphicsQueue = initInfo.graphicsQueue;
|
||||
m_imageFormatListSupported = initInfo.imageFormatListSupported;
|
||||
m_currentFrameIndex = 0;
|
||||
m_deferredReleases.clear();
|
||||
m_deferredReleases.resize(initInfo.frameCount);
|
||||
@@ -609,6 +610,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
DestroyDeferredReleases();
|
||||
m_textureResources.clear();
|
||||
m_aliveObjects.clear();
|
||||
m_storageImageTextures.clear();
|
||||
|
||||
m_device = VK_NULL_HANDLE;
|
||||
m_physicalDevice = VK_NULL_HANDLE;
|
||||
@@ -656,6 +658,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_textureResources.erase(resourceIt);
|
||||
}
|
||||
m_aliveObjects.erase(identity);
|
||||
m_storageImageTextures.erase(identity);
|
||||
}
|
||||
|
||||
void VkTextureManager::PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture) {
|
||||
@@ -1189,8 +1192,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ok;
|
||||
}
|
||||
|
||||
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
|
||||
m_storageImageTextures.insert(MakeTextureIdentity(&texture));
|
||||
}
|
||||
|
||||
Bool VkTextureManager::NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const {
|
||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||
if (m_storageImageTextures.find(identity) == m_storageImageTextures.end()) {
|
||||
return false;
|
||||
}
|
||||
const auto it = m_textureResources.find(identity);
|
||||
// No image yet: the first sync creates it with STORAGE straight away, so there is nothing
|
||||
// to preserve and nothing to order against.
|
||||
return it != m_textureResources.end() && it->second.image != VK_NULL_HANDLE &&
|
||||
!it->second.storageUsageResolved;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const {
|
||||
const auto it = m_textureResources.find(MakeTextureIdentity(&texture));
|
||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||
const auto it = m_textureResources.find(identity);
|
||||
if (it == m_textureResources.end()) {
|
||||
return true;
|
||||
}
|
||||
@@ -1198,6 +1218,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (resource.image == VK_NULL_HANDLE || resource.layout != VK_IMAGE_LAYOUT_GENERAL) {
|
||||
return true;
|
||||
}
|
||||
// The image predates this texture's first image-unit binding, so it was created without
|
||||
// STORAGE usage and has to be recreated - which is illegal inside a render pass.
|
||||
if (!resource.storageUsageResolved &&
|
||||
m_storageImageTextures.find(identity) != m_storageImageTextures.end()) {
|
||||
return true;
|
||||
}
|
||||
// Mirror SyncTexture's cross-draw skip condition: any version drift means the sync
|
||||
// path may upload or rebuild, both of which need the render pass ended first.
|
||||
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||
@@ -1304,7 +1330,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||
const Uint32 syncingMipLevelCount =
|
||||
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
|
||||
if (outResource.image != VK_NULL_HANDLE &&
|
||||
// A pending storage-usage upgrade also has to bust the skip: nothing about the texture's
|
||||
// content or params changed, but the image itself must be recreated with STORAGE usage
|
||||
// before it can back an image-unit descriptor.
|
||||
const Bool storageUpgradePending =
|
||||
!outResource.storageUsageResolved &&
|
||||
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
|
||||
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
|
||||
outResource.syncedContentVersion == syncingContentVersion &&
|
||||
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
|
||||
outResource.syncedMipLevelCount == syncingMipLevelCount) {
|
||||
@@ -1415,16 +1447,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkImageAspectFlags aspect = GetAspectMaskForFormat(format);
|
||||
VkFormatProperties formatProperties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
|
||||
const Bool supportsStorageImage =
|
||||
// Only textures that have actually been bound to a GL image unit get STORAGE usage (and
|
||||
// the MUTABLE_FORMAT it drags in for format-reinterpreting image views). Requesting it
|
||||
// for every storage-capable colour texture costs real bandwidth: Adreno cannot keep UBWC
|
||||
// compression on an image that may be written through a storage descriptor, so the whole
|
||||
// render target - MC's included - runs uncompressed. MarkStorageImageTexture upgrades a
|
||||
// texture before its first image-unit draw, and the usage below feeds the compatibility
|
||||
// check so the upgrade recreates the image.
|
||||
const Bool markedAsStorageImage =
|
||||
m_storageImageTextures.find(MakeTextureIdentity(
|
||||
const_cast<MG_State::GLState::ITextureObject*>(&texture))) != m_storageImageTextures.end();
|
||||
// Storage-image CAPABILITY (does the format allow it at all) is deliberately separate from
|
||||
// whether this texture actually needs the usage. MUTABLE_FORMAT keys off capability, as
|
||||
// before: format-reinterpreting views are not a storage-only concern - the SAMPLED path
|
||||
// needs them too (GetOrCreateSampledImageView bails out without it, see ~line 892), so
|
||||
// tying MUTABLE_FORMAT to the image-unit mark would break sampled format reinterpretation
|
||||
// for every texture that never becomes a storage image.
|
||||
const Bool storageImageCapable =
|
||||
!isMultisampleTexture &&
|
||||
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
|
||||
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
|
||||
const Bool supportsStorageImage = storageImageCapable && markedAsStorageImage;
|
||||
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
|
||||
if (supportsStorageImage && IsMutableStorageImageFormat(format) &&
|
||||
if (storageImageCapable && IsMutableStorageImageFormat(format) &&
|
||||
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
||||
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||
}
|
||||
|
||||
VkImageUsageFlags desiredUsage =
|
||||
VK_IMAGE_USAGE_SAMPLED_BIT |
|
||||
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
|
||||
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
|
||||
(((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) ?
|
||||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT :
|
||||
0);
|
||||
if (!isMultisampleTexture) {
|
||||
desiredUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
}
|
||||
|
||||
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
|
||||
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
|
||||
resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
|
||||
@@ -1433,6 +1493,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.viewType == shapeInfo.viewType &&
|
||||
resource.sampleCount == resolvedSampleCount &&
|
||||
resource.imageCreateFlags == imageCreateFlags &&
|
||||
resource.usageFlags == desiredUsage &&
|
||||
resource.mipLevels == backingMipLevels;
|
||||
if (compatible) {
|
||||
if (resource.perMipViews.size() != backingMipLevels) {
|
||||
@@ -1441,6 +1502,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (resource.perMipSampledViews.size() != backingMipLevels) {
|
||||
resource.perMipSampledViews.resize(backingMipLevels, VK_NULL_HANDLE);
|
||||
}
|
||||
// Keeping the image is itself the answer to the mark: either it already carries
|
||||
// STORAGE, or this format can never carry it. Either way there is nothing left to
|
||||
// recreate, so stop reporting the texture as needing preparation.
|
||||
resource.storageUsageResolved = markedAsStorageImage;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1455,7 +1520,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.sampleCount == resolvedSampleCount &&
|
||||
resource.imageCreateFlags == imageCreateFlags &&
|
||||
resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT &&
|
||||
resource.mipLevels < backingMipLevels &&
|
||||
// '<=' rather than '<': a storage-usage upgrade recreates the image with an
|
||||
// unchanged mip count, and its contents (a render target's pixels live only on the
|
||||
// GPU) still have to survive. The vkCmdCopyImage below copies min(mipLevels).
|
||||
resource.mipLevels <= backingMipLevels &&
|
||||
resource.layout != VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
|
||||
std::unique_ptr<TextureResource> preservedResource;
|
||||
@@ -1477,16 +1545,37 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
imageInfo.format = format;
|
||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT |
|
||||
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
|
||||
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
|
||||
(((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) ?
|
||||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT :
|
||||
0);
|
||||
if (!isMultisampleTexture) {
|
||||
imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
}
|
||||
imageInfo.usage = desiredUsage;
|
||||
imageInfo.samples = resolvedSampleCount;
|
||||
|
||||
// Bound the mutability. A blindly-mutable image has to be laid out so that ANY format in
|
||||
// its compatibility class can be viewed, which costs bandwidth compression on tilers;
|
||||
// naming the exact set instead lets the driver keep it. Only safe when that set really is
|
||||
// exhaustive, so it is restricted to textures that are not image-unit bound: sampled views
|
||||
// can only ever ask for ResolveSampledImageViewFormat's output, whereas glBindImageTexture
|
||||
// may name any compatible format, which nothing here can enumerate ahead of time.
|
||||
Vector<VkFormat> viewFormats;
|
||||
VkImageFormatListCreateInfo formatListInfo{};
|
||||
if (m_imageFormatListSupported && !supportsStorageImage &&
|
||||
(imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||
viewFormats.push_back(format);
|
||||
for (const SamplerNumericDomain domain : {SamplerNumericDomain::Float,
|
||||
SamplerNumericDomain::SignedInteger,
|
||||
SamplerNumericDomain::UnsignedInteger}) {
|
||||
const VkFormat viewFormat = ResolveSampledImageViewFormat(format, domain);
|
||||
if (viewFormat == VK_FORMAT_UNDEFINED) {
|
||||
continue;
|
||||
}
|
||||
if (std::find(viewFormats.begin(), viewFormats.end(), viewFormat) == viewFormats.end()) {
|
||||
viewFormats.push_back(viewFormat);
|
||||
}
|
||||
}
|
||||
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
|
||||
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
|
||||
formatListInfo.pViewFormats = viewFormats.data();
|
||||
imageInfo.pNext = &formatListInfo;
|
||||
}
|
||||
|
||||
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
||||
VkImageFormatProperties imageFormatProperties{};
|
||||
VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||
@@ -1543,6 +1632,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.viewType = shapeInfo.viewType;
|
||||
resource.sampleCount = resolvedSampleCount;
|
||||
resource.imageCreateFlags = imageCreateFlags;
|
||||
resource.usageFlags = imageInfo.usage;
|
||||
resource.storageUsageResolved = markedAsStorageImage;
|
||||
resource.syncedTextureParamsVersion = 0;
|
||||
|
||||
if (preservedResource) {
|
||||
|
||||
@@ -53,6 +53,9 @@ public:
|
||||
VkCommandPool commandPool = VK_NULL_HANDLE;
|
||||
VkQueue graphicsQueue = VK_NULL_HANDLE;
|
||||
Uint32 frameCount = 0;
|
||||
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
|
||||
// formats they will be viewed as, which is what lets a tiler keep them compressed.
|
||||
Bool imageFormatListSupported = false;
|
||||
};
|
||||
|
||||
struct TextureResource {
|
||||
@@ -157,6 +160,17 @@ public:
|
||||
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkImageCreateFlags imageCreateFlags = 0;
|
||||
// Usage the live image was created with. STORAGE is only requested for textures that
|
||||
// have actually been bound to a GL image unit, because on Adreno a storage-capable
|
||||
// image loses UBWC bandwidth compression; a later image binding upgrades the usage
|
||||
// and recreates the image, so the resolved usage has to be part of the compatibility
|
||||
// check that decides whether the existing image can be kept.
|
||||
VkImageUsageFlags usageFlags = 0;
|
||||
// True once this image was (re)resolved while the texture was already marked as an
|
||||
// image-unit texture. Distinguishes "not upgraded yet" from "cannot be upgraded"
|
||||
// (a format whose optimalTilingFeatures lack STORAGE_IMAGE never gains the bit), so
|
||||
// NeedsStorageImagePreparation cannot ask for a recreate that will never happen.
|
||||
Bool storageUsageResolved = false;
|
||||
Uint16 syncedTextureParamsVersion = 0;
|
||||
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
||||
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
||||
@@ -190,6 +204,8 @@ public:
|
||||
std::swap(this->viewType, that.viewType);
|
||||
std::swap(this->sampleCount, that.sampleCount);
|
||||
std::swap(this->imageCreateFlags, that.imageCreateFlags);
|
||||
std::swap(this->usageFlags, that.usageFlags);
|
||||
std::swap(this->storageUsageResolved, that.storageUsageResolved);
|
||||
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||
@@ -251,6 +267,8 @@ public:
|
||||
viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
imageCreateFlags = 0;
|
||||
usageFlags = 0;
|
||||
storageUsageResolved = false;
|
||||
syncedTextureParamsVersion = 0;
|
||||
syncedContentVersion = 0;
|
||||
syncedMipLevelCount = 0;
|
||||
@@ -289,6 +307,17 @@ public:
|
||||
VkImageLayout newLayout);
|
||||
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||
// Records that this texture is bound to a GL image unit, so its image must carry
|
||||
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
|
||||
// therefore before the render pass is committed: an image that has to be upgraded is
|
||||
// recreated, which is illegal inside a render pass. Sticky for the texture's lifetime -
|
||||
// GL lets an image binding come and go, and re-creating the image every time it does
|
||||
// would cost far more than the compression it wins back.
|
||||
void MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture);
|
||||
// True when this texture is marked but its live image predates the mark, i.e. the next sync
|
||||
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
|
||||
// submit their pending recording first, so that copy cannot read pre-flush content.
|
||||
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
|
||||
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
|
||||
// texture as a storage image may need work that is illegal inside a render pass (resource
|
||||
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
|
||||
@@ -375,6 +404,7 @@ private:
|
||||
VmaAllocator m_allocator = nullptr;
|
||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
|
||||
Bool m_imageFormatListSupported = false;
|
||||
Uint32 m_currentFrameIndex = 0;
|
||||
|
||||
Uint8 m_gcCounter = 0;
|
||||
@@ -398,6 +428,8 @@ private:
|
||||
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
||||
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
|
||||
Vector<Vector<TextureResource>> m_deferredReleases;
|
||||
Vector<Vector<VkImageView>> m_deferredViewReleases;
|
||||
};
|
||||
|
||||
@@ -2491,7 +2491,7 @@ void main() {
|
||||
MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed.");
|
||||
succeeded = m_textureManager->Initialize(
|
||||
{m_device, m_physicalDevice.handle, m_allocator, m_commandPool, m_graphicsQueue,
|
||||
m_frameContext.GetFrameCount()});
|
||||
m_frameContext.GetFrameCount(), m_imageFormatListExtensionEnabled});
|
||||
MOBILEGL_ASSERT(succeeded, "VkTextureManager initialization failed.");
|
||||
m_clearManager = MakeUnique<VkClearManager>();
|
||||
MOBILEGL_ASSERT(m_clearManager != nullptr, "VkClearManager creation failed.");
|
||||
@@ -4193,7 +4193,7 @@ void main() {
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::PrepareStorageImageTextures(
|
||||
VkCommandBuffer commandBuffer,
|
||||
FrameContext::FrameData& frame,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj) {
|
||||
if (!programObj.hasStorageImages) {
|
||||
@@ -4214,9 +4214,18 @@ void main() {
|
||||
// keep the render pass alive instead of splitting it on every storage-image draw (on
|
||||
// tiled GPUs each split is a full tile load/store). GL makes cross-draw image-store
|
||||
// coherence the app's job (glMemoryBarrier), so no implicit barrier is owed here.
|
||||
Bool anyNeedsPreparation = false;
|
||||
// Record every image-unit binding before probing anything: a texture whose image was
|
||||
// created without STORAGE usage (the default - it costs UBWC compression on Adreno)
|
||||
// needs a recreate, and the probe below is what ends the render pass so that recreate
|
||||
// lands here rather than mid-pass. This cannot be folded into the probe loop, which
|
||||
// stops at the first texture that needs work and would leave the rest unmarked.
|
||||
for (auto* texture : storageTextures) {
|
||||
MOBILEGL_ASSERT(texture != nullptr, "%s: collected a null storage texture", __func__);
|
||||
m_textureManager->MarkStorageImageTexture(*texture);
|
||||
}
|
||||
|
||||
Bool anyNeedsPreparation = false;
|
||||
for (auto* texture : storageTextures) {
|
||||
if (m_textureManager->NeedsStorageImagePreparation(*texture) ||
|
||||
m_clearManager->HasPendingClear(texture)) {
|
||||
anyNeedsPreparation = true;
|
||||
@@ -4227,21 +4236,51 @@ void main() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A first-time storage-usage upgrade recreates the image and carries the old contents
|
||||
// forward with an out-of-band, immediately-submitted copy (PreserveTextureContentsOnRecreate).
|
||||
// Whatever this frame already recorded into the old image is still sitting unsubmitted in
|
||||
// this command buffer, so that copy would read pre-frame content and this frame's rendering
|
||||
// into the texture would be lost - precisely the render-target-then-image-unit case this
|
||||
// whole path exists for. Submit what is recorded first; the copy then queues behind it.
|
||||
Bool anyNeedsStorageUpgrade = false;
|
||||
for (auto* texture : storageTextures) {
|
||||
if (m_textureManager->NeedsStorageUsageUpgrade(*texture)) {
|
||||
anyNeedsStorageUpgrade = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (anyNeedsStorageUpgrade && HasPendingRecordedWork()) {
|
||||
if (FlushPendingCommands()) {
|
||||
// Fresh command buffer: the sampled-descriptor-set memo describes bindings that
|
||||
// only existed in the retired one. FlushPendingCommands drops the pipeline memo
|
||||
// itself; this is the other command-buffer-scoped cache.
|
||||
m_lastSampledSetValid = false;
|
||||
} else {
|
||||
// Best effort: the upgrade still produces a correct image, only its preserved
|
||||
// contents may predate this frame's writes. Dropping the draw would be worse.
|
||||
MGLOG_E("%s: flush before a storage-usage image upgrade failed; preserved contents "
|
||||
"may be stale for one frame", __func__);
|
||||
}
|
||||
}
|
||||
if (!frame.isCommandRecording) {
|
||||
m_frameContext.BeginCommandRecording();
|
||||
}
|
||||
|
||||
// Image uploads, deferred-clear materialization, and layout barriers are illegal inside
|
||||
// a classic render pass. Do this before sampler preparation as well: a texture used by
|
||||
// both a sampler and an image must stay in GENERAL, and both descriptors must name that
|
||||
// same layout independent of SPIR-V reflection/binding order.
|
||||
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
|
||||
VkRenderPassManager::EndRenderPass(commandBuffer);
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
|
||||
for (auto* texture : storageTextures) {
|
||||
if (!MaterializePendingClearForTexture(commandBuffer, *texture)) {
|
||||
if (!MaterializePendingClearForTexture(frame.commandBuffer, *texture)) {
|
||||
MGLOG_E("%s: failed to materialize pending clear for storage textureId=%d",
|
||||
__func__, texture->GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
if (!m_textureManager->TransitionTextureForStorageImage(commandBuffer, *texture)) {
|
||||
if (!m_textureManager->TransitionTextureForStorageImage(frame.commandBuffer, *texture)) {
|
||||
MGLOG_E("%s: failed to prepare storage textureId=%d",
|
||||
__func__, texture->GetExternalIndex());
|
||||
return false;
|
||||
@@ -4267,7 +4306,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) {
|
||||
@@ -4277,7 +4325,7 @@ void main() {
|
||||
m_lastSampledSetValid = false;
|
||||
}
|
||||
|
||||
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
|
||||
if (!PrepareStorageImageTextures(frame, program, programObj)) {
|
||||
MGLOG_E("SetupDraw skipped: storage image preparation failed");
|
||||
return false;
|
||||
}
|
||||
@@ -4502,7 +4550,7 @@ void main() {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
|
||||
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
|
||||
if (!PrepareStorageImageTextures(frame, program, programObj)) {
|
||||
MGLOG_E("DispatchCompute skipped: storage image preparation failed");
|
||||
return;
|
||||
}
|
||||
@@ -4542,7 +4590,7 @@ void main() {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
|
||||
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) {
|
||||
if (!PrepareStorageImageTextures(frame, program, programObj)) {
|
||||
MGLOG_E("DispatchComputeIndirect skipped: storage image preparation failed");
|
||||
return;
|
||||
}
|
||||
@@ -8148,6 +8196,18 @@ void main() {
|
||||
|
||||
const Vector<VkExtensionProperties> availableExtensions = EnumerateDeviceExtensions(m_physicalDevice.handle);
|
||||
ResolveOptionalDeviceExtensions(availableExtensions, enabledDeviceExtensions);
|
||||
|
||||
// VK_KHR_image_format_list lets a MUTABLE_FORMAT image declare exactly which formats it
|
||||
// may be viewed as. Adreno drops UBWC bandwidth compression on a blindly-mutable image
|
||||
// (measured: 65 -> 80 fps in MC 26.2 once mutability is not requested); an explicit,
|
||||
// compression-compatible format list is the portable way to keep both.
|
||||
m_imageFormatListExtensionEnabled =
|
||||
IsExtensionSupported(availableExtensions, VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME);
|
||||
if (m_imageFormatListExtensionEnabled) {
|
||||
enabledDeviceExtensions.push_back(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME);
|
||||
}
|
||||
MGLOG_I("VK_KHR_image_format_list enabled: %s",
|
||||
m_imageFormatListExtensionEnabled ? "true" : "false");
|
||||
MGLOG_I("VK_KHR_draw_indirect_count enabled: %s", m_drawIndirectCountExtensionEnabled ? "true" : "false");
|
||||
|
||||
m_indexTypeUint8ExtensionEnabled = false;
|
||||
|
||||
@@ -576,8 +576,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const RenderPassEntry& renderPassEntry);
|
||||
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
|
||||
void DestroyComputePipelines();
|
||||
// Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to
|
||||
// flush the pending recording (see the body), which retires the current command buffer.
|
||||
Bool PrepareStorageImageTextures(
|
||||
VkCommandBuffer commandBuffer,
|
||||
FrameContext::FrameData& frame,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
|
||||
@@ -639,6 +641,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const PhysicalDevice& compareWithDevice,
|
||||
PhysicalDevice& outBetterDevice);
|
||||
static constexpr const char* s_validationLayerNames[] = {"VK_LAYER_KHRONOS_validation"};
|
||||
// VK_KHR_image_format_list: lets MUTABLE_FORMAT images declare their exact view-format
|
||||
// set so the driver can keep bandwidth compression (see CreateLogicalDeviceAndQueues).
|
||||
Bool m_imageFormatListExtensionEnabled = false;
|
||||
|
||||
static constexpr const char* s_deviceExtensionNames[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
|
||||
static Bool CheckValidationLayerSupport();
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user