mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Feat] (DirectVulkan): relax fragment precision where the bound formats allow it
- WIP, parked: measures 80.9 -> 94.8 fps on Adreno 650 / MC 26.2 (same scene, device cooled to 38-40C), but is NOT validated. Desktop GLSL carries no precision qualifiers, so every fragment value reaches the driver as fp32 while Adreno runs fp16 at twice the rate. - RelaxTextureDerivedPrecisionPass taints the values a fragment shader derives from built-in inputs and decorates everything else RelaxedPrecision. The taint direction matters: whitelisting outward from texture reads captures nothing, because MC multiplies every texel by an interpolated colour and a UBO value and one un-relaxed operand vetoes the expression - measured at 80.4 fps, i.e. no gain, both with and without varyings seeded. Precision-critical sources are few (gl_FragCoord cannot even hold a 3044-pixel x exactly), so tainting them and relaxing the rest is what actually pays. - SPIR-V cannot see the bound formats - sampler2D yields vec4 whether the texture is RGBA8 or RGBA32F - so the decision is made per draw and passed in as a compile option, the same shape ExplicitLod0Sampling already uses. RelaxedFragmentPrecision is only requested when every sampled texture and every colour attachment is an 8-bit-or-less normalized format, where fp16's 11-bit mantissa already carries the value exactly. Shaderpack HDR gbuffers, float data textures and 16-bit normalized targets therefore keep full precision, as do shaders that write gl_FragDepth or gl_SampleMask. - LocalMultiStoreElim runs first: glslang emits function-local variables, and a load can never be relaxed, so without SSA promotion the analysis dies at the first temporary. - WHY THIS IS PARKED: the retrace correctness gate never ran green. Every DirectVulkan retrace on Adreno 650 dies with DEVICE_LOST in UploadDirtyMipLevels on unmodified dev (pre-existing, device-gated), and on Adreno 830 - where the gate does pass on dev - minecraft-1.21.4-in-world times out at 900s with this change, which still needs explaining. Do not merge until that is understood and vanilla plus non-Photon shaderpack cases pass. (photon-v1.3b is broken on Adreno independently of this work.) - The /sdcard/MG/exp_relaxed_precision_all and exp_no_relaxed_precision file toggles are development scaffolding for A/B measurement; they must go before this ships.
This commit is contained in:
@@ -12,7 +12,10 @@
|
||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <unordered_set>
|
||||
#include <spirv-tools/libspirv.h>
|
||||
#include <spirv-tools/optimizer.hpp>
|
||||
#include <source/opt/build_module.h>
|
||||
@@ -1099,6 +1102,374 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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();
|
||||
@@ -1128,6 +1499,37 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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,
|
||||
ProgramFactory::CompileOptionFlags transformFlags) {
|
||||
if (input.empty()) {
|
||||
@@ -2186,6 +2588,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -47,6 +47,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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 HashType = Uint64;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include <vulkan/utility/vk_format_utils.h>
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include <Config.h>
|
||||
#include <cstdio>
|
||||
@@ -446,6 +447,64 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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;
|
||||
|
||||
@@ -76,6 +76,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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);
|
||||
|
||||
|
||||
@@ -4315,6 +4315,15 @@ void main() {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user