mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 20:58:31 +09:00
[Fix] (DirectVulkan): bound image mutability so Adreno keeps UBWC compression
- Every storage-capable colour texture was created MUTABLE_FORMAT, and Adreno gives up bandwidth compression on an image that may be viewed as any format in its compatibility class. MC's main render target therefore ran uncompressed; in a fill-bound scene that is the whole frame budget. Measured on Adreno 650, MC 26.2, same scene and camera, device cooled to 38-40C before each run: 65.3 -> 80.9 fps (+23.9%), GPU busy ~93% in both. - VK_KHR_image_format_list (enabled when present) fixes it without giving up mutability: VkImageFormatListCreateInfo names the exact formats a view may use, so the driver can keep the image compressed. The set must be exhaustive or the result is undefined - for sampled views it is exactly what ResolveSampledImageViewFormat can return over the three numeric domains. - glBindImageTexture may name any compatible format, which cannot be enumerated ahead of time, so a texture bound to an image unit gets no format list. That is what VK_IMAGE_USAGE_STORAGE_BIT becoming on-demand is for: it makes "unmarked" mean "will never receive an arbitrary-format storage view", which is what makes the list sound. Removing STORAGE is worth nothing on its own (65.4 fps, measured) - only the mutability bound pays. - MarkStorageImageTexture runs over every collected image-unit texture before the probe loop in PrepareStorageImageTextures, because that loop stops at the first texture needing work and would leave the rest unmarked. The mark makes NeedsStorageImagePreparation report true, which is what ends the render pass, so the recreate lands outside it. - storageUsageResolved separates "not upgraded yet" from "this format can never carry STORAGE", so a format whose optimalTilingFeatures lack STORAGE_IMAGE cannot ask for a recreate that will never happen. SyncTexture's cross-draw early-out also has to break on a pending upgrade or the recreate never runs. - An upgrade recreates the image and carries its contents forward through PreserveTextureContentsOnRecreate, which submits its own command buffer and waits. Whatever the frame already recorded into the old image is still unsubmitted, so that copy would read pre-frame content and this frame's rendering into the texture would be lost - exactly the render-target-then- image-unit case. PrepareStorageImageTextures now flushes first; it takes the FrameData rather than a command buffer because the flush retires the current one, and drops the sampled-descriptor-set memo that described it.
This commit is contained in:
@@ -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;
|
||||
@@ -4286,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;
|
||||
}
|
||||
@@ -4511,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;
|
||||
}
|
||||
@@ -4551,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;
|
||||
}
|
||||
@@ -8157,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;
|
||||
|
||||
Reference in New Issue
Block a user