Compare commits

...
Author SHA1 Message Date
swung0x48 3223ecb14e [Feat] (DirectVulkan): relax fragment precision where the bound formats allow it
- WIP, parked: measures 80.9 -> 94.8 fps on Adreno 650 / MC 26.2 (same scene,
  device cooled to 38-40C), but is NOT validated. Desktop GLSL carries no
  precision qualifiers, so every fragment value reaches the driver as fp32 while
  Adreno runs fp16 at twice the rate.
- RelaxTextureDerivedPrecisionPass taints the values a fragment shader derives
  from built-in inputs and decorates everything else RelaxedPrecision. The
  taint direction matters: whitelisting outward from texture reads captures
  nothing, because MC multiplies every texel by an interpolated colour and a UBO
  value and one un-relaxed operand vetoes the expression - measured at 80.4 fps,
  i.e. no gain, both with and without varyings seeded. Precision-critical
  sources are few (gl_FragCoord cannot even hold a 3044-pixel x exactly), so
  tainting them and relaxing the rest is what actually pays.
- SPIR-V cannot see the bound formats - sampler2D yields vec4 whether the
  texture is RGBA8 or RGBA32F - so the decision is made per draw and passed in
  as a compile option, the same shape ExplicitLod0Sampling already uses.
  RelaxedFragmentPrecision is only requested when every sampled texture and
  every colour attachment is an 8-bit-or-less normalized format, where fp16's
  11-bit mantissa already carries the value exactly. Shaderpack HDR gbuffers,
  float data textures and 16-bit normalized targets therefore keep full
  precision, as do shaders that write gl_FragDepth or gl_SampleMask.
- LocalMultiStoreElim runs first: glslang emits function-local variables, and a
  load can never be relaxed, so without SSA promotion the analysis dies at the
  first temporary.
- WHY THIS IS PARKED: the retrace correctness gate never ran green. Every
  DirectVulkan retrace on Adreno 650 dies with DEVICE_LOST in
  UploadDirtyMipLevels on unmodified dev (pre-existing, device-gated), and on
  Adreno 830 - where the gate does pass on dev - minecraft-1.21.4-in-world times
  out at 900s with this change, which still needs explaining. Do not merge until
  that is understood and vanilla plus non-Photon shaderpack cases pass.
  (photon-v1.3b is broken on Adreno independently of this work.)
- The /sdcard/MG/exp_relaxed_precision_all and exp_no_relaxed_precision file
  toggles are development scaffolding for A/B measurement; they must go before
  this ships.
2026-07-29 09:02:00 -04:00
5 changed files with 493 additions and 0 deletions
@@ -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