mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Fix, Test] (ShaderTranspiler): rewrite float-equals-zero exactly instead of within a 1e-4 epsilon
This commit is contained in:
@@ -15,8 +15,6 @@
|
||||
#include "source/opt/ir_builder.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
@@ -29,7 +27,6 @@ namespace MobileGL {
|
||||
|
||||
analysis::ConstantManager* const_mgr = context()->get_constant_mgr();
|
||||
analysis::DefUseManager* def_use_mgr = context()->get_def_use_mgr();
|
||||
analysis::TypeManager* type_mgr = context()->get_type_mgr();
|
||||
|
||||
// 2. Import `GLSL.std.450` extension ID (for abs() func)
|
||||
uint32_t glsl_std_450_id = context()->get_feature_mgr()->GetExtInstImportId_GLSLstd450();
|
||||
@@ -70,6 +67,10 @@ namespace MobileGL {
|
||||
uint32_t op2_id = inst.GetSingleWordInOperand(1);
|
||||
|
||||
uint32_t var_id = 0;
|
||||
// The zero the source spelled, reused verbatim as the right-hand side
|
||||
// of the rewritten compare - so nothing has to be synthesized for a
|
||||
// width this pass would have to encode by hand.
|
||||
uint32_t zero_id = 0;
|
||||
|
||||
// The constant's WIDTH decides which accessor may read it, and asking
|
||||
// the wrong one does not fail - it answers.
|
||||
@@ -78,8 +79,8 @@ namespace MobileGL {
|
||||
// bits. On a 64-bit constant words()[0] is the LOW half of the
|
||||
// mantissa, and that half is zero for every round double a shader
|
||||
// actually spells: 1.0lf, 2.0lf, 0.5lf, 100.0lf. Each of those
|
||||
// therefore looked like 0.0 here, and `d != 1.0lf` was rewritten into
|
||||
// `abs(d) >= epsilon` - which is TRUE for d == 1.0. That is the whole
|
||||
// therefore looked like 0.0 here, and `d != 1.0lf` was rewritten into a
|
||||
// test of `d` against ZERO - which is TRUE for d == 1.0. That is the whole
|
||||
// of KHR-GL43.compute_shader.fp64-case2: twelve uniforms compared
|
||||
// against vector and matrix constructors were untouched (a composite
|
||||
// is not a FloatConstant) and the one scalar comparison in the shader
|
||||
@@ -97,17 +98,24 @@ namespace MobileGL {
|
||||
const analysis::Float* floatType =
|
||||
floatConstant->type() != nullptr ? floatConstant->type()->AsFloat() : nullptr;
|
||||
if (floatType == nullptr) return false;
|
||||
// Exactly zero - a near-zero constant is not a zero constant.
|
||||
// `x == 1e-5` asks a different question than `x == 0.0` and must
|
||||
// keep its own right-hand side. -0.0 compares equal to 0.0 here,
|
||||
// which is correct: `x == -0.0` and `x == 0.0` are the same
|
||||
// predicate in IEEE, and abs() maps both zeroes onto +0.
|
||||
switch (floatType->width()) {
|
||||
case 32: return std::fabs(floatConstant->GetFloatValue()) <= K_EPSILON;
|
||||
case 64: return std::fabs(floatConstant->GetDoubleValue()) <= K_EPSILON;
|
||||
case 32: return floatConstant->GetFloatValue() == 0.0f;
|
||||
case 64: return floatConstant->GetDoubleValue() == 0.0;
|
||||
default: return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (is_float_zero(op2_id)) {
|
||||
var_id = op1_id; // x == 0.0
|
||||
zero_id = op2_id;
|
||||
} else if (is_float_zero(op1_id)) {
|
||||
var_id = op2_id; // 0.0 == x
|
||||
zero_id = op1_id;
|
||||
} else {
|
||||
++itInst;
|
||||
continue;
|
||||
@@ -120,12 +128,7 @@ namespace MobileGL {
|
||||
uint32_t float_type_id = def_use_mgr->GetDef(var_id)->type_id();
|
||||
uint32_t bool_type_id = inst.type_id();
|
||||
|
||||
// 2. Create constant ID for `Epsilon`
|
||||
const analysis::Constant* eps_const = const_mgr->GetConstant(
|
||||
type_mgr->GetType(float_type_id), {*(reinterpret_cast<const uint32_t*>(&K_EPSILON))});
|
||||
uint32_t eps_id = const_mgr->GetDefiningInstruction(eps_const)->result_id();
|
||||
|
||||
// 3. Build Abs(x) inst
|
||||
// 2. Build Abs(x) inst
|
||||
// OpExtInst %float_type %glsl_import Abs %x
|
||||
InstructionBuilder builder(
|
||||
context(), &inst, IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
@@ -140,38 +143,42 @@ namespace MobileGL {
|
||||
Instruction* abs_inst = builder.AddInstruction(MakeUnique<Instruction>(
|
||||
context(), spv::Op::OpExtInst, float_type_id, context()->TakeNextId(), abs_operands));
|
||||
|
||||
// 4. build Abs(x) < Epsilon
|
||||
// OpFOrdLessThan %bool_type %abs_val %eps
|
||||
std::vector<Operand> less_operands;
|
||||
less_operands.push_back({SPV_OPERAND_TYPE_ID, {abs_inst->result_id()}});
|
||||
less_operands.push_back({SPV_OPERAND_TYPE_ID, {eps_id}});
|
||||
// 3. build Abs(x) <= 0.0, or Abs(x) > 0.0 for the NotEqual forms
|
||||
// OpFOrdLessThanEqual %bool_type %abs_val %zero
|
||||
std::vector<Operand> cmp_operands;
|
||||
cmp_operands.push_back({SPV_OPERAND_TYPE_ID, {abs_inst->result_id()}});
|
||||
cmp_operands.push_back({SPV_OPERAND_TYPE_ID, {zero_id}});
|
||||
|
||||
// Equality is INCLUDED in the replacement, which is what makes the
|
||||
// rewrite exact: |x| <= 0 is true for +0 and -0 and false for every
|
||||
// other finite value, |x| > 0 is its complement. The ordered/unordered
|
||||
// half of the opcode is preserved, so NaN keeps answering as it did.
|
||||
spv::Op replacementOp = spv::Op::OpNop;
|
||||
switch (inst.opcode()) {
|
||||
case spv::Op::OpFOrdEqual:
|
||||
replacementOp = spv::Op::OpFOrdLessThan;
|
||||
replacementOp = spv::Op::OpFOrdLessThanEqual;
|
||||
break;
|
||||
case spv::Op::OpFUnordEqual:
|
||||
replacementOp = spv::Op::OpFUnordLessThan;
|
||||
replacementOp = spv::Op::OpFUnordLessThanEqual;
|
||||
break;
|
||||
case spv::Op::OpFOrdNotEqual:
|
||||
replacementOp = spv::Op::OpFOrdGreaterThanEqual;
|
||||
replacementOp = spv::Op::OpFOrdGreaterThan;
|
||||
break;
|
||||
case spv::Op::OpFUnordNotEqual:
|
||||
replacementOp = spv::Op::OpFUnordGreaterThanEqual;
|
||||
replacementOp = spv::Op::OpFUnordGreaterThan;
|
||||
break;
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "Unexpected float compare opcode: %d",
|
||||
static_cast<int>(inst.opcode()));
|
||||
break;
|
||||
}
|
||||
Instruction* less_than_inst = builder.AddInstruction(MakeUnique<Instruction>(
|
||||
context(), replacementOp, bool_type_id, context()->TakeNextId(), less_operands));
|
||||
Instruction* cmp_inst = builder.AddInstruction(MakeUnique<Instruction>(
|
||||
context(), replacementOp, bool_type_id, context()->TakeNextId(), cmp_operands));
|
||||
|
||||
// 5. Replaces all uses of old insn with new one
|
||||
context()->ReplaceAllUsesWith(inst.result_id(), less_than_inst->result_id());
|
||||
// 4. Replaces all uses of old insn with new one
|
||||
context()->ReplaceAllUsesWith(inst.result_id(), cmp_inst->result_id());
|
||||
|
||||
// 6. Kill old instruction (will be cleaned up by DCE later)
|
||||
// 5. Kill old instruction (will be cleaned up by DCE later)
|
||||
auto nextInstIt = context()->KillInst(&inst);
|
||||
if (nextInstIt) {
|
||||
itInst = nextInstIt;
|
||||
|
||||
@@ -15,15 +15,28 @@
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Keeps the driver's float-EQUALITY instruction out of the module: every scalar
|
||||
// comparison against a constant 0.0 is re-spelled through GLSL.std.450 FAbs, so no
|
||||
// OpFOrdEqual / OpFUnordEqual / OpFOrdNotEqual / OpFUnordNotEqual against zero ever
|
||||
// reaches a shader compiler that gets exact float compare wrong.
|
||||
//
|
||||
// The rewrite is EXACT, not a tolerance. `x == 0.0` becomes `abs(x) <= 0.0` and
|
||||
// `x != 0.0` becomes `abs(x) > 0.0`, both against the module's own zero constant:
|
||||
// |x| <= 0 holds for +0 and -0 and for nothing else, so the two forms agree on every
|
||||
// input, at any float width, with or without denormal flushing. The ordered/unordered
|
||||
// half of the opcode is carried across unchanged, which is what keeps NaN answering
|
||||
// the way it did before.
|
||||
//
|
||||
// It used to be an epsilon ball (abs(x) < 1e-4). That silently classified any
|
||||
// legitimately small value as zero - KHR-GL3x.buffer_objects.triangles renders a
|
||||
// specular term of ~6e-5 at a large render target and came out black - so the fuzz is
|
||||
// gone; the reason the pass exists never needed it.
|
||||
class EliminateFloatEqualsZeroPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "float-equals-zero-elimination"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateEliminateFloatEqualsZeroPass();
|
||||
|
||||
private:
|
||||
const float K_EPSILON = 0.0001f;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
|
||||
Reference in New Issue
Block a user