Compare commits

..
Author SHA1 Message Date
swung0x48 3223ecb14e [Feat] (DirectVulkan): relax fragment precision where the bound formats allow it
- WIP, parked: measures 80.9 -> 94.8 fps on Adreno 650 / MC 26.2 (same scene,
  device cooled to 38-40C), but is NOT validated. Desktop GLSL carries no
  precision qualifiers, so every fragment value reaches the driver as fp32 while
  Adreno runs fp16 at twice the rate.
- RelaxTextureDerivedPrecisionPass taints the values a fragment shader derives
  from built-in inputs and decorates everything else RelaxedPrecision. The
  taint direction matters: whitelisting outward from texture reads captures
  nothing, because MC multiplies every texel by an interpolated colour and a UBO
  value and one un-relaxed operand vetoes the expression - measured at 80.4 fps,
  i.e. no gain, both with and without varyings seeded. Precision-critical
  sources are few (gl_FragCoord cannot even hold a 3044-pixel x exactly), so
  tainting them and relaxing the rest is what actually pays.
- SPIR-V cannot see the bound formats - sampler2D yields vec4 whether the
  texture is RGBA8 or RGBA32F - so the decision is made per draw and passed in
  as a compile option, the same shape ExplicitLod0Sampling already uses.
  RelaxedFragmentPrecision is only requested when every sampled texture and
  every colour attachment is an 8-bit-or-less normalized format, where fp16's
  11-bit mantissa already carries the value exactly. Shaderpack HDR gbuffers,
  float data textures and 16-bit normalized targets therefore keep full
  precision, as do shaders that write gl_FragDepth or gl_SampleMask.
- LocalMultiStoreElim runs first: glslang emits function-local variables, and a
  load can never be relaxed, so without SSA promotion the analysis dies at the
  first temporary.
- WHY THIS IS PARKED: the retrace correctness gate never ran green. Every
  DirectVulkan retrace on Adreno 650 dies with DEVICE_LOST in
  UploadDirtyMipLevels on unmodified dev (pre-existing, device-gated), and on
  Adreno 830 - where the gate does pass on dev - minecraft-1.21.4-in-world times
  out at 900s with this change, which still needs explaining. Do not merge until
  that is understood and vanilla plus non-Photon shaderpack cases pass.
  (photon-v1.3b is broken on Adreno independently of this work.)
- The /sdcard/MG/exp_relaxed_precision_all and exp_no_relaxed_precision file
  toggles are development scaffolding for A/B measurement; they must go before
  this ships.
