mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 06:38:31 +09:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3223ecb14e | ||
|
|
fc4cd980f2 | ||
|
|
992d16267c | ||
|
|
0ea9e6de5f | ||
|
|
241ed377b4 | ||
|
|
bf312a4b67 |
@@ -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
@@ -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
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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]*
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user