shader: pack v_cvt_pkrtz_f16_f32 with the native half conversion (GTA V) (#1520)

* shader: pack v_cvt_pkrtz_f16_f32 with the native half conversion

The round-toward-zero pack was emulated with integer operations: about 20 to 30 operations
per half. GTA V uses it in 613 of its 656 pixel shaders, about 16 times in each G-buffer
pixel shader.

The pair is now packed with PackHalf2x16, unpacked again, and each half whose magnitude
exceeds its input's takes one unit off. Half signs and magnitudes are separate, so that is
the next value toward zero, whatever rounding the native conversion uses as long as it
returns one of the two neighbours. Same bits as before for every finite input and infinity;
a NaN stays a NaN. Checked against the old code on 2 million values on the CPU, and on an
Apple M5 GPU through Metal (400 000 values, subnormals included, safe and fast math).

GTA V on an Apple M5 (MoltenVK), 1080p, fixed script camera, three places, A B B A between
launches: -1.7 to -2.8 ms per frame (+4 to +6 %).

Scope: generic
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader: remove half-pack commentary and unused shift helper

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
AN0DIN
2026-10-16 02:56:35 +02:00
committed by GitHub
parent 13e4c315b6
commit 83db2efec3
3 changed files with 17 additions and 76 deletions
@@ -210,10 +210,23 @@ uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value
}
uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1) {
const auto low = EmitF32ToF16RtzBits(state, arg0);
const auto high = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
EmitF32ToF16RtzBits(state, arg1), ConstantU32(state, 16));
return Binary(state, spv::OpBitwiseOr, TypeU32(state), low, high);
const auto pair = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 2), pair, arg0, arg1);
const auto packed = EmitPackHalf2x16(state, pair);
const auto back = EmitExt(state, TypeF32Vector(state, 2), GLSLstd450UnpackHalf2x16, {packed});
auto result = packed;
for (uint32_t i = 0; i < 2; i++) {
const auto rounded = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), rounded, back, i);
const auto above =
Binary(state, spv::OpFOrdGreaterThan, TypeBool(state),
EmitExt(state, TypeF32(state), GLSLstd450FAbs, {rounded}),
EmitExt(state, TypeF32(state), GLSLstd450FAbs, {i == 0 ? arg0 : arg1}));
result = Binary(state, spv::OpISub, TypeU32(state), result,
Select(state, TypeU32(state), above,
ConstantU32(state, i == 0 ? 1u : 0x10000u), ConstantU32(state, 0)));
}
return result;
}
uint32_t EmitFPSaturate32(EmitterState& state, uint32_t arg0) {
@@ -9,13 +9,6 @@ uint32_t EmitAndConstant(EmitterState& state, uint32_t value, uint32_t mask) {
return ret;
}
uint32_t EmitShiftRightConstant(EmitterState& state, uint32_t value, uint32_t shift) {
const auto ret = state.builder.AllocateId();
state.builder.AddFunction(spv::OpShiftRightLogical, TypeU32(state), ret, value,
ConstantU32(state, shift));
return ret;
}
uint32_t EmitCompareU32Constant(EmitterState& state, spv::Op opcode, uint32_t value,
uint32_t constant) {
const auto ret = state.builder.AllocateId();
@@ -23,65 +16,6 @@ uint32_t EmitCompareU32Constant(EmitterState& state, spv::Op opcode, uint32_t va
return ret;
}
uint32_t EmitSubConstantMinusU32(EmitterState& state, uint32_t constant, uint32_t value) {
const auto ret = state.builder.AllocateId();
state.builder.AddFunction(spv::OpISub, TypeU32(state), ret, ConstantU32(state, constant),
value);
return ret;
}
uint32_t EmitF32ToF16RtzBits(EmitterState& state, uint32_t f32) {
const auto bits = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, f32);
const auto sign = EmitAndConstant(state, EmitShiftRightConstant(state, bits, 16), 0x8000u);
const auto exp = EmitAndConstant(state, EmitShiftRightConstant(state, bits, 23), 0xffu);
const auto mant = EmitAndConstant(state, bits, 0x007fffffu);
const auto half_exp = state.builder.AllocateId();
const auto normal_exp = state.builder.AllocateId();
const auto normal_mant = EmitShiftRightConstant(state, mant, 13);
const auto normal_payload = state.builder.AllocateId();
const auto normal = state.builder.AllocateId();
state.builder.AddFunction(spv::OpISub, TypeU32(state), half_exp, exp, ConstantU32(state, 112));
state.builder.AddFunction(spv::OpShiftLeftLogical, TypeU32(state), normal_exp, half_exp,
ConstantU32(state, 10));
state.builder.AddFunction(spv::OpBitwiseOr, TypeU32(state), normal_payload, normal_exp,
normal_mant);
state.builder.AddFunction(spv::OpBitwiseOr, TypeU32(state), normal, sign, normal_payload);
const auto mant_with_hidden = EmitOrU32(state, mant, ConstantU32(state, 0x00800000u));
const auto raw_sub_shift = EmitSubConstantMinusU32(state, 126, exp);
const auto exp_lt_103 = EmitCompareU32Constant(state, spv::OpULessThan, exp, 103);
const auto exp_gt_112 = EmitCompareU32Constant(state, spv::OpUGreaterThan, exp, 112);
const auto sub_shift_low =
EmitSelectValueU32(state, exp_lt_103, ConstantU32(state, 31), raw_sub_shift);
const auto sub_shift =
EmitSelectValueU32(state, exp_gt_112, ConstantU32(state, 14), sub_shift_low);
const auto sub_mant = state.builder.AllocateId();
const auto subnormal = state.builder.AllocateId();
state.builder.AddFunction(spv::OpShiftRightLogical, TypeU32(state), sub_mant, mant_with_hidden,
sub_shift);
state.builder.AddFunction(spv::OpBitwiseOr, TypeU32(state), subnormal, sign, sub_mant);
const auto nan_payload =
EmitOrU32(state, EmitShiftRightConstant(state, mant, 13), ConstantU32(state, 0x0200u));
const auto nan =
EmitOrU32(state, sign, EmitOrU32(state, ConstantU32(state, 0x7c00u), nan_payload));
const auto inf = EmitOrU32(state, sign, ConstantU32(state, 0x7c00u));
const auto max_finite = EmitOrU32(state, sign, ConstantU32(state, 0x7bffu));
const auto mant_zero = EmitCompareU32Constant(state, spv::OpIEqual, mant, 0);
const auto special = EmitSelectValueU32(state, mant_zero, inf, nan);
const auto exp_le_112 = EmitCompareU32Constant(state, spv::OpULessThanEqual, exp, 112);
const auto exp_ge_143 = EmitCompareU32Constant(state, spv::OpUGreaterThanEqual, exp, 143);
const auto exp_eq_255 = EmitCompareU32Constant(state, spv::OpIEqual, exp, 255);
const auto finite0 = EmitSelectValueU32(state, exp_le_112, subnormal, normal);
const auto finite1 = EmitSelectValueU32(state, exp_lt_103, sign, finite0);
const auto finite2 = EmitSelectValueU32(state, exp_ge_143, max_finite, finite1);
return EmitAndConstant(state, EmitSelectValueU32(state, exp_eq_255, special, finite2), 0xffffu);
}
F32Class EmitClassifyF32Bits(EmitterState& state, uint32_t bits) {
F32Class cls;
cls.bits = bits;
@@ -488,17 +488,11 @@ inline constexpr auto EmitSelectValueU32 =
uint32_t EmitAndConstant(EmitterState& state, uint32_t value, uint32_t mask);
uint32_t EmitShiftRightConstant(EmitterState& state, uint32_t value, uint32_t shift);
inline constexpr auto EmitOrU32 = EmitNative<spv::OpBitwiseOr, IR::Type::U32, uint32_t, uint32_t>;
uint32_t EmitCompareU32Constant(EmitterState& state, spv::Op opcode, uint32_t value,
uint32_t constant);
uint32_t EmitSubConstantMinusU32(EmitterState& state, uint32_t constant, uint32_t value);
uint32_t EmitF32ToF16RtzBits(EmitterState& state, uint32_t f32);
inline constexpr auto EmitBitcastF32ToU32 = EmitNative<spv::OpBitcast, IR::Type::U32, uint32_t>;
inline constexpr auto EmitBitcastU32ToF32 = EmitNative<spv::OpBitcast, IR::Type::F32, uint32_t>;