[Feat, Fix, Test] (ShaderTranspiler, DirectGLES): carry the seven normalized image formats as their own codes in rgba16ui

This commit is contained in:
2026-08-22 04:17:46 -04:00
parent d0f7fb99db
commit 7eac33d17b
12 changed files with 1469 additions and 75 deletions
@@ -1024,6 +1024,12 @@ namespace MobileGL {
return WidenImageFormatsPass::ImageFormatChannelCount(glInternalFormat);
}
bool ShaderCompiler::NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
bool& outSignedNormalized) {
return WidenImageFormatsPass::NormalizedImageCarrierCodes(glInternalFormat, outChannelMax,
outSignedNormalized);
}
bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
const std::set<String>& blockNames,
std::set<String>& flattenedBlockNames,
@@ -298,6 +298,13 @@ namespace MobileGL {
// Channels a GL image internal format really has (1-4), 0 when it is not one of
// the forty image formats.
static Uint ImageFormatChannelCount(Uint glInternalFormat);
// Whether the carrier holds the format's channels as the INTEGER CODES of a
// normalized value, and the largest code each channel can hold. See
// WidenImageFormatsPass::NormalizedImageCarrierCodes - DirectGLES needs it for
// both halves of the transfer, which no longer share the frontend format's
// component class with the ES storage.
static bool NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
bool& outSignedNormalized);
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
@@ -22,9 +22,12 @@
#include "source/opt/type_manager.h"
#include "source/opt/types.h"
#include "source/util/make_unique.h"
#include "source/util/string_utils.h"
#include <cstring>
#include <map>
#include <memory>
#include <set>
#include <vector>
namespace MobileGL {
@@ -88,18 +91,46 @@ namespace MobileGL {
// program holding eight images).
//
// The remaining seven - rgb10_a2, rgba16, rg16, r16, rgba16_snorm, rg16_snorm and
// r16_snorm - stay absent, and for a stronger reason than quantisation: core ESSL
// has no 16-bit normalized format at all and no 10-bit one, so every candidate
// carrier for them either loses range or changes the component TYPE the texture
// presents. They keep the honest "no GLSL ES spelling" diagnostic rather than a
// silent approximation.
// r16_snorm - are NORMALIZED, and core ESSL has no 16-bit normalized format at all
// and no 10-bit one. There is no carrier that keeps their component type, and no
// FLOAT carrier that is honest either: a half has eleven mantissa bits against a
// 16-bit normalized channel's sixteen, so rgba16f would quantise. What DOES hold
// every one of their values exactly is the format's own CODE: a normalized channel
// of b bits is an integer in [0, 2^b-1] (unsigned) or [-(2^(b-1)-1), 2^(b-1)-1]
// (signed), and rgba16ui gives every channel of all seven sixteen bits to hold
// that integer in - bit for bit, with the SAME quantisation grid the real format
// has, which is the one thing a float carrier could not reproduce.
//
// The price is that the carrier changes the SHADER-VISIBLE TYPE: an image2D
// becomes a uimage2D, so every imageLoad has to divide the code back out and every
// imageStore has to round a value onto it (GL 4.6 2.3.5). ChannelMax below is the
// denominator that conversion uses, per channel - the same number for all four of
// a 16-bit format and (1023, 1023, 1023, 3) for rgb10_a2, whose channels are not
// all the same width.
//
// What this carrier gives up, and it is real: the ES texture behind the image is
// now an INTEGER texture, so a `sampler2D` bound to it reads codes rather than the
// normalized value, and it can no longer be filtered. Measured against the
// alternative, which is not a truer sampler but no program at all - the stage that
// declares one of these seven has no legal ESSL, so before this it did not compile
// and nothing sampled anything.
struct ImageFormatWidening {
spv::ImageFormat Carrier = spv::ImageFormat::Unknown;
uint32_t Channels = 0;
// Non-zero when the carrier holds the format's channels as the INTEGER CODES
// of a NORMALIZED value rather than as the values themselves: the largest code
// each channel can hold, i.e. 2^b - 1 for an unsigned normalized channel of b
// bits and 2^(b-1) - 1 for a signed one.
uint32_t ChannelMax[4] = {0u, 0u, 0u, 0u};
bool SignedNormalized = false;
bool CarriesNormalizedCodes() const { return ChannelMax[0] != 0u; }
explicit operator bool() const { return Carrier != spv::ImageFormat::Unknown; }
};
constexpr uint32_t kUnorm16Max = 65535u;
constexpr uint32_t kSnorm16Max = 32767u;
ImageFormatWidening WideningOfSpirvImageFormat(spv::ImageFormat format) {
switch (format) {
// Float.
@@ -130,6 +161,32 @@ namespace MobileGL {
// FOUR channels, so there is no surplus channel to mask and no access is
// rewritten - 10, 10, 10 and 2 bits of unsigned integer all fit in sixteen.
case spv::ImageFormat::Rgb10a2ui: return {spv::ImageFormat::Rgba16ui, 4};
// Unsigned normalized, carried as codes in [0, 2^b - 1].
case spv::ImageFormat::Rgba16:
return {spv::ImageFormat::Rgba16ui, 4,
{kUnorm16Max, kUnorm16Max, kUnorm16Max, kUnorm16Max}, false};
case spv::ImageFormat::Rg16:
return {spv::ImageFormat::Rgba16ui, 2,
{kUnorm16Max, kUnorm16Max, kUnorm16Max, kUnorm16Max}, false};
case spv::ImageFormat::R16:
return {spv::ImageFormat::Rgba16ui, 1,
{kUnorm16Max, kUnorm16Max, kUnorm16Max, kUnorm16Max}, false};
// The one entry whose channels are not all the same width, which is the whole
// reason ChannelMax is per channel rather than one number.
case spv::ImageFormat::Rgb10A2:
return {spv::ImageFormat::Rgba16ui, 4, {1023u, 1023u, 1023u, 3u}, false};
// Signed normalized, carried as the two's-complement code in [-(2^(b-1) - 1),
// 2^(b-1) - 1]. The carrier's channel is UNSIGNED, so the code's sixteen bits
// are stored verbatim and sign-extended again on the way out.
case spv::ImageFormat::Rgba16Snorm:
return {spv::ImageFormat::Rgba16ui, 4,
{kSnorm16Max, kSnorm16Max, kSnorm16Max, kSnorm16Max}, true};
case spv::ImageFormat::Rg16Snorm:
return {spv::ImageFormat::Rgba16ui, 2,
{kSnorm16Max, kSnorm16Max, kSnorm16Max, kSnorm16Max}, true};
case spv::ImageFormat::R16Snorm:
return {spv::ImageFormat::Rgba16ui, 1,
{kSnorm16Max, kSnorm16Max, kSnorm16Max, kSnorm16Max}, true};
default:
return {};
}
@@ -287,6 +344,180 @@ namespace MobileGL {
}
return true;
}
// GLSL.std.450 instruction numbers (see 3rdparty/glslang/SPIRV/GLSL.std.450.h).
constexpr uint32_t kGlslFSign = 6u;
constexpr uint32_t kGlslFMax = 40u;
constexpr uint32_t kGlslFClamp = 43u;
// The module's GLSL.std.450 import, creating it when the module has none. glslang
// emits one for all but the most trivial shaders, but a module that reached here
// without one still has to be carriable. 0 means no id was available and NOTHING
// was added, so the caller can still hand the module back untouched.
uint32_t EnsureGlslStd450Import(IRContext* context) {
for (const Instruction& import : context->module()->ext_inst_imports()) {
if (spvtools::utils::MakeString(import.GetInOperand(0).words) == "GLSL.std.450") {
return import.result_id();
}
}
const uint32_t importId = context->TakeNextId();
if (importId == 0u) return 0u;
context->AddExtInstImport(spvtools::MakeUnique<Instruction>(
context, spv::Op::OpExtInstImport, 0, importId,
Instruction::OperandList{
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector("GLSL.std.450")}}));
return importId;
}
// Every type and constant the normalized-code rewrite emits, resolved ONCE before
// any instruction is inserted. The type and constant managers append to the
// module's globals and keep their own def-use bookkeeping straight; the rewrite
// below does not (this pass drops every analysis at the end instead), so a manager
// consulted after the first insertion would be reading a def-use map that no
// longer describes the function bodies.
struct NormalizedCarrierMaterial {
uint32_t Glsl450Id = 0;
uint32_t FloatTypeId = 0; // the component types, kept only so the declaration
uint32_t IntTypeId = 0; // order below can put each vector after its own
uint32_t UintTypeId = 0; // component - and the last is the image's new Sampled Type
uint32_t UvecTypeId = 0; // uvec4: what an OpImageRead of the carrier yields
uint32_t IvecTypeId = 0; // ivec4: the sign-extended snorm code
uint32_t FvecTypeId = 0; // vec4: what the shader asked for
uint32_t ShiftWidthId = 0; // ivec4(16), the snorm sign extension
uint32_t LowWordMaskId = 0; // uvec4(0xFFFF)
uint32_t ZeroId = 0; // vec4(0.0)
uint32_t OneId = 0; // vec4(1.0)
uint32_t MinusOneId = 0; // vec4(-1.0)
uint32_t HalfId = 0; // vec4(0.5)
explicit operator bool() const { return UvecTypeId != 0u; }
};
// A four-component constant of `typeId` from four component ids.
uint32_t MakeVec4Constant(IRContext* context, uint32_t typeId, const uint32_t (&componentIds)[4]) {
auto* constantMgr = context->get_constant_mgr();
analysis::Type* vectorType = context->get_type_mgr()->GetType(typeId);
if (vectorType == nullptr) return 0u;
// A vector constant's "literal words" are the IDS of its components
// (ConstantManager::CreateConstant -> GetConstantsFromIds).
const analysis::Constant* constant = constantMgr->GetConstant(
vectorType, {componentIds[0], componentIds[1], componentIds[2], componentIds[3]});
if (constant == nullptr) return 0u;
const Instruction* definition = constantMgr->GetDefiningInstruction(constant);
return definition == nullptr ? 0u : definition->result_id();
}
uint32_t MakeScalarConstant(IRContext* context, analysis::Type* scalarType, uint32_t word) {
const analysis::Constant* constant = context->get_constant_mgr()->GetConstant(scalarType, {word});
if (constant == nullptr) return 0u;
const Instruction* definition =
context->get_constant_mgr()->GetDefiningInstruction(constant);
return definition == nullptr ? 0u : definition->result_id();
}
uint32_t MakeSplatVec4Constant(IRContext* context, uint32_t vectorTypeId,
analysis::Type* scalarType, uint32_t word) {
const uint32_t scalarId = MakeScalarConstant(context, scalarType, word);
if (scalarId == 0u) return 0u;
const uint32_t componentIds[4] = {scalarId, scalarId, scalarId, scalarId};
return MakeVec4Constant(context, vectorTypeId, componentIds);
}
uint32_t FloatBits(float value) {
uint32_t bits = 0;
static_assert(sizeof(bits) == sizeof(value), "float is not 32 bits");
std::memcpy(&bits, &value, sizeof(bits));
return bits;
}
NormalizedCarrierMaterial ResolveNormalizedCarrierMaterial(IRContext* context) {
NormalizedCarrierMaterial material;
auto* typeMgr = context->get_type_mgr();
analysis::Integer uintScalar(32, false);
analysis::Integer intScalar(32, true);
analysis::Float floatScalar(32);
analysis::Type* uintReg = typeMgr->GetRegisteredType(&uintScalar);
analysis::Type* intReg = typeMgr->GetRegisteredType(&intScalar);
analysis::Type* floatReg = typeMgr->GetRegisteredType(&floatScalar);
if (uintReg == nullptr || intReg == nullptr || floatReg == nullptr) return {};
analysis::Vector uintVector(uintReg, 4);
analysis::Vector intVector(intReg, 4);
analysis::Vector floatVector(floatReg, 4);
const uint32_t uintTypeId = typeMgr->GetTypeInstruction(&uintScalar);
const uint32_t uvecTypeId = typeMgr->GetTypeInstruction(&uintVector);
const uint32_t ivecTypeId = typeMgr->GetTypeInstruction(&intVector);
const uint32_t fvecTypeId = typeMgr->GetTypeInstruction(&floatVector);
if (uintTypeId == 0u || uvecTypeId == 0u || ivecTypeId == 0u || fvecTypeId == 0u) return {};
const uint32_t glsl450Id = EnsureGlslStd450Import(context);
if (glsl450Id == 0u) return {};
material.Glsl450Id = glsl450Id;
material.FloatTypeId = typeMgr->GetTypeInstruction(&floatScalar);
material.IntTypeId = typeMgr->GetTypeInstruction(&intScalar);
material.UintTypeId = uintTypeId;
material.UvecTypeId = uvecTypeId;
material.IvecTypeId = ivecTypeId;
material.FvecTypeId = fvecTypeId;
material.ShiftWidthId = MakeSplatVec4Constant(context, ivecTypeId, intReg, 16u);
material.LowWordMaskId = MakeSplatVec4Constant(context, uvecTypeId, uintReg, 0xFFFFu);
material.ZeroId = MakeSplatVec4Constant(context, fvecTypeId, floatReg, FloatBits(0.0f));
material.OneId = MakeSplatVec4Constant(context, fvecTypeId, floatReg, FloatBits(1.0f));
material.MinusOneId = MakeSplatVec4Constant(context, fvecTypeId, floatReg, FloatBits(-1.0f));
material.HalfId = MakeSplatVec4Constant(context, fvecTypeId, floatReg, FloatBits(0.5f));
if (material.ShiftWidthId == 0u || material.LowWordMaskId == 0u || material.ZeroId == 0u ||
material.OneId == 0u || material.MinusOneId == 0u || material.HalfId == 0u) {
return {};
}
return material;
}
// spirv-tools' type manager APPENDS a new type declaration to the END of the
// module's type section - which is fine for a type only function bodies name, and
// NOT fine for the uint32 an OpTypeImage further up is about to take as its
// Sampled Type. SPIR-V requires an id to be defined before it is used, and
// spirv-tools' own RemoveDuplicates - which the caller runs immediately after this
// pass - walks the section in order and dereferences each image type's sampled
// type as it goes, so an out-of-order declaration is a null dereference inside the
// type manager rather than a diagnostic.
//
// `typeIdsInDependencyOrder` must list a component type before any vector of it:
// each move lands immediately in front of `target`, so the order they are
// processed in is the order they end up in.
void HoistTypeDeclarationsBefore(IRContext* context, Instruction* target,
const std::vector<uint32_t>& typeIdsInDependencyOrder) {
std::set<uint32_t> definedBeforeTarget;
for (Instruction& declaration : context->module()->types_values()) {
if (&declaration == target) break;
definedBeforeTarget.insert(declaration.result_id());
}
for (const uint32_t typeId : typeIdsInDependencyOrder) {
if (typeId == 0u || definedBeforeTarget.count(typeId) != 0u) continue;
Instruction* declaration = context->get_def_use_mgr()->GetDef(typeId);
if (declaration == nullptr || declaration == target) continue;
// IntrusiveNodeBase::InsertBefore MOVES the node it is called on - it
// unlinks it from wherever it is first - which is the opposite convention
// to Instruction::InsertBefore(unique_ptr), used everywhere else here.
declaration->InsertBefore(target);
}
}
// vec4(ChannelMax), the denominator of the format's own normalized conversion.
uint32_t ResolveDenominatorConstant(IRContext* context, const NormalizedCarrierMaterial& material,
const uint32_t (&channelMax)[4]) {
analysis::Float floatScalar(32);
analysis::Type* floatReg = context->get_type_mgr()->GetRegisteredType(&floatScalar);
if (floatReg == nullptr) return 0u;
uint32_t componentIds[4] = {0u, 0u, 0u, 0u};
for (uint32_t i = 0; i < 4; ++i) {
componentIds[i] = MakeScalarConstant(context, floatReg,
FloatBits(static_cast<float>(channelMax[i])));
if (componentIds[i] == 0u) return 0u;
}
return MakeVec4Constant(context, material.FvecTypeId, componentIds);
}
} // namespace
Uint WidenImageFormatsPass::WidenedCoreEsslImageFormat(Uint glInternalFormat) {
@@ -296,6 +527,17 @@ namespace MobileGL {
return GLInternalFormatOfSpirvImageFormat(widening.Carrier);
}
bool WidenImageFormatsPass::NormalizedImageCarrierCodes(Uint glInternalFormat,
Uint32 (&outChannelMax)[4],
bool& outSignedNormalized) {
const ImageFormatWidening widening =
WideningOfSpirvImageFormat(SpirvImageFormatOfGL(glInternalFormat));
if (!widening || !widening.CarriesNormalizedCodes()) return false;
for (Uint i = 0; i < 4; ++i) outChannelMax[i] = widening.ChannelMax[i];
outSignedNormalized = widening.SignedNormalized;
return true;
}
Uint WidenImageFormatsPass::ImageFormatChannelCount(Uint glInternalFormat) {
return ChannelsOfSpirvImageFormat(SpirvImageFormatOfGL(glInternalFormat));
}
@@ -344,15 +586,23 @@ namespace MobileGL {
spv::ImageFormat Carrier = spv::ImageFormat::Unknown;
uint32_t Channels = 0;
uint32_t SampledTypeId = 0;
// 0 unless the carrier holds NORMALIZED CODES, in which case it is the vec4 of
// per-channel denominators the conversion divides by and multiplies back up.
uint32_t DenominatorId = 0;
bool SignedNormalized = false;
};
std::map<uint32_t, WidenedImage> widenedByTypeId;
Bool anyNormalizedCarrier = false;
for (Instruction* type : imageTypes) {
const auto format =
static_cast<spv::ImageFormat>(type->GetSingleWordInOperand(kImageFormatOperand));
const ImageFormatWidening widening = WideningOfSpirvImageFormat(format);
widenedByTypeId.emplace(type->result_id(),
WidenedImage{widening.Carrier, widening.Channels,
type->GetSingleWordInOperand(kImageSampledTypeOperand)});
anyNormalizedCarrier = anyNormalizedCarrier || widening.CarriesNormalizedCodes();
widenedByTypeId.emplace(
type->result_id(),
WidenedImage{widening.Carrier, widening.Channels,
type->GetSingleWordInOperand(kImageSampledTypeOperand), 0u,
widening.SignedNormalized});
}
// Collect the accesses BEFORE anything is mutated, and refuse the whole rewrite if
@@ -517,6 +767,196 @@ namespace MobileGL {
}
}
// ...and the same for the normalized carriers, whose rewrite needs a good deal
// more of both: the uvec4 an OpImageRead of the carrier yields, the ivec4 the
// signed code is sign-extended in, the GLSL.std.450 import the clamp and the sign
// come from, and one vec4 of denominators per DISTINCT channel-width set (all
// 65535 for the unsigned 16-bit formats, all 32767 for the signed ones, and
// (1023, 1023, 1023, 3) for rgb10_a2, whose channels are not all the same width).
NormalizedCarrierMaterial normalizedMaterial;
if (anyNormalizedCarrier) {
normalizedMaterial = ResolveNormalizedCarrierMaterial(irContext);
if (!normalizedMaterial) {
return Status::SuccessWithoutChange;
}
Instruction* firstNormalizedImageType = nullptr;
for (Instruction* type : imageTypes) {
const auto widenedIt = widenedByTypeId.find(type->result_id());
if (widenedIt == widenedByTypeId.end()) continue;
const ImageFormatWidening widening = WideningOfSpirvImageFormat(
static_cast<spv::ImageFormat>(type->GetSingleWordInOperand(kImageFormatOperand)));
if (!widening.CarriesNormalizedCodes()) continue;
// The shader asked for a gvec4 of the ORIGINAL sampled type, and for these
// seven that type is float. An integer image declared with a normalized
// format is not something glslang can produce, so a module that somehow
// holds one is refused rather than converted through a type it never had.
const Instruction* sampledType = defUseMgr->GetDef(widenedIt->second.SampledTypeId);
if (sampledType == nullptr || sampledType->opcode() != spv::Op::OpTypeFloat ||
sampledType->GetSingleWordInOperand(0) != 32) {
return Status::SuccessWithoutChange;
}
const uint32_t denominatorId =
ResolveDenominatorConstant(irContext, normalizedMaterial, widening.ChannelMax);
if (denominatorId == 0u) {
return Status::SuccessWithoutChange;
}
widenedIt->second.DenominatorId = denominatorId;
if (firstNormalizedImageType == nullptr) firstNormalizedImageType = type;
}
// ...and the declarations have to reach the module in the right ORDER, not
// just exist. See HoistTypeDeclarationsBefore: the type manager appends, and
// the image type that names the new uint32 is already further up.
if (firstNormalizedImageType != nullptr) {
HoistTypeDeclarationsBefore(
irContext, firstNormalizedImageType,
{normalizedMaterial.FloatTypeId, normalizedMaterial.IntTypeId,
normalizedMaterial.UintTypeId, normalizedMaterial.FvecTypeId,
normalizedMaterial.IvecTypeId, normalizedMaterial.UvecTypeId});
}
}
// The two halves of GL 4.6 2.3.5 for a normalized carrier, spelled as SPIR-V.
//
// UNPACK (imageLoad), unsigned: f = c / (2^b - 1)
// UNPACK (imageLoad), signed: f = max(c / (2^(b-1) - 1), -1)
// PACK (imageStore), unsigned: c = round(clamp(f, 0, 1) * (2^b - 1))
// PACK (imageStore), signed: c = round(clamp(f, -1, 1) * (2^(b-1) - 1))
//
// `round` is round-to-NEAREST, and GL leaves the tie direction to the
// implementation ("if two values are equally near, the implementation may choose
// either"). This one always rounds a tie AWAY FROM ZERO, which is a legal choice
// and, unlike GLSL's own round(), a deterministic one - so the boundary cases can
// be pinned by a test rather than described. It is spelled as a truncation of
// x + 0.5*sign(x), because OpConvertFToU/OpConvertFToS truncate toward zero.
//
// A signed code is stored in an UNSIGNED carrier channel, so pack masks it to the
// low sixteen bits (a negative uint32 is out of an rgba16ui channel's range, and
// what a store does with an out-of-range integer is not defined) and unpack
// sign-extends it back with a shift pair.
//
// Both operate on all FOUR channels at once, including the surplus ones a one- or
// two-channel format does not have: the mask shuffle runs on the float side either
// way, so whatever the surplus channels hold is discarded on the way out and
// written as the format's own 0 and 1 on the way in.
auto insertUnpack = [&](Instruction* before, const WidenedImage& widened,
uint32_t rawId) -> uint32_t {
const auto emit = [&](spv::Op opcode, uint32_t typeId,
Instruction::OperandList operands) -> uint32_t {
const uint32_t resultId = irContext->TakeNextId();
if (resultId == 0u) return 0u;
before->InsertBefore(spvtools::MakeUnique<Instruction>(irContext, opcode, typeId, resultId,
Move(operands)));
return resultId;
};
// The raw texel through an OpCopyObject before anything reads it, which costs
// nothing in SPIR-V and is load-bearing in the ESSL: SPIRV-Cross emits a copy
// of a non-opaque value as a real `uvec4 _n = imageLoad(...);` statement,
// where the read's own result is FORWARDED into whatever consumes it. Mesa's
// llvmpipe compiler miscompiles the forwarded form - `vec4(imageLoad(img, c))`
// written straight into an expression comes back as zeroes, and 0xFFFF comes
// back as a NaN, while the identical arithmetic on a named uvec4 is correct.
// Measured with hand-written core-format ESSL (an rgba16ui uimage2D read into
// an rgba32f image2D), so it is the driver rather than anything this pass or
// the emitter does; the copy is the cheapest way to stay out of it, and every
// other driver folds it away.
const uint32_t texelId =
emit(spv::Op::OpCopyObject, normalizedMaterial.UvecTypeId,
{{SPV_OPERAND_TYPE_ID, {rawId}}});
if (texelId == 0u) return 0u;
uint32_t codeId = 0;
if (widened.SignedNormalized) {
const uint32_t asIntId =
emit(spv::Op::OpBitcast, normalizedMaterial.IvecTypeId,
{{SPV_OPERAND_TYPE_ID, {texelId}}});
if (asIntId == 0u) return 0u;
const uint32_t shiftedUpId =
emit(spv::Op::OpShiftLeftLogical, normalizedMaterial.IvecTypeId,
{{SPV_OPERAND_TYPE_ID, {asIntId}},
{SPV_OPERAND_TYPE_ID, {normalizedMaterial.ShiftWidthId}}});
if (shiftedUpId == 0u) return 0u;
const uint32_t signExtendedId =
emit(spv::Op::OpShiftRightArithmetic, normalizedMaterial.IvecTypeId,
{{SPV_OPERAND_TYPE_ID, {shiftedUpId}},
{SPV_OPERAND_TYPE_ID, {normalizedMaterial.ShiftWidthId}}});
if (signExtendedId == 0u) return 0u;
codeId = emit(spv::Op::OpConvertSToF, normalizedMaterial.FvecTypeId,
{{SPV_OPERAND_TYPE_ID, {signExtendedId}}});
} else {
codeId = emit(spv::Op::OpConvertUToF, normalizedMaterial.FvecTypeId,
{{SPV_OPERAND_TYPE_ID, {texelId}}});
}
if (codeId == 0u) return 0u;
const uint32_t normalizedId =
emit(spv::Op::OpFDiv, normalizedMaterial.FvecTypeId,
{{SPV_OPERAND_TYPE_ID, {codeId}}, {SPV_OPERAND_TYPE_ID, {widened.DenominatorId}}});
if (normalizedId == 0u || !widened.SignedNormalized) return normalizedId;
// -2^(b-1) is representable in the code but GL clamps it to -1: the signed
// decode is max(c / (2^(b-1) - 1), -1), not the bare division.
return emit(spv::Op::OpExtInst, normalizedMaterial.FvecTypeId,
{{SPV_OPERAND_TYPE_ID, {normalizedMaterial.Glsl450Id}},
{SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER, {kGlslFMax}},
{SPV_OPERAND_TYPE_ID, {normalizedId}},
{SPV_OPERAND_TYPE_ID, {normalizedMaterial.MinusOneId}}});
};
auto insertPack = [&](Instruction* before, const WidenedImage& widened,
uint32_t valueId) -> uint32_t {
const auto emit = [&](spv::Op opcode, uint32_t typeId,
Instruction::OperandList operands) -> uint32_t {
const uint32_t resultId = irContext->TakeNextId();
if (resultId == 0u) return 0u;
before->InsertBefore(spvtools::MakeUnique<Instruction>(irContext, opcode, typeId, resultId,
Move(operands)));
return resultId;
};
const uint32_t lowBoundId =
widened.SignedNormalized ? normalizedMaterial.MinusOneId : normalizedMaterial.ZeroId;
const uint32_t clampedId =
emit(spv::Op::OpExtInst, normalizedMaterial.FvecTypeId,
{{SPV_OPERAND_TYPE_ID, {normalizedMaterial.Glsl450Id}},
{SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER, {kGlslFClamp}},
{SPV_OPERAND_TYPE_ID, {valueId}},
{SPV_OPERAND_TYPE_ID, {lowBoundId}},
{SPV_OPERAND_TYPE_ID, {normalizedMaterial.OneId}}});
if (clampedId == 0u) return 0u;
const uint32_t scaledId =
emit(spv::Op::OpFMul, normalizedMaterial.FvecTypeId,
{{SPV_OPERAND_TYPE_ID, {clampedId}},
{SPV_OPERAND_TYPE_ID, {widened.DenominatorId}}});
if (scaledId == 0u) return 0u;
uint32_t biasId = normalizedMaterial.HalfId;
if (widened.SignedNormalized) {
// 0.5 * sign(x), so the truncation below rounds a tie away from zero on
// both sides. sign(0) is 0, which leaves an exact zero exactly zero.
const uint32_t signId = emit(spv::Op::OpExtInst, normalizedMaterial.FvecTypeId,
{{SPV_OPERAND_TYPE_ID, {normalizedMaterial.Glsl450Id}},
{SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER, {kGlslFSign}},
{SPV_OPERAND_TYPE_ID, {scaledId}}});
if (signId == 0u) return 0u;
biasId = emit(spv::Op::OpFMul, normalizedMaterial.FvecTypeId,
{{SPV_OPERAND_TYPE_ID, {signId}},
{SPV_OPERAND_TYPE_ID, {normalizedMaterial.HalfId}}});
if (biasId == 0u) return 0u;
}
const uint32_t roundedId =
emit(spv::Op::OpFAdd, normalizedMaterial.FvecTypeId,
{{SPV_OPERAND_TYPE_ID, {scaledId}}, {SPV_OPERAND_TYPE_ID, {biasId}}});
if (roundedId == 0u) return 0u;
if (!widened.SignedNormalized) {
return emit(spv::Op::OpConvertFToU, normalizedMaterial.UvecTypeId,
{{SPV_OPERAND_TYPE_ID, {roundedId}}});
}
const uint32_t signedCodeId = emit(spv::Op::OpConvertFToS, normalizedMaterial.IvecTypeId,
{{SPV_OPERAND_TYPE_ID, {roundedId}}});
if (signedCodeId == 0u) return 0u;
const uint32_t asUintId = emit(spv::Op::OpBitcast, normalizedMaterial.UvecTypeId,
{{SPV_OPERAND_TYPE_ID, {signedCodeId}}});
if (asUintId == 0u) return 0u;
return emit(spv::Op::OpBitwiseAnd, normalizedMaterial.UvecTypeId,
{{SPV_OPERAND_TYPE_ID, {asUintId}},
{SPV_OPERAND_TYPE_ID, {normalizedMaterial.LowWordMaskId}}});
};
// Masks first, while every image type still carries its ORIGINAL format: the
// rewrite below only touches the format operand, so the accesses' types do not
// move and the order is free either way - but doing it first keeps a failed
@@ -524,10 +964,13 @@ namespace MobileGL {
for (Instruction* write : writes) {
const WidenedImage* widened = widenedOf(write);
if (widened == nullptr) continue;
const Bool normalized = widened->DenominatorId != 0u;
// A carrier with as many channels as the original (rgb10_a2ui in rgba16ui) has
// no surplus channel to pin, and the shuffle would select (0, 1, 2, 3) from the
// texel - an identity the emitter would still print. Left out entirely.
if (widened->Channels >= 4) continue;
// texel - an identity the emitter would still print. Left out entirely, unless
// the texel still has to be PACKED, in which case the store is rewritten
// anyway and only the shuffle is skipped.
if (widened->Channels >= 4 && !normalized) continue;
uint32_t zeroOneId = 0;
uint32_t vec4TypeId = 0;
if (!resolveMaskMaterial(widened->SampledTypeId, zeroOneId, vec4TypeId)) {
@@ -540,22 +983,35 @@ namespace MobileGL {
if (texel == nullptr || texel->type_id() != vec4TypeId) {
return Status::SuccessWithoutChange;
}
const uint32_t maskedId = irContext->TakeNextId();
if (maskedId == 0) return Status::Failure;
Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {texelId}},
{SPV_OPERAND_TYPE_ID, {zeroOneId}}};
for (const Operand& component : maskComponents(widened->Channels)) {
shuffleOperands.push_back(component);
uint32_t storedId = texelId;
if (widened->Channels < 4) {
const uint32_t maskedId = irContext->TakeNextId();
if (maskedId == 0) return Status::Failure;
Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {texelId}},
{SPV_OPERAND_TYPE_ID, {zeroOneId}}};
for (const Operand& component : maskComponents(widened->Channels)) {
shuffleOperands.push_back(component);
}
write->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpVectorShuffle, vec4TypeId, maskedId, shuffleOperands));
storedId = maskedId;
}
write->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpVectorShuffle, vec4TypeId, maskedId, shuffleOperands));
write->SetInOperand(kImageWriteTexelOperand, {maskedId});
// The pack goes AFTER the mask, so the carrier's surplus channels are written
// as the codes GL's own 0 and 1 quantise to (0 and the channel maximum) rather
// than as raw zeroes and ones - which is what a later imageLoad, and the
// upload that seeds an untouched level, both have to agree with.
if (normalized) {
storedId = insertPack(write, *widened, storedId);
if (storedId == 0u) return Status::Failure;
}
write->SetInOperand(kImageWriteTexelOperand, {storedId});
}
for (Instruction* read : reads) {
const WidenedImage* widened = widenedOf(read);
if (widened == nullptr) continue;
if (widened->Channels >= 4) continue; // see the store loop
const Bool normalized = widened->DenominatorId != 0u;
if (widened->Channels >= 4 && !normalized) continue; // see the store loop
uint32_t zeroOneId = 0;
uint32_t vec4TypeId = 0;
if (!resolveMaskMaterial(widened->SampledTypeId, zeroOneId, vec4TypeId)) {
@@ -569,6 +1025,11 @@ namespace MobileGL {
// existing use of the read stays intact without a ReplaceAllUsesWith that
// would also rewrite the shuffle's own operand (the idiom
// EmulateNoPerspectivePass uses for the same reason).
//
// Under a normalized carrier the copy reads a uvec4 rather than the vec4 the
// shader asked for - that is the whole point of the carrier - and the unpack
// in between brings it back. Every extra instruction is inserted in front of
// the original too, so the chain stays in order.
const uint32_t rawReadId = irContext->TakeNextId();
if (rawReadId == 0) return Status::Failure;
Instruction::OperandList readOperands;
@@ -576,9 +1037,18 @@ namespace MobileGL {
readOperands.push_back(read->GetInOperand(i));
}
read->InsertBefore(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpImageRead, vec4TypeId, rawReadId, readOperands));
irContext, spv::Op::OpImageRead,
normalized ? normalizedMaterial.UvecTypeId : vec4TypeId, rawReadId, readOperands));
uint32_t loadedId = rawReadId;
if (normalized) {
loadedId = insertUnpack(read, *widened, rawReadId);
if (loadedId == 0u) return Status::Failure;
}
// Always a shuffle, even at four channels: it is what carries the ORIGINAL
// result id, which every existing use still names. At four channels the
// selectors are the identity (0, 1, 2, 3), so nothing is substituted.
read->SetOpcode(spv::Op::OpVectorShuffle);
Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {rawReadId}},
Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {loadedId}},
{SPV_OPERAND_TYPE_ID, {zeroOneId}}};
for (const Operand& component : maskComponents(widened->Channels)) {
shuffleOperands.push_back(component);
@@ -586,13 +1056,20 @@ namespace MobileGL {
read->SetInOperands(Move(shuffleOperands));
}
// The declaration itself, last. Only the format operand moves: the carrier has the
// same component type as the original by construction, so the OpTypeImage's
// Sampled Type still agrees with it (which is what spirv-val checks) and no
// pointer, array or access-chain type has to be rebuilt.
// The declaration itself, last. For most carriers only the format operand moves:
// the carrier has the same component type as the original by construction, so the
// OpTypeImage's Sampled Type still agrees with it (which is what spirv-val checks)
// and no pointer, array or access-chain type has to be rebuilt.
//
// A NORMALIZED carrier moves the Sampled Type too - float32 to uint32, which is
// what turns an image2D into a uimage2D - and it has to, because spirv-val
// requires the Sampled Type to match the format's component class. Nothing above
// the OpTypeImage has to change with it: the pointer, the variable and every
// OpLoad name the image type by ID, and the id is being mutated in place.
//
// Two image types can COLLIDE here - `layout(rg32f)` and `layout(rgba32f)` in one
// module both become Rgba32f - and duplicate non-aggregate type declarations are
// module both become Rgba32f, and so do `layout(rgba16)` image2D and
// `layout(rgba16ui)` uimage2D - and duplicate non-aggregate type declarations are
// invalid SPIR-V. The caller runs spirv-tools' RemoveDuplicates pass immediately
// after this one, which joins them (and cascades to the pointer and array types
// that named them) rather than this pass carrying its own join.
@@ -604,6 +1081,9 @@ namespace MobileGL {
// module that has since grown instructions it was never told about. Every
// analysis is dropped below instead.
type->SetInOperand(kImageFormatOperand, {static_cast<uint32_t>(widenedIt->second.Carrier)});
if (widenedIt->second.DenominatorId != 0u) {
type->SetInOperand(kImageSampledTypeOperand, {normalizedMaterial.UintTypeId});
}
}
// StorageImageExtendedFormats is deliberately left declared even though every
@@ -136,6 +136,21 @@ namespace MobileGL {
// forty image formats. The count the widened accesses are masked back to.
static Uint ImageFormatChannelCount(Uint glInternalFormat);
// Whether the carrier holds this format's channels as the INTEGER CODES of a
// NORMALIZED value rather than as the values themselves - true for the seven
// 16-bit and 10-bit normalized formats and nothing else. `outChannelMax` takes the
// largest code each channel can hold (2^b - 1 unsigned, 2^(b-1) - 1 signed), which
// is the denominator of GL 4.6 2.3.5 for that channel; `outSignedNormalized` says
// which of the two conversions applies.
//
// DirectGLES asks this on both sides of the transfer: the upload pads a missing
// alpha with outChannelMax[3] rather than the transfer type's own 1 (through a
// uint carrier "one" is the saturated CODE, not the integer one), and
// glGetTexImage divides the codes back out, because the ES storage is an integer
// texture the client still expects to read as floats.
static bool NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
bool& outSignedNormalized);
static spvtools::Optimizer::PassToken CreateWidenImageFormatsPass(
bool onlyFormatsSpirvCrossRefusesToPrint = false);