2026-07-29 09:02:00 -04:00
swung0x48 fc4cd980f2 [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.
2026-07-29 07:07:50 -04:00
swung0x48 992d16267c [Fix] (DirectVulkan): rewrite implicit-LOD fragment samples to explicit LOD 0 when every bound sampler is pinned to a single mip level - Adreno 650 (driver 512.502) reads outside a full-screen colour render target's allocation on its implicit-LOD sampling path and faults the GPU, which killed MC 26.2 on its own blit shader (texture(InSampler, texCoord)) between frames 344-421 on every run; this is the same driver defect the default-framebuffer blit shader already works around with textureLod, but an application's shader cannot be edited, so ForceExplicitLod0SamplePass converts OpImageSample*ImplicitLod to the explicit form at the SPIR-V level under a new CompileOptionBit that is only requested when the rewrite provably cannot move a texel (every sampler binding on a single-level view, no anisotropy, and either a LOD clamp that already pins lambda at 0 or min and mag filters that agree - an explicit LOD 0 always takes the magnification side of the min/mag decision); a single-level view now also clamps its sampler to mipmapMode NEAREST with maxLod min(maxLod, 0.25) rather than 0, since collapsing the clamp would make every fragment magnify and quietly retire the min filter; and the program's backend hash memo grows from one slot to four so a program resolved under two compile-flag sets in the same frame stops re-hashing every stage's SPIR-V once per draw 2026-07-29 03:26:57 -04:00
swung0x48 0ea9e6de5f [Fix] (DirectVulkan): follow surface resizes instead of rebuilding the swapchain on VK_SUBOPTIMAL_KHR - a per-frame surface-capabilities comparison (ANGLE's model) is now the only thing that schedules a rebuild, so a launcher-side resolution change reaches the swapchain and the compositor scales the smaller image up to the view, while a driver that merely reports the surface as suboptimal can no longer rebuild every frame (each rebuild destroys every pipeline, resets the render-pass manager and reallocates the default framebuffer, which showed as flicker, then corruption, then a crash); the comparison runs in SURFACE space against the extent the live swapchain was created from, since comparing against the swapchain's own quarter-turn-swapped extent reports a difference on every rotated frame 2026-07-28 21:06:57 -04:00
swung0x48 241ed377b4 [Fix] (macOS): harden Cocoa context setup and isolate embedded glslang 2026-07-28 11:54:50 -04:00
swung0x48 bf312a4b67 [Fix] (DirectVulkan): explicit-LOD blit sampling and present-path hardening - the default-framebuffer blit shader now samples with textureLod 0 (a blit reads exactly the selected level; Adreno 650's implicit-LOD path reads past a single-mip UBWC render target's allocation despite maxLod=0, page-faulting the GPU on MC 26.2's second startup frame once the neighbouring startup staging memory is returned - the invalidated context then failed the next Present submit with EDEADLK/DEVICE_LOST), TransitionToPresent appends the present barrier into the frame's open recording instead of silently dropping it whenever anything was recorded (frames without a default-FBO render pass presented images stuck in their acquired layout), VK_SUBOPTIMAL_KHR acquires are treated as the success they are (image acquired, semaphore signal armed - the early return skipped the fence reset and consumed-flag clear, and callers re-acquired on the same binary semaphore; rebuilds now defer to after the signal is consumed), and validation builds report through VK_EXT_debug_report when VK_EXT_debug_utils is absent instead of aborting instance creation 2026-07-28 06:00:20 -04:00
24 changed files with 1422 additions and 78 deletions
+14
View File
@@ -455,8 +455,21 @@ if (ANDROID)
endif() endif()
if (APPLE AND NOT MOBILEGL_IOS) if (APPLE AND NOT MOBILEGL_IOS)
# MobileGL statically embeds glslang, SPIRV-Tools, and SPIRV-Cross. When
# this dylib is injected with DYLD_INSERT_LIBRARIES, exporting those C++
# symbols interposes incompatible copies embedded by host libraries such
# as shaderc. Keep only the public GL/EGL/CGL loader surface globally
# visible; GetProcAddress can still return pointers to hidden internals.
set(MOBILEGL_MACOS_EXPORTED_SYMBOLS
"${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt")
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
"LINKER:-exported_symbols_list,${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
set_property(TARGET ${CMAKE_PROJECT_NAME} APPEND PROPERTY
LINK_DEPENDS "${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
"-framework Cocoa" "-framework Cocoa"
"-framework CoreVideo"
"-framework QuartzCore" "-framework QuartzCore"
"-framework Foundation" "-framework Foundation"
"-framework OpenGL" "-framework OpenGL"
@@ -464,6 +477,7 @@ if (APPLE AND NOT MOBILEGL_IOS)
if(TARGET ${CMAKE_PROJECT_NAME}_s) if(TARGET ${CMAKE_PROJECT_NAME}_s)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
"-framework Cocoa" "-framework Cocoa"
"-framework CoreVideo"
"-framework QuartzCore" "-framework QuartzCore"
"-framework Foundation" "-framework Foundation"
"-framework OpenGL" "-framework OpenGL"
+6 -4
View File
@@ -107,9 +107,11 @@ namespace MobileGL {
// (EGL/WGL/CGL): initialization happens lazily on the first entry point // (EGL/WGL/CGL): initialization happens lazily on the first entry point
// via EnsureInitialized(), and full teardown happens deterministically // via EnsureInitialized(), and full teardown happens deterministically
// when the last EGL display is terminated with nothing current (EGLImpl // when the last EGL display is terminated with nothing current (EGLImpl
// calls Destroy()). There is intentionally no static constructor, no // calls Destroy()). There is intentionally no backend-initializing static
// static destructor, and no DllMain: the global singletons use // constructor, no static destructor, and no DllMain: the global singletons
// leak-at-exit storage (see GlobalObjects.cpp), so a process that exits // use leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
// without eglTerminate simply leaks them to the OS instead of running // without eglTerminate simply leaks them to the OS instead of running
// backend destructors during static teardown. // backend destructors during static teardown. macOS has a lightweight
// dyld constructor that installs NSOpenGL dispatch hooks only; full backend
// initialization still enters here from the first hooked CGL context.
} // namespace MobileGL } // namespace MobileGL
+4 -3
View File
@@ -13,9 +13,10 @@ namespace MobileGL {
void Initialize(); void Initialize();
// Thread-safe, idempotent, and re-entrant wrapper around Initialize(). // Thread-safe, idempotent, and re-entrant wrapper around Initialize().
// Host layers (EGL/WGL/CGL entry points) call this lazily on first use so // Host layers (EGL/WGL/CGL entry points) call this lazily on first use so
// MobileGL's lifecycle never depends on ELF/DLL static constructors, and // full backend initialization never depends on ELF/DLL static constructors,
// so a fresh init can follow a full Destroy() (e.g. after the last // and so a fresh init can follow a full Destroy() (e.g. after the last
// eglTerminate). // eglTerminate). The macOS dyld bootstrap installs only lightweight
// NSOpenGL method hooks.
void EnsureInitialized(); void EnsureInitialized();
void Destroy(); void Destroy();
@@ -150,12 +150,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) { Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) {
auto& frame = GetCurrent(); auto& frame = GetCurrent();
if (frame.hasCommandBufferRecorded || frame.isCommandRecording || oldLayout == presentLayout || if (oldLayout == presentLayout || oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
return false; return false;
} }
auto& commandBuffer = BeginCommandRecording(); // The barrier belongs in the frame's own recording. Bailing out because
// something was already recorded (the previous behaviour) dropped the
// transition entirely for every frame that never ran a default-framebuffer
// render pass - the only other thing that carries the image to
// PRESENT_SRC_KHR, via that pass's finalLayout - so the swapchain image was
// handed to the WSI still in the layout it was acquired in.
// A closed-but-unsubmitted buffer can only come from a submit that already
// failed (SubmitPendingCommandBuffer leaves the flag set on error), and
// appending to it is illegal while reopening would reset the frame's own
// commands away. The device is gone on that path anyway - stay silent-safe
// rather than trade a lost device for a barrier into a closed buffer.
if (frame.hasCommandBufferRecorded) {
MGLOG_E("TransitionToPresent: command buffer already closed; skipping the present barrier");
return false;
}
// Reopening a recording here would vkResetCommandBuffer this frame's own
// commands away, so append to the open one and let the caller close it.
const Bool openedRecording = !frame.isCommandRecording;
VkCommandBuffer commandBuffer = openedRecording ? BeginCommandRecording() : frame.commandBuffer;
VkImageMemoryBarrier presentBarrier{}; VkImageMemoryBarrier presentBarrier{};
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
@@ -174,7 +192,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
nullptr, 0, nullptr, 1, &presentBarrier); nullptr, 0, nullptr, 1, &presentBarrier);
if (openedRecording) {
EndCommandRecording(); EndCommandRecording();
}
return true; return true;
} }
@@ -227,12 +247,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence, result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
&outImageIndex); &outImageIndex);
if (result != VK_SUCCESS) { // VK_SUBOPTIMAL_KHR is a success code: an image *was* acquired and
// imageAvailableSemaphore *will* be signaled. Bailing out on it skipped both
// the consumed-flag reset (leaving a stale "already consumed", so the next
// submit never waited on the pending signal) and the fence reset (leaving
// the slot's fence signaled for the next submit to reuse). Only a genuine
// failure - VK_ERROR_OUT_OF_DATE_KHR and friends, where nothing is acquired
// and nothing is signaled - skips the bookkeeping.
if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
return result; return result;
} }
frame.imageAvailableSemaphoreConsumed = false; frame.imageAvailableSemaphoreConsumed = false;
return vkResetFences(device, 1, &frame.imageInFlightFence); const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence);
// Hand the acquire's own code back so the caller can schedule a rebuild.
return resetResult == VK_SUCCESS ? result : resetResult;
} }
Uint32 FrameContext::GetCurrentFrameIndex() const { Uint32 FrameContext::GetCurrentFrameIndex() const {
@@ -12,7 +12,10 @@
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h" #include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
#include "MG_Util/ShaderTranspiler/SpvcSession.h" #include "MG_Util/ShaderTranspiler/SpvcSession.h"
#include "MG_Util/ShaderTranspiler/Types.h" #include "MG_Util/ShaderTranspiler/Types.h"
#include <cmath>
#include <cstdio>
#include <cstring> #include <cstring>
#include <unordered_set>
#include <spirv-tools/libspirv.h> #include <spirv-tools/libspirv.h>
#include <spirv-tools/optimizer.hpp> #include <spirv-tools/optimizer.hpp>
#include <source/opt/build_module.h> #include <source/opt/build_module.h>
@@ -923,11 +926,610 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ProgramFactory::CompileOptionFlags m_transformFlags; 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>());
}
// TEMP-PERFDIAG: measure what fragment-stage fp32 costs on this GPU. Desktop GLSL carries
// no precision qualifiers, so everything reaches the driver as full fp32 while Adreno runs
// fp16 at twice the rate. Decorating every float-typed result in a fragment entry point
// with RelaxedPrecision is the blunt "all mediump" upper bound - it changes results, so it
// is a probe, not a shipping transform. Toggled by /sdcard/MG/exp_relaxed_precision.
class RelaxedPrecisionProbePass final : public spvtools::opt::Pass {
public:
const char* name() const override { return "relaxed-precision-probe"; }
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;
// Every 32-bit-float scalar/vector/matrix type in the module. Anything wider (f64)
// or narrower is left alone: RelaxedPrecision only has meaning for 32-bit floats.
std::unordered_set<Uint32> relaxableTypes;
for (auto& type : get_module()->types_values()) {
const Uint32 typeId = type.result_id();
if (typeId == 0) continue;
switch (type.opcode()) {
case spv::Op::OpTypeFloat:
if (type.GetSingleWordInOperand(0) == 32) relaxableTypes.insert(typeId);
break;
case spv::Op::OpTypeVector:
case spv::Op::OpTypeMatrix:
if (relaxableTypes.count(type.GetSingleWordInOperand(0)) != 0) {
relaxableTypes.insert(typeId);
}
break;
default:
break;
}
}
if (relaxableTypes.empty()) return Status::SuccessWithoutChange;
Vector<Uint32> targets;
for (auto& function : *get_module()) {
for (auto& block : function) {
for (auto& inst : block) {
const Uint32 resultId = inst.result_id();
if (resultId == 0) continue;
if (relaxableTypes.count(inst.type_id()) == 0) continue;
targets.push_back(resultId);
}
}
}
if (targets.empty()) return Status::SuccessWithoutChange;
for (const Uint32 id : targets) {
context()->get_decoration_mgr()->AddDecoration(
id, static_cast<Uint32>(spv::Decoration::RelaxedPrecision));
}
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
};
// Relax fragment-stage arithmetic that provably came out of a texture read. Desktop GLSL
// has no precision qualifiers, so every fragment value reaches the driver as fp32 while
// Adreno runs fp16 at twice the rate - and a texel is at most 8 bits per channel, which
// fp16's 11-bit mantissa carries exactly. Seeding at image reads and propagating only
// through operations whose every input is already relaxed keeps everything the shader
// computes from other sources (screen coordinates, depth, wide-range uniforms) at full
// precision, which is where fp16 would actually go wrong: fp16 cannot even represent a
// 3044-pixel gl_FragCoord.x exactly.
class RelaxTextureDerivedPrecisionPass final : public spvtools::opt::Pass {
public:
const char* name() const override { return "relax-texture-derived-precision"; }
Status Process() override {
if (!IsFragmentEntryPoint()) return Status::SuccessWithoutChange;
// A shader that drives depth or coverage itself is out of scope: those values must
// stay exact, and proving which computations feed them is not worth it here.
if (WritesDepthOrSampleMask()) return Status::SuccessWithoutChange;
CollectRelaxableFloatTypes();
if (m_relaxableTypes.empty()) return Status::SuccessWithoutChange;
// Whitelisting from texture reads captures nothing in practice: MC's fragment
// shaders multiply every texel by an interpolated colour and a UBO value, so one
// un-relaxed operand vetoes the whole expression (measured: no fps change).
// Taint the few genuinely precision-critical sources instead and relax the rest.
std::unordered_set<Uint32> tainted;
CollectPrecisionCriticalSeeds(tainted);
Bool grew = true;
while (grew) {
grew = false;
for (auto& function : *get_module()) {
for (auto& block : function) {
for (auto& inst : block) {
const Uint32 resultId = inst.result_id();
if (resultId == 0 || tainted.count(resultId) != 0) continue;
if (!AnyOperandTainted(inst, tainted)) continue;
tainted.insert(resultId);
grew = true;
}
}
}
}
std::unordered_set<Uint32> relaxed;
for (auto& function : *get_module()) {
for (auto& block : function) {
for (auto& inst : block) {
const Uint32 resultId = inst.result_id();
if (resultId == 0 || tainted.count(resultId) != 0) continue;
if (m_relaxableTypes.count(inst.type_id()) == 0) continue;
relaxed.insert(resultId);
}
}
}
if (relaxed.empty()) return Status::SuccessWithoutChange;
for (const Uint32 id : relaxed) {
context()->get_decoration_mgr()->AddDecoration(
id, static_cast<Uint32>(spv::Decoration::RelaxedPrecision));
}
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
private:
std::unordered_set<Uint32> m_relaxableTypes;
Bool IsFragmentEntryPoint() const {
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) {
return true;
}
}
return false;
}
Bool WritesDepthOrSampleMask() const {
for (auto& annotation : get_module()->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate) continue;
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
spv::Decoration::BuiltIn) {
continue;
}
const auto builtIn = static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2));
if (builtIn == spv::BuiltIn::FragDepth || builtIn == spv::BuiltIn::SampleMask) {
return true;
}
}
return false;
}
void CollectRelaxableFloatTypes() {
m_relaxableTypes.clear();
for (auto& type : get_module()->types_values()) {
const Uint32 typeId = type.result_id();
if (typeId == 0) continue;
switch (type.opcode()) {
case spv::Op::OpTypeFloat:
if (type.GetSingleWordInOperand(0) == 32) m_relaxableTypes.insert(typeId);
break;
case spv::Op::OpTypeVector:
if (m_relaxableTypes.count(type.GetSingleWordInOperand(0)) != 0) {
m_relaxableTypes.insert(typeId);
}
break;
default:
break;
}
}
}
void CollectImageReadSeeds(std::unordered_set<Uint32>& relaxed) const {
for (auto& function : *get_module()) {
for (auto& block : function) {
for (auto& inst : block) {
const Uint32 resultId = inst.result_id();
if (resultId == 0 || m_relaxableTypes.count(inst.type_id()) == 0) continue;
// Interpolated user varyings seed too, or propagation dies at the
// first `texel * vertexColour`: the load of an Input can never be
// relaxed by the rule below (its operand is a pointer), so a single
// varying vetoes every downstream operation. This is what ESSL's
// mediump varyings already mean. Built-ins are excluded - gl_FragCoord
// carries pixel coordinates that fp16 cannot represent exactly.
if (inst.opcode() == spv::Op::OpLoad && IsNonBuiltInFragmentInput(inst)) {
relaxed.insert(resultId);
continue;
}
switch (inst.opcode()) {
case spv::Op::OpImageSampleImplicitLod:
case spv::Op::OpImageSampleExplicitLod:
case spv::Op::OpImageSampleProjImplicitLod:
case spv::Op::OpImageSampleProjExplicitLod:
case spv::Op::OpImageSampleDrefImplicitLod:
case spv::Op::OpImageSampleDrefExplicitLod:
case spv::Op::OpImageFetch:
case spv::Op::OpImageRead:
case spv::Op::OpImageGather:
relaxed.insert(resultId);
break;
default:
break;
}
}
}
}
}
// OpLoad straight out of a fragment Input variable that carries no BuiltIn decoration.
// Only a direct load counts: a load through an access chain could be indexing a
// structure whose other members are not interpolated colour data.
Bool IsNonBuiltInFragmentInput(const spvtools::opt::Instruction& load) const {
const Uint32 pointerId = load.GetSingleWordInOperand(0);
const auto* pointer = context()->get_def_use_mgr()->GetDef(pointerId);
if (pointer == nullptr || pointer->opcode() != spv::Op::OpVariable) return false;
if (static_cast<spv::StorageClass>(pointer->GetSingleWordInOperand(0)) !=
spv::StorageClass::Input) {
return false;
}
Bool isBuiltIn = false;
context()->get_decoration_mgr()->ForEachDecoration(
pointerId, static_cast<Uint32>(spv::Decoration::BuiltIn),
[&isBuiltIn](const spvtools::opt::Instruction&) { isBuiltIn = true; });
return !isBuiltIn;
}
// A float constant small enough that fp16 represents it without surprise. Colour math
// constants (0, 1, 0.5, 255, gamma exponents) all live here; anything larger is
// treated as unknown so it stops propagation.
Bool IsBoundedFloatConstant(Uint32 id) const {
const auto* constant = context()->get_constant_mgr()->FindDeclaredConstant(id);
if (constant == nullptr) return false;
if (const auto* scalar = constant->AsFloatConstant()) {
const float value = scalar->GetFloat();
return std::isfinite(value) && std::fabs(value) <= 1024.0f;
}
if (const auto* composite = constant->AsVectorConstant()) {
for (const auto* component : composite->GetComponents()) {
const auto* scalar = component->AsFloatConstant();
if (scalar == nullptr) return false;
const float value = scalar->GetFloat();
if (!std::isfinite(value) || std::fabs(value) > 1024.0f) return false;
}
return true;
}
return false;
}
// Precision-critical sources: a built-in fragment input. gl_FragCoord is the one that
// matters - fp16 cannot represent a 3044-pixel x coordinate exactly, and anything
// derived from it (screen-space effects, manual depth reconstruction) would visibly
// quantise. Everything else a fragment shader reads is colour-range data.
void CollectPrecisionCriticalSeeds(std::unordered_set<Uint32>& tainted) const {
for (auto& function : *get_module()) {
for (auto& block : function) {
for (auto& inst : block) {
if (inst.opcode() != spv::Op::OpLoad || inst.result_id() == 0) continue;
if (IsBuiltInInputLoad(inst)) tainted.insert(inst.result_id());
}
}
}
}
Bool IsBuiltInInputLoad(const spvtools::opt::Instruction& load) const {
const Uint32 pointerId = load.GetSingleWordInOperand(0);
const auto* pointer = context()->get_def_use_mgr()->GetDef(pointerId);
if (pointer == nullptr || pointer->opcode() != spv::Op::OpVariable) return false;
if (static_cast<spv::StorageClass>(pointer->GetSingleWordInOperand(0)) !=
spv::StorageClass::Input) {
return false;
}
Bool isBuiltIn = false;
context()->get_decoration_mgr()->ForEachDecoration(
pointerId, static_cast<Uint32>(spv::Decoration::BuiltIn),
[&isBuiltIn](const spvtools::opt::Instruction&) { isBuiltIn = true; });
return isBuiltIn;
}
Bool AnyOperandTainted(const spvtools::opt::Instruction& inst,
const std::unordered_set<Uint32>& tainted) const {
const Uint32 operandCount = inst.NumInOperands();
for (Uint32 i = 0; i < operandCount; ++i) {
const auto& operand = inst.GetInOperand(i);
if (!spvIsIdType(operand.type)) continue;
if (IsNonNumericOperand(inst, i)) continue;
if (tainted.count(operand.words[0]) != 0) return true;
}
return false;
}
Bool AllValueOperandsRelaxed(const spvtools::opt::Instruction& inst,
const std::unordered_set<Uint32>& relaxed) const {
switch (inst.opcode()) {
// Pointer-typed plumbing: relaxing the loaded value would say nothing about the
// memory it came from, and the pointer operand can never be in the set.
case spv::Op::OpLoad:
case spv::Op::OpStore:
case spv::Op::OpAccessChain:
case spv::Op::OpInBoundsAccessChain:
case spv::Op::OpFunctionCall:
return false;
default:
break;
}
Bool sawValueOperand = false;
Bool allRelaxed = true;
const Uint32 operandCount = inst.NumInOperands();
for (Uint32 i = 0; i < operandCount; ++i) {
const auto& operand = inst.GetInOperand(i);
if (!spvIsIdType(operand.type)) continue; // literals: selectors, swizzle indices
const Uint32 id = operand.words[0];
// OpPhi's block labels, OpSelect's condition and OpExtInst's instruction-set id
// are ids that carry no numeric precision; skip them rather than let them veto.
if (IsNonNumericOperand(inst, i)) continue;
sawValueOperand = true;
if (relaxed.count(id) != 0) continue;
if (IsBoundedFloatConstant(id)) continue;
allRelaxed = false;
break;
}
return sawValueOperand && allRelaxed;
}
static Bool IsNonNumericOperand(const spvtools::opt::Instruction& inst, Uint32 index) {
switch (inst.opcode()) {
case spv::Op::OpPhi:
return (index % 2) == 1; // parent block labels
case spv::Op::OpSelect:
return index == 0; // condition
case spv::Op::OpExtInst:
return index == 0; // extended instruction set
default:
return false;
}
}
};
// TEMP-PERFDIAG: A/B switch between the scoped transform and the all-float upper bound.
Bool PerfDiagRelaxAllPrecision() {
static const Bool enabled = [] {
std::FILE* probe = std::fopen("/sdcard/MG/exp_relaxed_precision_all", "rb");
if (probe == nullptr) return false;
std::fclose(probe);
MGLOG_I("[PERFDIAG] fragment RelaxedPrecision: ALL floats (upper-bound probe)");
return true;
}();
return enabled;
}
// TEMP-PERFDIAG: lets a run turn the transform off entirely for an A/B baseline.
Bool PerfDiagRelaxedPrecisionEnabled() {
static const Bool disabled = [] {
std::FILE* probe = std::fopen("/sdcard/MG/exp_no_relaxed_precision", "rb");
if (probe == nullptr) return false;
std::fclose(probe);
MGLOG_I("[PERFDIAG] fragment RelaxedPrecision DISABLED");
return true;
}();
return !disabled;
}
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( spvtools::Optimizer::PassToken CreateGlToVulkanPositionFixPass(
ProgramFactory::CompileOptionFlags transformFlags) { ProgramFactory::CompileOptionFlags transformFlags) {
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags)); return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
} }
// TEMP-PERFDIAG
Bool TransformSpirvForRelaxedPrecisionProbe(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;
options.set_run_validator(false);
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
const char* message) {
MGLOG_E("Vulkan: relaxed-precision probe: %s", message != nullptr ? message : "");
});
// SSA promotion first: glslang emits function-local variables with stores and loads,
// and a load can never be relaxed (its operand is a pointer), so without this the
// propagation below dies at the first temporary.
optimizer.RegisterPass(spvtools::CreateLocalMultiStoreElimPass());
if (PerfDiagRelaxAllPrecision()) {
optimizer.RegisterPass(spvtools::Optimizer::PassToken(MakeUnique<RelaxedPrecisionProbePass>()));
} else {
optimizer.RegisterPass(
spvtools::Optimizer::PassToken(MakeUnique<RelaxTextureDerivedPrecisionPass>()));
}
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
if (!success) {
MGLOG_E("Vulkan: relaxed-precision probe failed; keeping the original module");
output = input;
}
return success;
}
Bool TransformSpirvForVulkanPositionFix(const Vector<Uint>& input, Vector<Uint>& output, Bool TransformSpirvForVulkanPositionFix(const Vector<Uint>& input, Vector<Uint>& output,
ProgramFactory::CompileOptionFlags transformFlags) { ProgramFactory::CompileOptionFlags transformFlags) {
if (input.empty()) { if (input.empty()) {
@@ -1978,6 +2580,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
moduleSpirvs[i] = spv; 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);
}
}
if ((flags & ProgramFactory::CompileOptionBit::RelaxedFragmentPrecision) &&
PerfDiagRelaxedPrecisionEnabled() && shaders[i] &&
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
Vector<Uint> relaxedSpirv;
if (TransformSpirvForRelaxedPrecisionProbe(moduleSpirvs[i], relaxedSpirv)) {
moduleSpirvs[i] = Move(relaxedSpirv);
}
}
// GL apps depend on cross-program position invariance for multi-pass equality // 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 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 // depth its own first pass wrote); decorate Position outputs Invariant so
@@ -42,6 +42,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SurfaceRotate90 = 1 << 2, SurfaceRotate90 = 1 << 2,
SurfaceRotate180 = 1 << 3, SurfaceRotate180 = 1 << 3,
SurfaceRotate270 = 1 << 4, 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,
// Fragment arithmetic may run at relaxed (fp16) precision. Only requested for draws
// where every sampled texture and every colour attachment is an 8-bit-or-less
// normalized format, so nothing the shader reads or writes carries more precision
// than fp16 already represents exactly.
RelaxedFragmentPrecision = 1 << 6,
}; };
using CompileOptionFlags = Flags<CompileOptionBit>; using CompileOptionFlags = Flags<CompileOptionBit>;
using HashType = Uint64; using HashType = Uint64;
@@ -247,6 +247,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_surfaceFormat = {createInfo.imageFormat, createInfo.imageColorSpace}; m_surfaceFormat = {createInfo.imageFormat, createInfo.imageColorSpace};
m_extent = createInfo.imageExtent; m_extent = createInfo.imageExtent;
// The surface-space extent this swapchain was built from, i.e. before the
// quarter-turn swap above. Out-of-date checks must compare in THIS space: comparing a
// freshly queried currentExtent against the swapped m_extent flips axes every rotation
// and makes the comparison alternate forever.
m_surfaceExtent = defaultFramebufferExtent;
m_preTransform = createInfo.preTransform; m_preTransform = createInfo.preTransform;
VK_VERIFY(vkCreateSwapchainKHR(device, &createInfo, nullptr, &m_swapchain)); VK_VERIFY(vkCreateSwapchainKHR(device, &createInfo, nullptr, &m_swapchain));
@@ -35,6 +35,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSwapchainKHR GetHandle() const { return m_swapchain; } VkSwapchainKHR GetHandle() const { return m_swapchain; }
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; } const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
VkExtent2D GetExtent() const { return m_extent; } VkExtent2D GetExtent() const { return m_extent; }
// Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was
// created from - the value to compare a freshly queried currentExtent against.
VkExtent2D GetSurfaceExtent() const { return m_surfaceExtent; }
VkSurfaceTransformFlagBitsKHR GetPreTransform() const { return m_preTransform; } VkSurfaceTransformFlagBitsKHR GetPreTransform() const { return m_preTransform; }
const Vector<VkImage>& GetImages() const { return m_images; } const Vector<VkImage>& GetImages() const { return m_images; }
const Vector<VkImageView>& GetImageViews() const { return m_imageViews; } const Vector<VkImageView>& GetImageViews() const { return m_imageViews; }
@@ -63,6 +66,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE; VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
VkSurfaceFormatKHR m_surfaceFormat{}; VkSurfaceFormatKHR m_surfaceFormat{};
VkExtent2D m_extent{}; VkExtent2D m_extent{};
VkExtent2D m_surfaceExtent{};
VkSurfaceTransformFlagBitsKHR m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; VkSurfaceTransformFlagBitsKHR m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
Vector<VkImage> m_images; Vector<VkImage> m_images;
Vector<VkImageView> m_imageViews; Vector<VkImageView> m_imageViews;
@@ -16,6 +16,7 @@
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h" #include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h" #include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include <vulkan/utility/vk_format_utils.h>
#include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/Metrics/TextureMetrics.h"
#include <Config.h> #include <Config.h>
#include <cstdio> #include <cstdio>
@@ -384,24 +385,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Uint16 samplerVersion = samplerToUse->GetVersion(); const Uint16 samplerVersion = samplerToUse->GetVersion();
const Uint64 textureLifetimeId = texture->GetLifetimeId(); const Uint64 textureLifetimeId = texture->GetLifetimeId();
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion(); 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 && if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion &&
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion && memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion &&
memo.forceNearestFiltering == forceNearestFiltering) { memo.forceNearestFiltering == forceNearestFiltering && memo.viewLevelCount == viewLevelCount) {
resolvedSampler = memo.sampler; resolvedSampler = memo.sampler;
} else { } else {
resolvedSampler = resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture,
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering); forceNearestFiltering, viewLevelCount);
memo.samplerLifetimeId = samplerLifetimeId; memo.samplerLifetimeId = samplerLifetimeId;
memo.samplerVersion = samplerVersion; memo.samplerVersion = samplerVersion;
memo.textureLifetimeId = textureLifetimeId; memo.textureLifetimeId = textureLifetimeId;
memo.textureParamsVersion = textureParamsVersion; memo.textureParamsVersion = textureParamsVersion;
memo.forceNearestFiltering = forceNearestFiltering; memo.forceNearestFiltering = forceNearestFiltering;
memo.viewLevelCount = viewLevelCount;
memo.sampler = resolvedSampler; memo.sampler = resolvedSampler;
memo.valid = true; memo.valid = true;
} }
} else { } else {
resolvedSampler = resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering,
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering); resource->sampledLevelCount);
} }
outImageInfo = { outImageInfo = {
.sampler = resolvedSampler, .sampler = resolvedSampler,
@@ -442,6 +447,106 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return outImageInfo.sampler != VK_NULL_HANDLE; return outImageInfo.sampler != VK_NULL_HANDLE;
} }
namespace {
// fp16 carries an 11-bit mantissa, so an 8-bit normalized channel round-trips exactly.
// Anything wider - 16-bit normalized, half float, full float, and every packed HDR
// encoding - holds precision or range that relaxing the arithmetic would throw away.
Bool IsLowPrecisionNormalizedFormat(VkFormat format) {
if (format == VK_FORMAT_UNDEFINED) return false;
if (!vkuFormatIsUNORM(format) && !vkuFormatIsSNORM(format) && !vkuFormatIsSRGB(format)) {
return false;
}
const struct VKU_FORMAT_INFO info = vkuGetFormatInfo(format);
for (Uint32 i = 0; i < info.component_count; ++i) {
if (info.components[i].size > 8) return false;
}
return info.component_count > 0;
}
} // namespace
Bool UniformManager::DrawTargetIsLowPrecision(const MG_State::GLState::FramebufferObject* drawFramebuffer) {
// Default framebuffer: the swapchain is an 8-bit normalized surface.
if (drawFramebuffer == nullptr) return true;
Bool sawColour = false;
for (Int i = static_cast<Int>(FramebufferAttachmentType::Color0);
i < static_cast<Int>(FramebufferAttachmentType::FramebufferAttachmentTypeCount);
++i) {
const auto& attachment =
drawFramebuffer->GetAttachment(static_cast<FramebufferAttachmentType>(i));
VkFormat format = VK_FORMAT_UNDEFINED;
if (const auto& texture = attachment.GetTexture()) {
format = MG_Util::ConvertTextureInternalFormatToVkEnum(texture->GetFormat());
} else if (const auto& renderbuffer = attachment.GetRenderbuffer()) {
format = MG_Util::ConvertTextureInternalFormatToVkEnum(
renderbuffer->GetInternalFormat());
} else {
continue;
}
if (!IsLowPrecisionNormalizedFormat(format)) return false;
sawColour = true;
}
return sawColour;
}
Bool UniformManager::ProgramSamplesOnlyLowPrecisionTextures(
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) {
for (Uint32 binding = 0; binding < programObj.bindingKinds.size(); ++binding) {
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
continue;
}
const auto* texture = ResolveSamplerTextureRaw(program, programObj, binding);
// An unresolvable binding is unknown territory, not licence to relax.
if (texture == nullptr) return false;
const VkFormat format =
MG_Util::ConvertTextureInternalFormatToVkEnum(texture->GetFormat());
if (!IsLowPrecisionNormalizedFormat(format)) return false;
}
return true;
}
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, Bool UniformManager::ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) { SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
@@ -69,6 +69,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat, static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
VkFormat resourceFormat, Bool useBindingFormat); 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.
// True when every texture this program samples is an 8-bit-or-less normalized format, so
// relaxing the fragment stage to fp16 cannot lose a bit the texel ever carried. Says
// nothing about the render target - the caller must check that too.
static Bool ProgramSamplesOnlyLowPrecisionTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
// True when every colour attachment the draw writes is an 8-bit-or-less normalized
// format (nullptr = default framebuffer, which is). Blending happens at attachment
// precision, so a wider target must keep the fragment stage at full precision.
static Bool DrawTargetIsLowPrecision(const MG_State::GLState::FramebufferObject* drawFramebuffer);
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
private: private:
struct DescriptorPoolBucket { struct DescriptorPoolBucket {
VkDescriptorPool handle = VK_NULL_HANDLE; VkDescriptorPool handle = VK_NULL_HANDLE;
@@ -191,6 +210,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 samplerLifetimeId = 0; Uint64 samplerLifetimeId = 0;
Uint64 textureLifetimeId = 0; Uint64 textureLifetimeId = 0;
VkSampler sampler = VK_NULL_HANDLE; VkSampler sampler = VK_NULL_HANDLE;
Uint32 viewLevelCount = 0;
Uint16 samplerVersion = 0; Uint16 samplerVersion = 0;
Uint16 textureParamsVersion = 0; Uint16 textureParamsVersion = 0;
Bool forceNearestFiltering = false; Bool forceNearestFiltering = false;
@@ -51,6 +51,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) { Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
return std::min(sampler.GetMinLod(), 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 } // namespace
Bool VkSamplerManager::Initialize(const InitInfo& initInfo) { Bool VkSamplerManager::Initialize(const InitInfo& initInfo) {
@@ -120,11 +132,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture, 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"); MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion)); XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering))); XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView)));
const auto minFilter = sampler.GetMinFilter(); const auto minFilter = sampler.GetMinFilter();
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter))); 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))); XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
const auto wrapR = sampler.GetWrapR(); const auto wrapR = sampler.GetWrapR();
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR))); 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); const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod))); XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod))); 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, VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture, const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) { Bool forceNearestFiltering, Uint32 viewLevelCount) {
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering); // 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); auto it = m_samplers.find(key);
if (it != m_samplers.end()) { if (it != m_samplers.end()) {
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter; it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
@@ -172,7 +194,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter()); samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter());
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter()); samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter());
samplerInfo.mipmapMode = forceNearestFiltering ? VK_SAMPLER_MIPMAP_MODE_NEAREST samplerInfo.mipmapMode = (forceNearestFiltering || singleLevelView)
? VK_SAMPLER_MIPMAP_MODE_NEAREST
: ToVkMipmapMode(sampler.GetMipmapMode()); : ToVkMipmapMode(sampler.GetMipmapMode());
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS()); samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT()); samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
@@ -185,7 +208,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
samplerInfo.maxAnisotropy = maxAnisotropy; samplerInfo.maxAnisotropy = maxAnisotropy;
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE; samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture)); 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.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture); samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
samplerInfo.unnormalizedCoordinates = VK_FALSE; samplerInfo.unnormalizedCoordinates = VK_FALSE;
@@ -33,9 +33,12 @@ public:
Bool Initialize(const InitInfo& initInfo); Bool Initialize(const InitInfo& initInfo);
void Shutdown(); 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, VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture, 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 // Frame boundary hook: ages the sampler cache and destroys samplers not used
// for many frames. The key hashes continuous float state (lodBias, LOD clamps, // for many frames. The key hashes continuous float state (lodBias, LOD clamps,
// anisotropy), so an app animating those would otherwise mint an unbounded // anisotropy), so an app animating those would otherwise mint an unbounded
@@ -61,7 +64,7 @@ private:
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture, const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) const; Bool forceNearestFiltering, Bool singleLevelView) const;
static VkFilter ToVkFilter(SamplerFilterMode mode); static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode); static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode); static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
@@ -587,6 +587,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_allocator = initInfo.allocator; m_allocator = initInfo.allocator;
m_commandPool = initInfo.commandPool; m_commandPool = initInfo.commandPool;
m_graphicsQueue = initInfo.graphicsQueue; m_graphicsQueue = initInfo.graphicsQueue;
m_imageFormatListSupported = initInfo.imageFormatListSupported;
m_currentFrameIndex = 0; m_currentFrameIndex = 0;
m_deferredReleases.clear(); m_deferredReleases.clear();
m_deferredReleases.resize(initInfo.frameCount); m_deferredReleases.resize(initInfo.frameCount);
@@ -609,6 +610,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DestroyDeferredReleases(); DestroyDeferredReleases();
m_textureResources.clear(); m_textureResources.clear();
m_aliveObjects.clear(); m_aliveObjects.clear();
m_storageImageTextures.clear();
m_device = VK_NULL_HANDLE; m_device = VK_NULL_HANDLE;
m_physicalDevice = VK_NULL_HANDLE; m_physicalDevice = VK_NULL_HANDLE;
@@ -656,6 +658,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_textureResources.erase(resourceIt); m_textureResources.erase(resourceIt);
} }
m_aliveObjects.erase(identity); m_aliveObjects.erase(identity);
m_storageImageTextures.erase(identity);
} }
void VkTextureManager::PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture) { void VkTextureManager::PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture) {
@@ -1189,8 +1192,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ok; 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 { 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()) { if (it == m_textureResources.end()) {
return true; return true;
} }
@@ -1198,6 +1218,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (resource.image == VK_NULL_HANDLE || resource.layout != VK_IMAGE_LAYOUT_GENERAL) { if (resource.image == VK_NULL_HANDLE || resource.layout != VK_IMAGE_LAYOUT_GENERAL) {
return true; 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 // 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. // path may upload or rebuild, both of which need the render pass ended first.
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture); 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 auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
const Uint32 syncingMipLevelCount = const Uint32 syncingMipLevelCount =
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u; 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.syncedContentVersion == syncingContentVersion &&
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() && outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
outResource.syncedMipLevelCount == syncingMipLevelCount) { outResource.syncedMipLevelCount == syncingMipLevelCount) {
@@ -1415,16 +1447,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkImageAspectFlags aspect = GetAspectMaskForFormat(format); const VkImageAspectFlags aspect = GetAspectMaskForFormat(format);
VkFormatProperties formatProperties{}; VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &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 && !isMultisampleTexture &&
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 && (aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0; (formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
const Bool supportsStorageImage = storageImageCapable && markedAsStorageImage;
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags; VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
if (supportsStorageImage && IsMutableStorageImageFormat(format) && if (storageImageCapable && IsMutableStorageImageFormat(format) &&
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) { m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; 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 && const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
resource.extent.width == static_cast<Uint32>(texelSize.x()) && resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
resource.extent.height == static_cast<Uint32>(texelSize.y()) && resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
@@ -1433,6 +1493,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.viewType == shapeInfo.viewType && resource.viewType == shapeInfo.viewType &&
resource.sampleCount == resolvedSampleCount && resource.sampleCount == resolvedSampleCount &&
resource.imageCreateFlags == imageCreateFlags && resource.imageCreateFlags == imageCreateFlags &&
resource.usageFlags == desiredUsage &&
resource.mipLevels == backingMipLevels; resource.mipLevels == backingMipLevels;
if (compatible) { if (compatible) {
if (resource.perMipViews.size() != backingMipLevels) { if (resource.perMipViews.size() != backingMipLevels) {
@@ -1441,6 +1502,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (resource.perMipSampledViews.size() != backingMipLevels) { if (resource.perMipSampledViews.size() != backingMipLevels) {
resource.perMipSampledViews.resize(backingMipLevels, VK_NULL_HANDLE); 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; return true;
} }
@@ -1455,7 +1520,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.sampleCount == resolvedSampleCount && resource.sampleCount == resolvedSampleCount &&
resource.imageCreateFlags == imageCreateFlags && resource.imageCreateFlags == imageCreateFlags &&
resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT && 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; resource.layout != VK_IMAGE_LAYOUT_UNDEFINED;
std::unique_ptr<TextureResource> preservedResource; std::unique_ptr<TextureResource> preservedResource;
@@ -1477,16 +1545,37 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfo.format = format; imageInfo.format = format;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | imageInfo.usage = desiredUsage;
(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.samples = resolvedSampleCount; 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) { if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
VkImageFormatProperties imageFormatProperties{}; VkImageFormatProperties imageFormatProperties{};
VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties( VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
@@ -1543,6 +1632,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.viewType = shapeInfo.viewType; resource.viewType = shapeInfo.viewType;
resource.sampleCount = resolvedSampleCount; resource.sampleCount = resolvedSampleCount;
resource.imageCreateFlags = imageCreateFlags; resource.imageCreateFlags = imageCreateFlags;
resource.usageFlags = imageInfo.usage;
resource.storageUsageResolved = markedAsStorageImage;
resource.syncedTextureParamsVersion = 0; resource.syncedTextureParamsVersion = 0;
if (preservedResource) { if (preservedResource) {
@@ -53,6 +53,9 @@ public:
VkCommandPool commandPool = VK_NULL_HANDLE; VkCommandPool commandPool = VK_NULL_HANDLE;
VkQueue graphicsQueue = VK_NULL_HANDLE; VkQueue graphicsQueue = VK_NULL_HANDLE;
Uint32 frameCount = 0; 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 { struct TextureResource {
@@ -157,6 +160,17 @@ public:
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D; VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
VkImageCreateFlags imageCreateFlags = 0; 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; Uint16 syncedTextureParamsVersion = 0;
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync; // Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged. // 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->viewType, that.viewType);
std::swap(this->sampleCount, that.sampleCount); std::swap(this->sampleCount, that.sampleCount);
std::swap(this->imageCreateFlags, that.imageCreateFlags); 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->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
std::swap(this->syncedContentVersion, that.syncedContentVersion); std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
@@ -251,6 +267,8 @@ public:
viewType = VK_IMAGE_VIEW_TYPE_2D; viewType = VK_IMAGE_VIEW_TYPE_2D;
sampleCount = VK_SAMPLE_COUNT_1_BIT; sampleCount = VK_SAMPLE_COUNT_1_BIT;
imageCreateFlags = 0; imageCreateFlags = 0;
usageFlags = 0;
storageUsageResolved = false;
syncedTextureParamsVersion = 0; syncedTextureParamsVersion = 0;
syncedContentVersion = 0; syncedContentVersion = 0;
syncedMipLevelCount = 0; syncedMipLevelCount = 0;
@@ -289,6 +307,17 @@ public:
VkImageLayout newLayout); VkImageLayout newLayout);
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
Bool TransitionTextureForStorageImage(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 // 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 // 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 // creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
@@ -375,6 +404,7 @@ private:
VmaAllocator m_allocator = nullptr; VmaAllocator m_allocator = nullptr;
VkCommandPool m_commandPool = VK_NULL_HANDLE; VkCommandPool m_commandPool = VK_NULL_HANDLE;
VkQueue m_graphicsQueue = VK_NULL_HANDLE; VkQueue m_graphicsQueue = VK_NULL_HANDLE;
Bool m_imageFormatListSupported = false;
Uint32 m_currentFrameIndex = 0; Uint32 m_currentFrameIndex = 0;
Uint8 m_gcCounter = 0; Uint8 m_gcCounter = 0;
@@ -398,6 +428,8 @@ private:
std::unordered_set<VkFormat> m_mutableFormatUnsupported; std::unordered_set<VkFormat> m_mutableFormatUnsupported;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects; std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources; 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<TextureResource>> m_deferredReleases;
Vector<Vector<VkImageView>> m_deferredViewReleases; Vector<Vector<VkImageView>> m_deferredViewReleases;
}; };
@@ -1016,7 +1016,18 @@ layout(location = 0) in vec2 vTexCoord;
layout(location = 0) out vec4 outColor; layout(location = 0) out vec4 outColor;
void main() { void main() {
outColor = texture(uSource, vTexCoord); // Explicit LOD, not texture(): a blit reads exactly the selected level, so
// derivative-based mip selection has no business here. It is also load-bearing:
// on Adreno 650 (driver 512.502) an implicit-LOD sample of this single-mip
// UBWC render target through the pre-rotation (ROTATE_90) mapping reads past
// the image's allocation - despite the sampler's maxLod=0 and a nominal 1:1
// texel mapping whose LOD is 0, so the driver's implicit-LOD path itself is at
// fault - and page-faults the GPU once the neighbouring memory is returned to
// the kernel (frame 2 of Minecraft 26.2's resource reload; the kernel then
// invalidates the context and the next submit dies with EDEADLK ->
// VK_ERROR_DEVICE_LOST at Present). Verified on device: texture() faults on
// the second frame every run, textureLod survives with identical state.
outColor = textureLod(uSource, vTexCoord, 0.0);
} }
)"; )";
@@ -2480,7 +2491,7 @@ void main() {
MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed."); MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed.");
succeeded = m_textureManager->Initialize( succeeded = m_textureManager->Initialize(
{m_device, m_physicalDevice.handle, m_allocator, m_commandPool, m_graphicsQueue, {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."); MOBILEGL_ASSERT(succeeded, "VkTextureManager initialization failed.");
m_clearManager = MakeUnique<VkClearManager>(); m_clearManager = MakeUnique<VkClearManager>();
MOBILEGL_ASSERT(m_clearManager != nullptr, "VkClearManager creation failed."); MOBILEGL_ASSERT(m_clearManager != nullptr, "VkClearManager creation failed.");
@@ -2563,11 +2574,20 @@ void main() {
if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE) { if (m_swapchainObject.GetHandle() != VK_NULL_HANDLE) {
VkResult acquireResult = VkResult acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR || acquireResult == VK_SUBOPTIMAL_KHR) { if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR) {
// Nothing was acquired and no semaphore signal was armed, so
// rebuilding and re-acquiring on the same semaphore is safe.
MGLOG_D("Initialize, vkAcquireNextImageKHR got %d, recreating swapchain", acquireResult); MGLOG_D("Initialize, vkAcquireNextImageKHR got %d, recreating swapchain", acquireResult);
RecreateSwapchain(); RecreateSwapchain();
acquireResult = acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
} else if (acquireResult == VK_SUBOPTIMAL_KHR) {
// The image is usable, and its acquire signal is already armed on
// imageAvailableSemaphore. Re-acquiring here would arm a second signal on a
// binary semaphore whose first one nobody has waited on yet; keep the image.
// Only a real surface change schedules a rebuild.
m_swapchainResizeRequested = m_swapchainResizeRequested || SwapchainIsOutOfDate();
acquireResult = VK_SUCCESS;
} }
VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage"); VK_VERIFY(acquireResult, "Initialize, WaitAndAcquireNextImage");
} else { } else {
@@ -2693,6 +2713,7 @@ void main() {
DestroyDebugMessenger(); DestroyDebugMessenger();
m_debugMessenger = VK_NULL_HANDLE; m_debugMessenger = VK_NULL_HANDLE;
} }
DestroyDebugReportCallback();
if (m_instance != VK_NULL_HANDLE) { if (m_instance != VK_NULL_HANDLE) {
vkDestroyInstance(m_instance, nullptr); vkDestroyInstance(m_instance, nullptr);
@@ -4172,7 +4193,7 @@ void main() {
} }
Bool VulkanRenderer::PrepareStorageImageTextures( Bool VulkanRenderer::PrepareStorageImageTextures(
VkCommandBuffer commandBuffer, FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj) { const ProgramFactory::VkProgramObject& programObj) {
if (!programObj.hasStorageImages) { if (!programObj.hasStorageImages) {
@@ -4193,9 +4214,18 @@ void main() {
// keep the render pass alive instead of splitting it on every storage-image draw (on // 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 // 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. // 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) { for (auto* texture : storageTextures) {
MOBILEGL_ASSERT(texture != nullptr, "%s: collected a null storage texture", __func__); 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) || if (m_textureManager->NeedsStorageImagePreparation(*texture) ||
m_clearManager->HasPendingClear(texture)) { m_clearManager->HasPendingClear(texture)) {
anyNeedsPreparation = true; anyNeedsPreparation = true;
@@ -4206,21 +4236,51 @@ void main() {
return true; 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 // 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 // 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 // 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. // same layout independent of SPIR-V reflection/binding order.
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(commandBuffer); VkRenderPassManager::EndRenderPass(frame.commandBuffer);
} }
for (auto* texture : storageTextures) { 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", MGLOG_E("%s: failed to materialize pending clear for storage textureId=%d",
__func__, texture->GetExternalIndex()); __func__, texture->GetExternalIndex());
return false; return false;
} }
if (!m_textureManager->TransitionTextureForStorageImage(commandBuffer, *texture)) { if (!m_textureManager->TransitionTextureForStorageImage(frame.commandBuffer, *texture)) {
MGLOG_E("%s: failed to prepare storage textureId=%d", MGLOG_E("%s: failed to prepare storage textureId=%d",
__func__, texture->GetExternalIndex()); __func__, texture->GetExternalIndex());
return false; return false;
@@ -4246,7 +4306,25 @@ void main() {
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray(); const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
const auto& program = *MG_State::pGLContext->GetCurrentProgram(); const auto& program = *MG_State::pGLContext->GetCurrentProgram();
ProgramFactory::CompileOptionFlags transformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform()); 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);
}
// fp16 fragment arithmetic is only sound when nothing this draw reads or writes carries
// more than 8 normalized bits per channel. A shaderpack's HDR gbuffer, or a data texture
// holding positions, must keep full precision - and SPIR-V cannot tell, since sampler2D
// yields vec4 whatever the bound format is, so the decision has to be made here.
if (UniformManager::ProgramSamplesOnlyLowPrecisionTextures(program, *programObjPtr) &&
UniformManager::DrawTargetIsLowPrecision(drawFbo.get())) {
transformFlags |= ProgramFactory::CompileOptionBit::RelaxedFragmentPrecision;
programObjPtr = &m_programFactory->GetOrCreateProgram(program, transformFlags);
}
const auto& programObj = *programObjPtr;
// Begin command recording if not yet // Begin command recording if not yet
if (!frame.isCommandRecording) { if (!frame.isCommandRecording) {
@@ -4256,7 +4334,7 @@ void main() {
m_lastSampledSetValid = false; m_lastSampledSetValid = false;
} }
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) { if (!PrepareStorageImageTextures(frame, program, programObj)) {
MGLOG_E("SetupDraw skipped: storage image preparation failed"); MGLOG_E("SetupDraw skipped: storage image preparation failed");
return false; return false;
} }
@@ -4481,7 +4559,7 @@ void main() {
VkRenderPassManager::EndRenderPass(frame.commandBuffer); VkRenderPassManager::EndRenderPass(frame.commandBuffer);
} }
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) { if (!PrepareStorageImageTextures(frame, program, programObj)) {
MGLOG_E("DispatchCompute skipped: storage image preparation failed"); MGLOG_E("DispatchCompute skipped: storage image preparation failed");
return; return;
} }
@@ -4521,7 +4599,7 @@ void main() {
VkRenderPassManager::EndRenderPass(frame.commandBuffer); VkRenderPassManager::EndRenderPass(frame.commandBuffer);
} }
if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) { if (!PrepareStorageImageTextures(frame, program, programObj)) {
MGLOG_E("DispatchComputeIndirect skipped: storage image preparation failed"); MGLOG_E("DispatchComputeIndirect skipped: storage image preparation failed");
return; return;
} }
@@ -7542,7 +7620,11 @@ void main() {
m_presentSuspended = false; m_presentSuspended = false;
const VkResult acquireResult = const VkResult acquireResult =
m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (acquireResult != VK_SUBOPTIMAL_KHR) { if (acquireResult == VK_SUBOPTIMAL_KHR) {
// Usable image with its acquire signal already armed; a rebuild is scheduled
// only if the surface genuinely no longer matches (see step 4 of Present).
m_swapchainResizeRequested = m_swapchainResizeRequested || SwapchainIsOutOfDate();
} else {
VK_VERIFY(acquireResult, "Present, deferred first WaitAndAcquireNextImage"); VK_VERIFY(acquireResult, "Present, deferred first WaitAndAcquireNextImage");
} }
} }
@@ -7570,16 +7652,24 @@ void main() {
if (activeRenderPass) if (activeRenderPass)
VkRenderPassManager::EndRenderPass(frame.commandBuffer); VkRenderPassManager::EndRenderPass(frame.commandBuffer);
// Transition while this frame's recording is still open. A frame that
// rendered only into FBOs has no default-framebuffer render pass, and that
// pass's finalLayout is the only other thing that carries the swapchain
// image to PRESENT_SRC_KHR - so closing the buffer first, which made
// TransitionToPresent refuse to record, handed the image to
// vkQueuePresentKHR in the layout it was acquired in (UNDEFINED on a fresh
// swapchain). The SetImageLayout below then made the tracker's
// disagreement with reality permanent for that image index.
const auto acquiredImageLayout = m_swapchainObject.GetImageLayout(m_imageIndexAcquired);
m_frameContext.TransitionToPresent(m_swapchainObject.GetImage(m_imageIndexAcquired), acquiredImageLayout);
if (frame.isCommandRecording) { if (frame.isCommandRecording) {
m_frameContext.EndCommandRecording(); m_frameContext.EndCommandRecording();
frame.hasCommandBufferRecorded = true; frame.hasCommandBufferRecorded = true;
m_lastPipelineValid = false; // command-buffer boundary: drop the pipeline memo m_lastPipelineValid = false; // command-buffer boundary: drop the pipeline memo
} }
const auto acquiredImageLayout = m_swapchainObject.GetImageLayout(m_imageIndexAcquired); const Bool shouldSubmitCommandBuffer = frame.hasCommandBufferRecorded;
const Bool needsLayoutTransitionForPresent =
m_frameContext.TransitionToPresent(m_swapchainObject.GetImage(m_imageIndexAcquired), acquiredImageLayout);
const Bool shouldSubmitCommandBuffer = frame.hasCommandBufferRecorded || needsLayoutTransitionForPresent;
// 1) Submit current frame work. // 1) Submit current frame work.
auto submitPacket = m_frameContext.GetSubmitInfo(shouldSubmitCommandBuffer, m_imageIndexAcquired); auto submitPacket = m_frameContext.GetSubmitInfo(shouldSubmitCommandBuffer, m_imageIndexAcquired);
@@ -7593,7 +7683,15 @@ void main() {
// 2) Present current frame. // 2) Present current frame.
auto presentPacket = m_frameContext.GetPresentInfo(m_swapchainObject.GetHandle(), m_imageIndexAcquired); auto presentPacket = m_frameContext.GetPresentInfo(m_swapchainObject.GetHandle(), m_imageIndexAcquired);
auto result = vkQueuePresentKHR(m_presentQueue, &presentPacket.presentInfo); auto result = vkQueuePresentKHR(m_presentQueue, &presentPacket.presentInfo);
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { if (result == VK_SUBOPTIMAL_KHR) {
// Suboptimal is not a reason to rebuild on its own: a driver may report it for a
// surface whose size and orientation still match what we built from (Android does
// this routinely), and rebuilding on it alone destroys every pipeline and
// reallocates the default framebuffer once per frame - flicker, then garbage.
// Defer to the surface-capabilities comparison below.
result = VK_SUCCESS;
}
if (result == VK_ERROR_OUT_OF_DATE_KHR) {
MGLOG_D("Present, vkQueuePresentKHR got %d, recreating swapchain", result); MGLOG_D("Present, vkQueuePresentKHR got %d, recreating swapchain", result);
if (!RecreateSwapchain()) { if (!RecreateSwapchain()) {
// Window went zero-area (minimize) with the swapchain out of date: // Window went zero-area (minimize) with the swapchain out of date:
@@ -7607,6 +7705,13 @@ void main() {
result = VK_SUCCESS; result = VK_SUCCESS;
} }
VK_VERIFY(result, "Present, vkQueuePresentKHR"); VK_VERIFY(result, "Present, vkQueuePresentKHR");
// The authoritative check, done here - after the frame is presented, before the next
// acquire. This is what makes a launcher-side resolution change take effect: shrinking
// the window's buffer (SurfaceHolder.setFixedSize) moves currentExtent, the swapchain
// follows, and the compositor scales the smaller image up to the view for free.
if (!m_swapchainResizeRequested && SwapchainIsOutOfDate()) {
m_swapchainResizeRequested = true;
}
if (m_swapchainResizeRequested) { if (m_swapchainResizeRequested) {
MGLOG_D("Present, processing requested swapchain resize"); MGLOG_D("Present, processing requested swapchain resize");
if (!RecreateSwapchain()) { if (!RecreateSwapchain()) {
@@ -7623,7 +7728,16 @@ void main() {
// 4) Wait/reset/acquire for next frame. // 4) Wait/reset/acquire for next frame.
result = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); result = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired);
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { if (result == VK_SUBOPTIMAL_KHR) {
// An image WAS acquired and its signal is armed on this slot's
// imageAvailableSemaphore, so the frame proceeds normally. Whether a rebuild is
// actually needed is decided by the surface-capabilities comparison at the next
// Present - suboptimal alone must not schedule one, or a driver that reports it
// every frame would rebuild every frame.
m_swapchainResizeRequested = m_swapchainResizeRequested || SwapchainIsOutOfDate();
result = VK_SUCCESS;
} else if (result == VK_ERROR_OUT_OF_DATE_KHR) {
// Nothing acquired, nothing signaled: safe to rebuild and re-acquire.
MGLOG_D("Present, vkAcquireNextImageKHR got %d, recreating swapchain", result); MGLOG_D("Present, vkAcquireNextImageKHR got %d, recreating swapchain", result);
if (!RecreateSwapchain()) { if (!RecreateSwapchain()) {
m_presentSuspended = true; m_presentSuspended = true;
@@ -7671,6 +7785,24 @@ void main() {
m_validationLayersEnabled = m_config.EnableValidationLayers && validationLayerAvailable; m_validationLayersEnabled = m_config.EnableValidationLayers && validationLayerAvailable;
// The debug messenger is a VK_EXT_debug_utils object, but a driver can ship
// the validation layers while exposing only the older VK_EXT_debug_report
// (Adreno 650 / Vulkan 1.1.128 does exactly that). Requesting the extension
// unconditionally tripped the required-extension assert below, aborting every
// validation-enabled build in CreateInstance. Keep the layers - they still
// validate, and on Android they report to logcat on their own - and drop only
// the messenger.
const Bool debugUtilsAvailable =
m_validationLayersEnabled && IsExtensionSupported(m_extensions, VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
// Without a reporting channel the layers validate but say nothing, so fall
// back to VK_EXT_debug_report when debug_utils is missing.
const Bool debugReportAvailable = m_validationLayersEnabled && !debugUtilsAvailable &&
IsExtensionSupported(m_extensions, VK_EXT_DEBUG_REPORT_EXTENSION_NAME);
if (m_validationLayersEnabled && !debugUtilsAvailable) {
MGLOG_I("%s not available; validation reports via %s instead.", VK_EXT_DEBUG_UTILS_EXTENSION_NAME,
debugReportAvailable ? VK_EXT_DEBUG_REPORT_EXTENSION_NAME : "(no channel)");
}
// ---------------- App info ------------------- // ---------------- App info -------------------
VkApplicationInfo appInfo = {}; VkApplicationInfo appInfo = {};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
@@ -7721,8 +7853,10 @@ void main() {
} }
#endif #endif
if (m_validationLayersEnabled) { if (debugUtilsAvailable) {
exts.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); exts.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
} else if (debugReportAvailable) {
exts.push_back(VK_EXT_DEBUG_REPORT_EXTENSION_NAME);
} }
MGLOG_I("Enabling %d Vulkan instance extensions:", exts.size()); MGLOG_I("Enabling %d Vulkan instance extensions:", exts.size());
@@ -7747,7 +7881,8 @@ void main() {
MGLOG_I("Enabling validation layer..."); MGLOG_I("Enabling validation layer...");
instanceInfo.enabledLayerCount = static_cast<uint32_t>(std::size(s_validationLayerNames)); instanceInfo.enabledLayerCount = static_cast<uint32_t>(std::size(s_validationLayerNames));
instanceInfo.ppEnabledLayerNames = s_validationLayerNames; instanceInfo.ppEnabledLayerNames = s_validationLayerNames;
instanceInfo.pNext = &debugMessengerCreateInfo; // Chaining the messenger create-info is only legal with the extension on.
instanceInfo.pNext = debugUtilsAvailable ? &debugMessengerCreateInfo : nullptr;
} else { } else {
instanceInfo.enabledLayerCount = 0; instanceInfo.enabledLayerCount = 0;
instanceInfo.pNext = nullptr; instanceInfo.pNext = nullptr;
@@ -7755,7 +7890,40 @@ void main() {
VK_VERIFY(vkCreateInstance(&instanceInfo, nullptr, &m_instance), "vkCreateInstance failed"); VK_VERIFY(vkCreateInstance(&instanceInfo, nullptr, &m_instance), "vkCreateInstance failed");
if (m_validationLayersEnabled) VK_VERIFY(SetupDebugMessenger()); if (debugUtilsAvailable) {
VK_VERIFY(SetupDebugMessenger());
} else if (debugReportAvailable) {
VK_VERIFY(SetupDebugReportCallback());
}
}
static VKAPI_ATTR VkBool32 VKAPI_CALL DebugReportCallback(VkDebugReportFlagsEXT flags, VkDebugReportObjectTypeEXT,
Uint64, size_t, Int32 messageCode, const char* pLayerPrefix,
const char* pMessage, void*) {
if ((flags & (VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT |
VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT)) != 0) {
MGLOG_F("[Vulkan %s %d] %s", pLayerPrefix ? pLayerPrefix : "?", messageCode, pMessage ? pMessage : "");
}
return VK_FALSE;
}
VkResult VulkanRenderer::SetupDebugReportCallback() {
auto vkCreateDebugReportCallbackEXT =
(PFN_vkCreateDebugReportCallbackEXT)vkGetInstanceProcAddr(m_instance, "vkCreateDebugReportCallbackEXT");
if (!vkCreateDebugReportCallbackEXT) return VK_ERROR_EXTENSION_NOT_PRESENT;
VkDebugReportCallbackCreateInfoEXT createInfo{VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT};
createInfo.flags = VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT |
VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT;
createInfo.pfnCallback = &DebugReportCallback;
return vkCreateDebugReportCallbackEXT(m_instance, &createInfo, nullptr, &m_debugReportCallback);
}
void VulkanRenderer::DestroyDebugReportCallback() {
if (m_debugReportCallback == VK_NULL_HANDLE) return;
auto func = (PFN_vkDestroyDebugReportCallbackEXT)vkGetInstanceProcAddr(m_instance,
"vkDestroyDebugReportCallbackEXT");
if (func != nullptr) func(m_instance, m_debugReportCallback, nullptr);
m_debugReportCallback = VK_NULL_HANDLE;
} }
VkResult VulkanRenderer::SetupDebugMessenger() { VkResult VulkanRenderer::SetupDebugMessenger() {
@@ -8037,6 +8205,18 @@ void main() {
const Vector<VkExtensionProperties> availableExtensions = EnumerateDeviceExtensions(m_physicalDevice.handle); const Vector<VkExtensionProperties> availableExtensions = EnumerateDeviceExtensions(m_physicalDevice.handle);
ResolveOptionalDeviceExtensions(availableExtensions, enabledDeviceExtensions); 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"); MGLOG_I("VK_KHR_draw_indirect_count enabled: %s", m_drawIndirectCountExtensionEnabled ? "true" : "false");
m_indexTypeUint8ExtensionEnabled = false; m_indexTypeUint8ExtensionEnabled = false;
@@ -8501,6 +8681,38 @@ void main() {
return m_physicalDevice; return m_physicalDevice;
} }
Bool VulkanRenderer::SwapchainIsOutOfDate() {
if (m_surface == VK_NULL_HANDLE || m_swapchainObject.GetHandle() == VK_NULL_HANDLE) {
return false;
}
VkSurfaceCapabilitiesKHR surfaceCaps{};
if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physicalDevice.handle, m_surface, &surfaceCaps) !=
VK_SUCCESS) {
return false;
}
// A driver-defined currentExtent (UINT32_MAX) means the surface takes its size from the
// swapchain, so there is nothing to compare against - the app's requested size wins and
// only an explicit RequestSwapchainResize can change it.
if (surfaceCaps.currentExtent.width == UINT32_MAX || surfaceCaps.currentExtent.height == UINT32_MAX) {
return false;
}
// Compare in SURFACE space against the extent the live swapchain was created from. Using
// the swapchain's own (quarter-turn swapped) extent here would report a difference on
// every rotated frame and rebuild forever.
const VkExtent2D builtFrom = m_swapchainObject.GetSurfaceExtent();
const Bool extentChanged = surfaceCaps.currentExtent.width != builtFrom.width ||
surfaceCaps.currentExtent.height != builtFrom.height;
const Bool transformChanged = surfaceCaps.currentTransform != m_swapchainObject.GetPreTransform();
if (!extentChanged && !transformChanged) {
return false;
}
MGLOG_I("Swapchain out of date: surface %ux%u transform %u -> %ux%u transform %u",
builtFrom.width, builtFrom.height, static_cast<Uint32>(m_swapchainObject.GetPreTransform()),
surfaceCaps.currentExtent.width, surfaceCaps.currentExtent.height,
static_cast<Uint32>(surfaceCaps.currentTransform));
return true;
}
void VulkanRenderer::RequestSwapchainResize(Uint32 width, Uint32 height) { void VulkanRenderer::RequestSwapchainResize(Uint32 width, Uint32 height) {
width = std::max<Uint32>(width, 1); width = std::max<Uint32>(width, 1);
height = std::max<Uint32>(height, 1); height = std::max<Uint32>(height, 1);
@@ -274,6 +274,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const; Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
void RequestSwapchainResize(Uint32 width, Uint32 height); void RequestSwapchainResize(Uint32 width, Uint32 height);
// Re-query the surface and report whether the live swapchain no longer matches it
// (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
// rebuild, so a surface the driver merely considers suboptimal cannot thrash.
Bool SwapchainIsOutOfDate();
// Returns false when the surface is zero-area (minimized/hidden window): // Returns false when the surface is zero-area (minimized/hidden window):
// no new swapchain is installed and presentation must stay suspended. // no new swapchain is installed and presentation must stay suspended.
Bool RecreateSwapchain(); Bool RecreateSwapchain();
@@ -399,6 +403,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<VkExtensionProperties> m_extensions; Vector<VkExtensionProperties> m_extensions;
VkInstance m_instance = VK_NULL_HANDLE; VkInstance m_instance = VK_NULL_HANDLE;
VkDebugUtilsMessengerEXT m_debugMessenger = VK_NULL_HANDLE; VkDebugUtilsMessengerEXT m_debugMessenger = VK_NULL_HANDLE;
// Fallback reporting channel for drivers that ship the validation layers but
// only expose the older VK_EXT_debug_report (Adreno 650 / Vulkan 1.1.128).
VkDebugReportCallbackEXT m_debugReportCallback = VK_NULL_HANDLE;
PhysicalDevice m_physicalDevice; PhysicalDevice m_physicalDevice;
VkDevice m_device = VK_NULL_HANDLE; VkDevice m_device = VK_NULL_HANDLE;
VmaAllocator m_allocator = nullptr; VmaAllocator m_allocator = nullptr;
@@ -549,6 +556,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void CreateInstance(); void CreateInstance();
VkResult SetupDebugMessenger(); VkResult SetupDebugMessenger();
VkResult DestroyDebugMessenger(); VkResult DestroyDebugMessenger();
VkResult SetupDebugReportCallback();
void DestroyDebugReportCallback();
VkDebugUtilsMessengerCreateInfoEXT PopulateDebugMessengerCreateInfo(); VkDebugUtilsMessengerCreateInfoEXT PopulateDebugMessengerCreateInfo();
void CreateSurface(); void CreateSurface();
void PickPhysicalDevice(); void PickPhysicalDevice();
@@ -567,8 +576,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const RenderPassEntry& renderPassEntry); const RenderPassEntry& renderPassEntry);
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj); VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
void DestroyComputePipelines(); 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( Bool PrepareStorageImageTextures(
VkCommandBuffer commandBuffer, FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj); const ProgramFactory::VkProgramObject& programObj);
@@ -630,6 +641,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const PhysicalDevice& compareWithDevice, const PhysicalDevice& compareWithDevice,
PhysicalDevice& outBetterDevice); PhysicalDevice& outBetterDevice);
static constexpr const char* s_validationLayerNames[] = {"VK_LAYER_KHRONOS_validation"}; 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 constexpr const char* s_deviceExtensionNames[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
static Bool CheckValidationLayerSupport(); static Bool CheckValidationLayerSupport();
+33
View File
@@ -24,6 +24,7 @@ namespace MobileGL::MG_Impl::CGLImpl {
GLint Samples = 0; GLint Samples = 0;
GLint Profile = kCGLOGLPVersion_3_2_Core; GLint Profile = kCGLOGLPVersion_3_2_Core;
GLint RendererId = 0x4d474c; GLint RendererId = 0x4d474c;
GLint DisplayMask = 0;
}; };
struct ContextObject { struct ContextObject {
@@ -134,6 +135,9 @@ namespace MobileGL::MG_Impl::CGLImpl {
case kCGLPFARendererID: case kCGLPFARendererID:
pixelFormat.RendererId = value; pixelFormat.RendererId = value;
break; break;
case kCGLPFADisplayMask:
pixelFormat.DisplayMask = value;
break;
default: default:
break; break;
} }
@@ -343,6 +347,9 @@ namespace MobileGL::MG_Impl::CGLImpl {
case kCGLPFARendererID: case kCGLPFARendererID:
*value = pixelFormat->RendererId; *value = pixelFormat->RendererId;
return kCGLNoError; return kCGLNoError;
case kCGLPFADisplayMask:
*value = pixelFormat->DisplayMask;
return kCGLNoError;
case kCGLPFAOpenGLProfile: case kCGLPFAOpenGLProfile:
*value = pixelFormat->Profile; *value = pixelFormat->Profile;
return kCGLNoError; return kCGLNoError;
@@ -481,6 +488,32 @@ namespace MobileGL::MG_Impl::CGLImpl {
return it == currentContexts.end() ? nullptr : it->second; return it == currentContexts.end() ? nullptr : it->second;
} }
CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
if (screen != 0) {
return kCGLBadValue;
}
object->VirtualScreen = screen;
return kCGLNoError;
}
CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
if (!screen) {
return kCGLBadAddress;
}
*screen = object->VirtualScreen;
return kCGLNoError;
}
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) { CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex()); const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx); auto* object = TryGetContext(ctx);
+2
View File
@@ -32,6 +32,8 @@ namespace MobileGL::MG_Impl::CGLImpl {
CGLError SetCurrentContext(CGLContextObj ctx); CGLError SetCurrentContext(CGLContextObj ctx);
CGLContextObj GetCurrentContext(); CGLContextObj GetCurrentContext();
CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen);
CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen);
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params); CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params);
CGLError GetParameter(CGLContextObj ctx, CGLContextParameter pname, GLint* params); CGLError GetParameter(CGLContextObj ctx, CGLContextParameter pname, GLint* params);
CGLError UpdateContext(CGLContextObj ctx); CGLError UpdateContext(CGLContextObj ctx);
@@ -71,6 +71,14 @@ MOBILEGL_CGL_API CGLContextObj CGLGetCurrentContext(void) {
return MobileGL::MG_Impl::CGLImpl::GetCurrentContext(); return MobileGL::MG_Impl::CGLImpl::GetCurrentContext();
} }
MOBILEGL_CGL_API CGLError CGLSetVirtualScreen(CGLContextObj ctx, GLint screen) {
return MobileGL::MG_Impl::CGLImpl::SetVirtualScreen(ctx, screen);
}
MOBILEGL_CGL_API CGLError CGLGetVirtualScreen(CGLContextObj ctx, GLint* screen) {
return MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(ctx, screen);
}
MOBILEGL_CGL_API CGLError CGLSetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) { MOBILEGL_CGL_API CGLError CGLSetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
return MobileGL::MG_Impl::CGLImpl::SetParameter(ctx, pname, params); return MobileGL::MG_Impl::CGLImpl::SetParameter(ctx, pname, params);
} }
@@ -10,8 +10,12 @@
#if defined(__APPLE__) #if defined(__APPLE__)
#include "MG_Impl/CGLImpl/CGLImpl.h"
#include "MG_Impl/GetProcAddress.h" #include "MG_Impl/GetProcAddress.h"
#include <CoreGraphics/CoreGraphics.h>
#include <CoreVideo/CVDisplayLink.h>
#include <cstdint>
#include <dlfcn.h> #include <dlfcn.h>
namespace { namespace {
@@ -47,10 +51,52 @@ namespace {
return dlsym(handle, symbol); return dlsym(handle, symbol);
} }
CGDirectDisplayID DisplayForMask(GLint displayMask) {
constexpr std::uint32_t MaxDisplays = sizeof(CGOpenGLDisplayMask) * 8;
CGDirectDisplayID displays[MaxDisplays] = {};
std::uint32_t displayCount = 0;
if (displayMask != 0 &&
CGGetActiveDisplayList(MaxDisplays, displays, &displayCount) == kCGErrorSuccess) {
const auto mask = static_cast<CGOpenGLDisplayMask>(displayMask);
for (std::uint32_t i = 0; i < displayCount; ++i) {
if ((CGDisplayIDToOpenGLDisplayMask(displays[i]) & mask) != 0) {
return displays[i];
}
}
}
return CGMainDisplayID();
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
CVReturn MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(
CVDisplayLinkRef displayLink,
CGLContextObj context,
CGLPixelFormatObj pixelFormat) {
GLint virtualScreen = 0;
if (MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(context, &virtualScreen) == kCGLNoError) {
GLint displayMask = 0;
if (!displayLink ||
MobileGL::MG_Impl::CGLImpl::DescribePixelFormat(
pixelFormat, virtualScreen, kCGLPFADisplayMask, &displayMask) != kCGLNoError) {
return kCVReturnInvalidArgument;
}
return CVDisplayLinkSetCurrentCGDisplay(displayLink, DisplayForMask(displayMask));
}
using OriginalFunction = CVReturn (*)(CVDisplayLinkRef, CGLContextObj, CGLPixelFormatObj);
static const auto original = reinterpret_cast<OriginalFunction>(
dlsym(RTLD_NEXT, "CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext"));
return original ? original(displayLink, context, pixelFormat) : kCVReturnError;
}
__attribute__((used)) static const DyldInterposeEntry kMobileGLDyldInterpose[] __attribute__((used)) static const DyldInterposeEntry kMobileGLDyldInterpose[]
__attribute__((section("__DATA,__interpose"))) = { __attribute__((section("__DATA,__interpose"))) = {
{reinterpret_cast<const void*>(MobileGLDlsym), reinterpret_cast<const void*>(dlsym)}, {reinterpret_cast<const void*>(MobileGLDlsym), reinterpret_cast<const void*>(dlsym)},
{reinterpret_cast<const void*>(MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext),
reinterpret_cast<const void*>(CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext)},
}; };
#pragma clang diagnostic pop
} // namespace } // namespace
#endif #endif
@@ -0,0 +1,10 @@
# Public CGL entry points.
_CGL*
# Public EGL entry points.
_egl*
# Public OpenGL and GLX entry points. OpenGL function names always use an
# uppercase letter or digit after the "gl" prefix; excluding lowercase here
# deliberately prevents glslang_* from matching this pattern.
_gl[A-Z0-9]*
+2
View File
@@ -85,6 +85,8 @@ namespace MobileGL::MG_Impl {
GETPROC(CGLGetPixelFormat, name); GETPROC(CGLGetPixelFormat, name);
GETPROC(CGLSetCurrentContext, name); GETPROC(CGLSetCurrentContext, name);
GETPROC(CGLGetCurrentContext, name); GETPROC(CGLGetCurrentContext, name);
GETPROC(CGLSetVirtualScreen, name);
GETPROC(CGLGetVirtualScreen, name);
GETPROC(CGLSetParameter, name); GETPROC(CGLSetParameter, name);
GETPROC(CGLGetParameter, name); GETPROC(CGLGetParameter, name);
GETPROC(CGLUpdateContext, name); GETPROC(CGLUpdateContext, name);
+36 -4
View File
@@ -29,10 +29,19 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
char kContextViewKey; char kContextViewKey;
char kContextLayerKey; char kContextLayerKey;
std::once_flag g_installOnce;
IMP g_pixelFormatDealloc = nullptr; IMP g_pixelFormatDealloc = nullptr;
IMP g_contextDealloc = nullptr; IMP g_contextDealloc = nullptr;
std::mutex& HookInstallMutex() {
static auto* mutex = new std::mutex();
return *mutex;
}
Bool& HooksInstalled() {
static auto* installed = new Bool(false);
return *installed;
}
template <typename Fn> template <typename Fn>
Fn ObjcMsgSend() { Fn ObjcMsgSend() {
return reinterpret_cast<Fn>(objc_msgSend); return reinterpret_cast<Fn>(objc_msgSend);
@@ -431,12 +440,12 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
method_setImplementation(method, replacement); method_setImplementation(method, replacement);
} }
void InstallHooksOnce() { Bool InstallHooksOnce() {
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat"); Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
Class contextClass = objc_getClass("NSOpenGLContext"); Class contextClass = objc_getClass("NSOpenGLContext");
if (!pixelFormatClass || !contextClass) { if (!pixelFormatClass || !contextClass) {
MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed"); MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
return; return false;
} }
ReplaceInstanceMethod(pixelFormatClass, "initWithAttributes:", ReplaceInstanceMethod(pixelFormatClass, "initWithAttributes:",
@@ -471,11 +480,34 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
ReplaceInstanceMethod(contextClass, "dealloc", reinterpret_cast<IMP>(ContextDealloc), &g_contextDealloc); ReplaceInstanceMethod(contextClass, "dealloc", reinterpret_cast<IMP>(ContextDealloc), &g_contextDealloc);
MGLOG_I("NSOpenGLImpl hooks installed"); MGLOG_I("NSOpenGLImpl hooks installed");
return true;
} }
} // namespace } // namespace
void InstallHooks() { void InstallHooks() {
std::call_once(g_installOnce, InstallHooksOnce); const std::lock_guard<std::mutex> lock(HookInstallMutex());
if (!HooksInstalled()) {
// Do not permanently consume the install attempt when the OpenGL
// framework has not registered its Objective-C classes yet. The
// dyld bootstrap normally runs after framework dependencies, but
// an explicitly loaded/static-linked MobileGL can arrive earlier.
HooksInstalled() = InstallHooksOnce();
}
} }
} // namespace MobileGL::MG_Impl::NSOpenGLImpl } // namespace MobileGL::MG_Impl::NSOpenGLImpl
namespace {
// SDL's Cocoa backend creates NSOpenGLPixelFormat/NSOpenGLContext before
// its first dlsym("glGetString") or other MobileGL host-API call. Install
// only the lightweight Objective-C dispatch hooks while the injected dylib
// is loading so those first Cocoa objects are routed through CGLImpl. The
// hooked context constructor reaches EGLImpl::GetDisplay(), which performs
// the full, thread-safe MobileGL initialization outside this bootstrap.
//
// There is intentionally no matching destructor: backend teardown remains
// owned by the EGL lifecycle and process-exit globals remain leak-at-exit.
__attribute__((constructor)) void BootstrapNSOpenGLHooks() {
MobileGL::MG_Impl::NSOpenGLImpl::InstallHooks();
}
} // namespace
#endif #endif
@@ -334,16 +334,32 @@ namespace MobileGL::MG_State::GLState {
// draw. The memo is keyed by (backendStateVersion, flags); ResetLinkArtifacts and // draw. The memo is keyed by (backendStateVersion, flags); ResetLinkArtifacts and
// the binding setters below invalidate it by bumping m_backendStateVersion. // the binding setters below invalidate it by bumping m_backendStateVersion.
Bool GetBackendHashMemo(Uint flags, Uint64& outHash) const { Bool GetBackendHashMemo(Uint flags, Uint64& outHash) const {
if (m_backendHashMemoVersion != m_backendStateVersion || m_backendHashMemoFlags != flags) { if (m_backendHashMemoVersion != m_backendStateVersion) return false;
return false; for (const auto& slot : m_backendHashMemoSlots) {
} if (slot.valid && slot.flags == flags) {
outHash = m_backendHashMemo; outHash = slot.hash;
return true; return true;
} }
}
return false;
}
void SetBackendHashMemo(Uint flags, Uint64 hash) const { void SetBackendHashMemo(Uint flags, Uint64 hash) const {
m_backendHashMemo = hash; if (m_backendHashMemoVersion != m_backendStateVersion) {
for (auto& slot : m_backendHashMemoSlots) slot.valid = false;
m_backendHashMemoVersion = m_backendStateVersion; m_backendHashMemoVersion = m_backendStateVersion;
m_backendHashMemoFlags = flags; 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) { void SetUniformSamplerOrImageUnitIndex(Uint location, Int unit) {
@@ -527,10 +543,19 @@ namespace MobileGL::MG_State::GLState {
Uint32 m_backendStateVersion = 0; Uint32 m_backendStateVersion = 0;
// Backend-owned content-hash memo (see GetBackendHashMemo): valid only while // Backend-owned content-hash memo (see GetBackendHashMemo): valid only while
// m_backendStateVersion and the compile flags match the recorded values. // m_backendStateVersion matches. Several slots, not one: a backend may resolve the same
mutable Uint64 m_backendHashMemo = 0; // 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 Uint32 m_backendHashMemoVersion = ~0u;
mutable Uint m_backendHashMemoFlags = 0;
Uint32 m_uboContentVersion = 0; Uint32 m_uboContentVersion = 0;
Uint32 m_linkVersion = 0; Uint32 m_linkVersion = 0;
}; };