mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[Feat, Fix, Test] (ShaderTranspiler, DirectGLES): carry the seven normalized image formats as their own codes in rgba16ui
This commit is contained in:
@@ -1088,7 +1088,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
backendObj = MakeShared<BackendTextureObject>();
|
||||
}
|
||||
if (imageBindableStorageRequired) {
|
||||
backendObj->RequireImageBindableStorage();
|
||||
backendObj->RequireImageBindableStorage(textureObject);
|
||||
}
|
||||
backendObj->SyncTextureParamsToBackend(textureObject);
|
||||
backendObj->SyncBuiltinSamplerToBackend(textureObject);
|
||||
@@ -7687,9 +7687,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// scratch framebuffer, so the frontend's READ binding describes a different image entirely -
|
||||
// consulting it there would both miss real widenings and corrupt readbacks of ordinary
|
||||
// textures taken while some unrelated widened attachment happened to be bound.
|
||||
// The image-format widening's READ half, for the seven normalized formats whose carrier holds
|
||||
// their channels as INTEGER CODES (GL_RGBA16 stored as a GL_RGBA16UI - see
|
||||
// TextureImpl::GetImageBindableStorageWidening). Nothing else in the readback would get those
|
||||
// right: the attachment is an integer one while the application's format is normalized, so the
|
||||
// class check below would refuse the read outright, and a repack that got past it would hand
|
||||
// back 65535.0 where GL owes 1.0.
|
||||
//
|
||||
// Inactive (ChannelMax all zero) for every other read, which is all but a handful.
|
||||
struct NormalizedImageCarrierRead {
|
||||
Uint ChannelMax[4] = {0u, 0u, 0u, 0u};
|
||||
Bool SignedNormalized = false;
|
||||
|
||||
Bool Active() const { return ChannelMax[0] != 0u; }
|
||||
};
|
||||
|
||||
static Bool ReadPixelsViaFormatConversion(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format,
|
||||
GLenum type, void* pixels, Bool honorPackImageParams,
|
||||
Bool applyFixedPointReadClamp, Bool forceOpaqueAlpha) {
|
||||
Bool applyFixedPointReadClamp, Bool forceOpaqueAlpha,
|
||||
const NormalizedImageCarrierRead& normalizedCarrier = {}) {
|
||||
ReadbackChannelMapping mapping{};
|
||||
if (!GetReadbackChannelMapping(format, mapping)) {
|
||||
return false;
|
||||
@@ -7712,7 +7728,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const GLenum attachmentComponentType = QueryReadAttachmentComponentType();
|
||||
const Bool integerAttachment =
|
||||
attachmentComponentType == GL_INT || attachmentComponentType == GL_UNSIGNED_INT;
|
||||
if (mapping.isInteger != integerAttachment) {
|
||||
// A normalized image carrier is EXACTLY the case where the two disagree on purpose, and
|
||||
// it is the caller - which knows the TEXTURE being read, not just the attachment - that
|
||||
// says so. An integer client format through such a carrier is not a shape GL can ask for
|
||||
// (the frontend format is normalized), so it is refused here rather than converted.
|
||||
if (normalizedCarrier.Active() && (mapping.isInteger || !integerAttachment)) {
|
||||
MGLOG_E_ONCE("Readback conversion: a normalized image carrier was read as %s, which is not a "
|
||||
"normalized client format; skipping",
|
||||
MG_Util::ConvertGLEnumToString(format).c_str());
|
||||
return true;
|
||||
}
|
||||
if (!normalizedCarrier.Active() && mapping.isInteger != integerAttachment) {
|
||||
MGLOG_E_ONCE("Readback conversion: integer-ness of format %s does not match the read buffer, skipping",
|
||||
MG_Util::ConvertGLEnumToString(format).c_str());
|
||||
return true;
|
||||
@@ -7732,7 +7758,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
};
|
||||
WideReadCandidate candidates[4];
|
||||
Int candidateCount = 0;
|
||||
if (mapping.isInteger) {
|
||||
if (normalizedCarrier.Active()) {
|
||||
// The storage IS an integer texture, whatever the application's format says, so the
|
||||
// only read that can answer is the integer one. The codes it hands back are turned
|
||||
// into the floats the client asked for below.
|
||||
candidates[candidateCount++] = {GL_RGBA_INTEGER, GL_UNSIGNED_INT};
|
||||
} else if (mapping.isInteger) {
|
||||
if (GetWideReadChannelCount(static_cast<GLenum>(implFormat)) > 0 && IsIntegerReadFormat(implFormat) &&
|
||||
(implType == GL_INT || implType == GL_UNSIGNED_INT)) {
|
||||
candidates[candidateCount++] = {static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)};
|
||||
@@ -7794,6 +7825,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedCarrier.Active()) {
|
||||
// GL 4.6 2.3.5, the same conversion the shader-side unpack does and with the same
|
||||
// denominators, so a texel an imageStore wrote and a texel the upload seeded read back
|
||||
// identically: f = c / (2^b - 1) unsigned, f = max(c / (2^(b-1) - 1), -1) signed, with
|
||||
// the signed code recovered from the low sixteen bits of the unsigned channel.
|
||||
const SizeT pixelCount = static_cast<SizeT>(width) * static_cast<SizeT>(height);
|
||||
Vector<Uint8> floatWide(pixelCount * 4 * sizeof(Float));
|
||||
auto* dst = reinterpret_cast<Float*>(floatWide.data());
|
||||
const auto* src = reinterpret_cast<const Uint32*>(wide.data());
|
||||
for (SizeT i = 0; i < pixelCount; ++i) {
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
const Uint32 code = src[i * 4 + channel];
|
||||
const auto denominator = static_cast<Float>(normalizedCarrier.ChannelMax[channel]);
|
||||
if (normalizedCarrier.SignedNormalized) {
|
||||
const auto signedCode = static_cast<Int16>(static_cast<Uint16>(code));
|
||||
dst[i * 4 + channel] =
|
||||
std::max(static_cast<Float>(signedCode) / denominator, -1.0f);
|
||||
} else {
|
||||
dst[i * 4 + channel] = static_cast<Float>(code) / denominator;
|
||||
}
|
||||
}
|
||||
}
|
||||
wide = Move(floatWide);
|
||||
wideType = GL_FLOAT;
|
||||
readChannels = 4;
|
||||
}
|
||||
if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) {
|
||||
// Unpack the packed words into a float wide buffer (full 10-bit precision on e.g.
|
||||
// GL_RGB10_A2 attachments, whose implementation read pair is RGBA/2_10_10_10_REV).
|
||||
@@ -8441,6 +8498,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// GL_READ_FRAMEBUFFER, so the widening question has to be asked of the texture.
|
||||
const Bool forceOpaqueAlpha =
|
||||
TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(), textureObject->GetTarget());
|
||||
// An image-bindable texture in one of the seven normalized formats has its ES storage
|
||||
// in a GL_RGBA16UI, holding the format's own channel CODES. glGetTexImage still owes
|
||||
// the application the NORMALIZED value, so the conversion has to be undone here - and
|
||||
// it can only be asked of the TEXTURE, which is why it is not derived from the
|
||||
// attachment the scratch framebuffer happens to hold.
|
||||
NormalizedImageCarrierRead normalizedCarrier;
|
||||
if (const auto imageWidening =
|
||||
TextureImpl::GetImageBindableStorageWidening(textureObject->GetFormat());
|
||||
imageWidening && imageWidening.CarriesNormalizedCodes() &&
|
||||
(*backendTextureSlot)->RequiresImageBindableStorage()) {
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
normalizedCarrier.ChannelMax[channel] = imageWidening.ChannelMax[channel];
|
||||
}
|
||||
normalizedCarrier.SignedNormalized = imageWidening.SignedNormalized;
|
||||
}
|
||||
// GL_PACK_IMAGE_HEIGHT/GL_PACK_SKIP_IMAGES only apply to 3D/array image
|
||||
// readbacks (cube-map arrays address as arrays); 2D targets must ignore
|
||||
// them (GL 3.3 section 6.1.4). A 1D ARRAY is one of those 2D targets: GL hands it back
|
||||
@@ -8550,7 +8622,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void* sliceDst = static_cast<Uint8*>(pixels) + sliceOffset;
|
||||
if (!ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, sliceDst,
|
||||
/*honorPackImageParams=*/false,
|
||||
/*applyFixedPointReadClamp=*/false, forceOpaqueAlpha)) {
|
||||
/*applyFixedPointReadClamp=*/false, forceOpaqueAlpha,
|
||||
normalizedCarrier)) {
|
||||
allSlicesRead = false;
|
||||
break;
|
||||
}
|
||||
@@ -8573,7 +8646,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (tempFBOComplete && ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, pixels,
|
||||
applyPackImageParams,
|
||||
/*applyFixedPointReadClamp=*/false,
|
||||
forceOpaqueAlpha)) {
|
||||
forceOpaqueAlpha, normalizedCarrier)) {
|
||||
MGLOG_D("GetTexImage: finished via client-format conversion");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2408,12 +2408,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return m_backendTextureId;
|
||||
}
|
||||
|
||||
void BackendTextureObject::RequireImageBindableStorage() {
|
||||
void BackendTextureObject::RequireImageBindableStorage(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
||||
if (m_imageBindableStorageRequired) {
|
||||
return;
|
||||
}
|
||||
m_imageBindableStorageRequired = true;
|
||||
m_isInitialized = false;
|
||||
// Every level this object has ALREADY uploaded has to be replayed, because the
|
||||
// regeneration this transition schedules re-mints the storage in the image carrier and
|
||||
// only uploads levels the shadow still calls dirty - which, for a texture that was
|
||||
// synced before its first glBindImageTexture, is none of them. The new storage would
|
||||
// come out ALLOCATED AND EMPTY, and every texel the application defined before that
|
||||
// bind would be gone: the shader reads zeroes and the shadow still holds the data, so
|
||||
// glGetTexImage (which falls back to the shadow) keeps answering correctly and only
|
||||
// the image loads are wrong. Reached whenever anything syncs the texture first - a
|
||||
// glGetTexImage, a draw that samples it, an FBO attach - which is why it survived so
|
||||
// long: the scenario that binds the image immediately after uploading never sees it.
|
||||
if (auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject.get())) {
|
||||
const auto levelCount = mipmapObject->GetMipmapLevelCount();
|
||||
for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) {
|
||||
for (Uint level = 0; level < levelCount; ++level) {
|
||||
const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level);
|
||||
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0) continue;
|
||||
if (mipmapObject->GetMipmapByteSize(uploadTarget, level) == 0) continue;
|
||||
mipmapObject->MarkStorageDirty(uploadTarget, level, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
// The storage this re-mints may also be CHANNEL WIDENED (a GL_RG32F image is not
|
||||
// bindable on this driver at all, so it becomes a GL_RGBA32F carrying two channels),
|
||||
// and a widened texture's sampled view has to answer the channels the logical format
|
||||
@@ -2716,7 +2738,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// runs after any type conversion (which keeps the component count) has already happened.
|
||||
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize,
|
||||
const void* data, SizeT byteSize, GLenum uploadType,
|
||||
Vector<Uint8>& widenedData, Bool integerData) {
|
||||
Vector<Uint8>& widenedData, Bool integerData,
|
||||
Uint32 alphaOneCodeOverride) {
|
||||
Uint8 oneBits[8] = {};
|
||||
SizeT componentSize = 0;
|
||||
// One and two source components as well as three: the image-format widening carries
|
||||
@@ -2728,6 +2751,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
!GetUploadComponentOneBits(uploadType, integerData, oneBits, &componentSize)) {
|
||||
return data;
|
||||
}
|
||||
// ...except where the carrier holds CODES of a normalized value (GL_R16 in a
|
||||
// GL_RGBA16UI), where the transfer type says GL_UNSIGNED_SHORT and neither of that
|
||||
// type's two "ones" is right: the integer 1 is a code for 1/65535 and the saturated
|
||||
// 0xFFFF is only right for the UNSIGNED 16-bit formats, not the signed ones, whose
|
||||
// saturated code is 0x7FFF. The caller passes the channel's own maximum instead.
|
||||
// Written through a value of the component's own width rather than as the low
|
||||
// `componentSize` bytes of the Uint32, so the encoding does not turn on the host's
|
||||
// byte order.
|
||||
if (alphaOneCodeOverride != 0u) {
|
||||
if (componentSize == sizeof(Uint16)) {
|
||||
const auto one = static_cast<Uint16>(alphaOneCodeOverride);
|
||||
Memcpy(oneBits, &one, sizeof(one));
|
||||
} else if (componentSize == sizeof(Uint32)) {
|
||||
Memcpy(oneBits, &alphaOneCodeOverride, sizeof(alphaOneCodeOverride));
|
||||
} else if (componentSize == sizeof(Uint8)) {
|
||||
const auto one = static_cast<Uint8>(alphaOneCodeOverride);
|
||||
Memcpy(oneBits, &one, sizeof(one));
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT srcTexelBytes = componentSize * componentCount;
|
||||
// Sized from the level, never from the source: the driver reads a full
|
||||
@@ -2982,7 +3024,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return data;
|
||||
}
|
||||
return PrepareChannelWidenedUpload(widening.SourceChannels, texelSize, data, byteSize, widening.Type,
|
||||
widenedData, widening.IntegerData);
|
||||
widenedData, widening.IntegerData,
|
||||
widening.CarriesNormalizedCodes() ? widening.ChannelMax[3] : 0u);
|
||||
}
|
||||
|
||||
// Overwrites the (internal format, format, type) triple GenerateTextureFormatInfo chose
|
||||
@@ -5447,6 +5490,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 10/10/10/2 unsigned INTEGER channels in an rgba16ui: same component type, same
|
||||
// channel count, every value representable. Only the transfer is re-encoded.
|
||||
case glslang::ElfRgb10a2ui: return 0x906F; // GL_RGB10_A2UI
|
||||
// The seven NORMALIZED formats, carried in an rgba16ui as their own channel CODES.
|
||||
// These are the entries whose carrier changes the shader-visible type as well as
|
||||
// the qualifier (image2D becomes uimage2D), so every access through them is
|
||||
// wrapped in the GL 4.6 2.3.5 conversion - see WidenImageFormatsPass.h.
|
||||
case glslang::ElfRgba16: return 0x805B; // GL_RGBA16
|
||||
case glslang::ElfRg16: return 0x822C; // GL_RG16
|
||||
case glslang::ElfR16: return 0x822A; // GL_R16
|
||||
case glslang::ElfRgb10A2: return 0x8059; // GL_RGB10_A2
|
||||
case glslang::ElfRgba16Snorm: return 0x8F9B; // GL_RGBA16_SNORM
|
||||
case glslang::ElfRg16Snorm: return 0x8F99; // GL_RG16_SNORM
|
||||
case glslang::ElfR16Snorm: return 0x8F98; // GL_R16_SNORM
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -745,9 +745,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Returns `data` untouched when no widening applies. Pure CPU and context-free so a unit
|
||||
// test can exercise the exact packing the driver is handed; `widenedData` is the caller's
|
||||
// scratch buffer and has to outlive the returned pointer.
|
||||
// `alphaOneCodeOverride`, when non-zero, replaces the value written into the synthetic
|
||||
// alpha channel: an image carrier that holds a NORMALIZED format's channel CODES has to
|
||||
// pad alpha with that channel's saturated CODE (65535, 32767, 3), which neither of the
|
||||
// transfer type's own "ones" is.
|
||||
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data,
|
||||
SizeT byteSize, GLenum uploadType, Vector<Uint8>& widenedData,
|
||||
Bool integerData = false);
|
||||
Bool integerData = false, Uint32 alphaOneCodeOverride = 0u);
|
||||
|
||||
// Splits a GL_UNSIGNED_INT_2_10_10_10_REV shadow (rgb10_a2, rgb10_a2ui) into the four
|
||||
// GL_UNSIGNED_SHORT channel CODES its GL_RGBA16UI image carrier is uploaded as: red in
|
||||
@@ -790,7 +794,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void RequireImageBindableStorage();
|
||||
// Marks the texture as one whose ES storage has to be image-bindable, which for a
|
||||
// non-core image format means re-minting it in the widening's carrier. Takes the state
|
||||
// object because the levels already uploaded have to be marked dirty again: the
|
||||
// re-mint allocates fresh storage and only replays what the shadow still calls dirty.
|
||||
void RequireImageBindableStorage(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
// Whether this texture's ES storage was minted in an image carrier rather than in the
|
||||
// frontend format's own layout - the readback has to ask, because for a NORMALIZED
|
||||
// carrier the storage is an integer texture holding codes and glGetTexImage still owes
|
||||
// the application floats.
|
||||
Bool RequiresImageBindableStorage() const { return m_imageBindableStorageRequired; }
|
||||
void Bind(GLenum target, Uint unit = TempTextureUnit);
|
||||
Uint GetBackendTextureId() const;
|
||||
|
||||
@@ -1176,23 +1190,51 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
|
||||
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
|
||||
// assigned through glUniform1i.
|
||||
//
|
||||
// ALL THIRTY-THREE of them, in the one contiguous block ARB_shader_image_load_store allocated
|
||||
// (GL_IMAGE_1D 0x904C through GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY 0x906C). The list
|
||||
// used to hold only the fifteen whose TARGET exists in ES, which read as a reasonable
|
||||
// shortcut and was two bugs: an image uniform this says "no" to is one
|
||||
// CollectImageFormatBakeInputs never walks, so its non-core format is neither baked nor
|
||||
// widened and SPIRV-Cross throws for the whole stage ("Attempting to use image format not
|
||||
// supported in ES profile"), and it is also one SyncToBackend then treats as a SAMPLER and
|
||||
// assigns with glUniform1i, which ES makes an INVALID_OPERATION. A GL_TEXTURE_CUBE_MAP_ARRAY
|
||||
// image - which ES 3.2 has in core, so it is not even an emulated target - hit both.
|
||||
inline Bool IsImageUniformType(GLenum type) {
|
||||
switch (type) {
|
||||
case 0x904C: /*GL_IMAGE_1D*/
|
||||
case 0x904D: /*GL_IMAGE_2D*/
|
||||
case 0x904E: /*GL_IMAGE_3D*/
|
||||
case 0x904F: /*GL_IMAGE_2D_RECT*/
|
||||
case 0x9050: /*GL_IMAGE_CUBE*/
|
||||
case 0x9051: /*GL_IMAGE_BUFFER*/
|
||||
case 0x9052: /*GL_IMAGE_1D_ARRAY*/
|
||||
case 0x9053: /*GL_IMAGE_2D_ARRAY*/
|
||||
case 0x9054: /*GL_IMAGE_CUBE_MAP_ARRAY*/
|
||||
case 0x9055: /*GL_IMAGE_2D_MULTISAMPLE*/
|
||||
case 0x9056: /*GL_IMAGE_2D_MULTISAMPLE_ARRAY*/
|
||||
case 0x9057: /*GL_INT_IMAGE_1D*/
|
||||
case 0x9058: /*GL_INT_IMAGE_2D*/
|
||||
case 0x9059: /*GL_INT_IMAGE_3D*/
|
||||
case 0x905A: /*GL_INT_IMAGE_2D_RECT*/
|
||||
case 0x905B: /*GL_INT_IMAGE_CUBE*/
|
||||
case 0x905C: /*GL_INT_IMAGE_BUFFER*/
|
||||
case 0x905D: /*GL_INT_IMAGE_1D_ARRAY*/
|
||||
case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/
|
||||
case 0x905F: /*GL_INT_IMAGE_CUBE_MAP_ARRAY*/
|
||||
case 0x9060: /*GL_INT_IMAGE_2D_MULTISAMPLE*/
|
||||
case 0x9061: /*GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY*/
|
||||
case 0x9062: /*GL_UNSIGNED_INT_IMAGE_1D*/
|
||||
case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/
|
||||
case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/
|
||||
case 0x9065: /*GL_UNSIGNED_INT_IMAGE_2D_RECT*/
|
||||
case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/
|
||||
case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/
|
||||
case 0x9068: /*GL_UNSIGNED_INT_IMAGE_1D_ARRAY*/
|
||||
case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/
|
||||
case 0x906A: /*GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY*/
|
||||
case 0x906B: /*GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE*/
|
||||
case 0x906C: /*GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY*/
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
||||
@@ -292,13 +292,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
widening.SourceEncoding = ImageWidenSourceEncoding::PackedFloat11f11f10f;
|
||||
break;
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
case TextureInternalFormat::RGB10A2:
|
||||
// GL_UNSIGNED_INT_2_10_10_10_REV -> the GL_RGBA_INTEGER / GL_UNSIGNED_SHORT the
|
||||
// GL_RGBA16UI carrier already asked for above; only the split is new.
|
||||
// GL_RGBA16UI carrier already asked for above; only the split is new. The two
|
||||
// formats share it: rgb10_a2's channel codes are the same fields rgb10_a2ui's are,
|
||||
// and what the shader divides them by is not the transfer's business.
|
||||
widening.SourceEncoding = ImageWidenSourceEncoding::PackedInt2101010Rev;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// The seven normalized formats whose carrier holds CODES rather than values. Both
|
||||
// halves of the transfer need to know: a missing alpha is padded with the saturated
|
||||
// code rather than the integer 1, and glGetTexImage has to divide the codes back out.
|
||||
bool signedNormalized = false;
|
||||
Uint32 channelMax[4] = {0u, 0u, 0u, 0u};
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::NormalizedImageCarrierCodes(requested, channelMax,
|
||||
signedNormalized)) {
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
widening.ChannelMax[channel] = channelMax[channel];
|
||||
}
|
||||
widening.SignedNormalized = signedNormalized;
|
||||
}
|
||||
return widening;
|
||||
}
|
||||
} // namespace TextureImpl
|
||||
|
||||
@@ -132,7 +132,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// What the upload has to do to the frontend shadow before it describes the level to
|
||||
// the driver (PrepareImageWidenedUpload).
|
||||
ImageWidenSourceEncoding SourceEncoding = ImageWidenSourceEncoding::Components;
|
||||
// Non-zero when the carrier holds this format's channels as the INTEGER CODES of a
|
||||
// NORMALIZED value - the seven 16-bit and 10-bit normalized formats, which core ESSL
|
||||
// has no image format of any width for and which a float carrier would requantise.
|
||||
// Each entry is the largest code that channel can hold, i.e. the denominator of GL 4.6
|
||||
// 2.3.5; SignedNormalized picks which of the two conversions it is the denominator of.
|
||||
//
|
||||
// Two things depend on it, both because the ES storage no longer shares the frontend
|
||||
// format's component class: the upload pads a missing alpha with ChannelMax[3] instead
|
||||
// of the transfer type's own "one" (through a uint carrier the saturated field IS the
|
||||
// one), and glGetTexImage divides the codes back out into the floats the application
|
||||
// is still owed.
|
||||
Uint ChannelMax[4] = {0u, 0u, 0u, 0u};
|
||||
Bool SignedNormalized = false;
|
||||
|
||||
Bool CarriesNormalizedCodes() const { return ChannelMax[0] != 0u; }
|
||||
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
|
||||
};
|
||||
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat);
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
// store, or masked it with the wrong constants, or widened the storage without widening the bind,
|
||||
// fails these on the device while the software lanes stay green.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -176,6 +178,62 @@ namespace MGITest {
|
||||
return texels;
|
||||
}
|
||||
|
||||
// A GL_TEXTURE_CUBE_MAP_ARRAY of `cubeCount` cubes, i.e. 6 * cubeCount layer-faces
|
||||
// addressed as array layers. The target the allTargets walkers reach last and the one
|
||||
// that has caught the most emulation bugs, because it is the only one whose ES
|
||||
// equivalent is a 2D array with a different addressing rule from the GL name.
|
||||
GLuint MakeCubeArrayTexture(GLenum internalFormat, GLenum uploadFormat, GLenum uploadType,
|
||||
const void* seed, int cubeCount) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 1, internalFormat, kExtent, kExtent,
|
||||
6 * cubeCount);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "allocating cube-array storage errored with " << GLErrorName(error);
|
||||
return 0;
|
||||
}
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
if (seed != nullptr) {
|
||||
glTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, 0, kExtent, kExtent,
|
||||
6 * cubeCount, uploadFormat, uploadType, seed);
|
||||
}
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
void BindLayeredImage(GLuint unit, GLuint texture, GLenum internalFormat, GLenum access) {
|
||||
glBindImageTexture(unit, texture, 0, GL_TRUE, 0, access, internalFormat);
|
||||
ASSERT_EQ(FirstGLError(), 0u)
|
||||
<< "glBindImageTexture refused layered format " << std::hex << internalFormat;
|
||||
}
|
||||
|
||||
// Sets `name` from `values`, which must hold 4 * count floats.
|
||||
void SetVec4Array(GLuint program, const char* name, const std::vector<float>& values,
|
||||
int count) {
|
||||
glUseProgram(program);
|
||||
const GLint location = glGetUniformLocation(program, name);
|
||||
ASSERT_GE(location, 0) << "the uniform array '" << name << "' was not reflected";
|
||||
glUniform4fv(location, count, values.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "setting '" << name << "' errored";
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
std::vector<float> ReadFloatsFrom(GLenum target, GLuint texture, GLenum format,
|
||||
int componentsPerTexel, int texelCount) {
|
||||
std::vector<float> texels(static_cast<std::size_t>(texelCount) * componentsPerTexel,
|
||||
-12345.0f);
|
||||
glBindTexture(target, texture);
|
||||
glGetTexImage(target, 0, format, GL_FLOAT, texels.data());
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "reading the image back errored with " << GLErrorName(error);
|
||||
}
|
||||
return texels;
|
||||
}
|
||||
|
||||
std::vector<GLuint> ReadUints(GLuint texture, GLenum format, int componentsPerTexel) {
|
||||
std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * componentsPerTexel,
|
||||
0xFFFFFFFFu);
|
||||
@@ -506,6 +564,430 @@ void main()
|
||||
}
|
||||
}
|
||||
|
||||
// GL_RG16, the first of the seven NORMALIZED formats and the first carrier that changes the
|
||||
// shader-visible TYPE: core ESSL has no 16-bit normalized image format of any width, and
|
||||
// no float carrier is honest either (a half has eleven mantissa bits against sixteen), so
|
||||
// the rgba16ui behind it holds the format's own CODES and every access converts.
|
||||
//
|
||||
// Both directions of GL 4.6 2.3.5 are checked, and the STORE direction is checked as exact
|
||||
// INTEGER CODES rather than as floats within a tolerance - which is the point of a code
|
||||
// carrier over a float one, and the only thing that would catch a rounding rule that was
|
||||
// merely close. The values are chosen so that the products are exact in float32: 0.25 and
|
||||
// 0.75 land off a tie, 0.5 lands exactly ON one (0.5 * 65535 = 32767.5), and the two
|
||||
// out-of-range values must be clamped before they are rounded rather than after.
|
||||
TEST_F(NonCoreImageFormatScenario, UnsignedNormalizedImageCarriesItsCodesBothWays) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
constexpr int kTexels = kExtent * kExtent;
|
||||
constexpr double kUnorm16Max = 65535.0;
|
||||
|
||||
// THE UPLOAD. Distinct per texel, and the codes are the shadow's own 16-bit words: a
|
||||
// widening that padded or sheared them still produces plausible normalized floats.
|
||||
std::vector<GLushort> seed(static_cast<std::size_t>(kTexels) * 2u, 0);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
seed[texel * 2 + 0] = static_cast<GLushort>(texel * 4001);
|
||||
seed[texel * 2 + 1] = static_cast<GLushort>(65535 - texel * 3001);
|
||||
}
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -1.0f);
|
||||
const GLuint narrow = MakeTexture(GL_RG16, GL_RG, GL_UNSIGNED_SHORT, seed.data());
|
||||
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
|
||||
if (narrow == 0 || wide == 0) return;
|
||||
|
||||
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rg16, binding = 0) readonly uniform image2D narrow;
|
||||
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(wide, coord, imageLoad(narrow, coord));
|
||||
}
|
||||
)");
|
||||
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rg16, binding = 0) writeonly uniform image2D narrow;
|
||||
uniform vec4 g_values[16];
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(narrow, coord, g_values[coord.y * 4 + coord.x]);
|
||||
}
|
||||
)");
|
||||
if (loadProgram == 0 || storeProgram == 0) return;
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RG16, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * kUnorm16Max), seed[texel * 2 + 0])
|
||||
<< "texel " << texel << " red";
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 1] * kUnorm16Max), seed[texel * 2 + 1])
|
||||
<< "texel " << texel << " green";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 2], 0.0f)
|
||||
<< "texel " << texel << ": imageLoad on a two-channel format must report 0 for blue";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f)
|
||||
<< "texel " << texel << ": imageLoad on a format without alpha must report 1";
|
||||
}
|
||||
|
||||
// THE STORE, per GL 4.6 2.3.5: c = round(clamp(f, 0, 1) * (2^b - 1)), with a tie
|
||||
// rounded away from zero.
|
||||
struct Boundary {
|
||||
float value;
|
||||
long code;
|
||||
};
|
||||
const Boundary boundaries[kTexels] = {
|
||||
{0.0f, 0}, {1.0f, 65535}, {0.5f, 32768}, {0.25f, 16384},
|
||||
{0.75f, 49151}, {-0.5f, 0}, {2.0f, 65535}, {-1.0f, 0},
|
||||
{1.0f / 131072.0f, 0}, // 0.4999923 of a code: rounds DOWN
|
||||
{3.0f / 131072.0f, 1}, // 1.4999771 of a code: rounds DOWN to 1
|
||||
{1.0f / 65535.0f, 1}, // exactly one code
|
||||
{32767.0f / 65535.0f, 32767}, {32768.0f / 65535.0f, 32768},
|
||||
// 0.125 * 65535 = 8191.875 and 0.875 * 65535 = 57343.125 - neither is a tie, and
|
||||
// both round DOWN, which is the pair that catches a conversion that scaled by 2^b
|
||||
// instead of 2^b - 1.
|
||||
{65534.0f / 65535.0f, 65534}, {0.125f, 8192}, {0.875f, 57343},
|
||||
};
|
||||
std::vector<float> values(static_cast<std::size_t>(kTexels) * 4u, 0.0f);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
values[texel * 4 + 0] = boundaries[texel].value;
|
||||
values[texel * 4 + 1] = boundaries[texel].value;
|
||||
values[texel * 4 + 2] = 0.5f; // dropped: a two-channel format has no blue
|
||||
values[texel * 4 + 3] = 0.5f; // dropped: nor an alpha
|
||||
}
|
||||
SetVec4Array(storeProgram, "g_values", values, kTexels);
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RG16, GL_WRITE_ONLY);
|
||||
Dispatch(storeProgram);
|
||||
|
||||
// Read back through the IMAGE, so what is compared is the code the store actually
|
||||
// wrote rather than anything the readback path might renormalize on its own.
|
||||
BindImage(kNarrowUnit, narrow, GL_RG16, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * kUnorm16Max), boundaries[texel].code)
|
||||
<< "texel " << texel << " stored " << boundaries[texel].value;
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 1] * kUnorm16Max), boundaries[texel].code)
|
||||
<< "texel " << texel << " green";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 2], 0.0f) << "texel " << texel << " blue";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f) << "texel " << texel << " alpha";
|
||||
}
|
||||
|
||||
// ...and glGetTexImage owes the application the NORMALIZED value, whatever the ES
|
||||
// storage holds. The whole texture is an integer one now, so this is the only place
|
||||
// the readback conversion is exercised at all.
|
||||
const std::vector<float> viaGetTexImage = ReadFloats(narrow, GL_RG, 2);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(viaGetTexImage[texel * 2 + 0] * kUnorm16Max), boundaries[texel].code)
|
||||
<< "texel " << texel << " red through glGetTexImage";
|
||||
EXPECT_EQ(std::lround(viaGetTexImage[texel * 2 + 1] * kUnorm16Max), boundaries[texel].code)
|
||||
<< "texel " << texel << " green through glGetTexImage";
|
||||
}
|
||||
}
|
||||
|
||||
// GL_RGBA16_SNORM, the signed twin. Two things differ and both are one-line mistakes: the
|
||||
// code is a two's-complement 16-bit integer stored in an UNSIGNED carrier channel, so it
|
||||
// has to be sign-extended on the way out (a zero extension reads every negative value as
|
||||
// something near +1), and the decode is max(c / 32767, -1) rather than the bare division,
|
||||
// because the code -32768 exists and GL says it means exactly -1.
|
||||
TEST_F(NonCoreImageFormatScenario, SignedNormalizedImageSignExtendsItsCodesAndClampsAtMinusOne) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
constexpr int kTexels = kExtent * kExtent;
|
||||
constexpr double kSnorm16Max = 32767.0;
|
||||
|
||||
// The seed walks the whole signed range, INCLUDING -32768, whose decode is the one
|
||||
// value the division alone gets wrong.
|
||||
const GLshort seedCodes[kTexels] = {0, 32767, -32767, -32768, 1, -1, 16384, -16384,
|
||||
12345, -12345, 32766, -32766, 255, -256, 4095, -4096};
|
||||
std::vector<GLshort> seed(static_cast<std::size_t>(kTexels) * 4u, 0);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
seed[texel * 4 + 0] = seedCodes[texel];
|
||||
seed[texel * 4 + 1] = static_cast<GLshort>(-seedCodes[texel] == -32768 ? 32767
|
||||
: -seedCodes[texel]);
|
||||
seed[texel * 4 + 2] = seedCodes[(texel + 1) % kTexels];
|
||||
seed[texel * 4 + 3] = seedCodes[(texel + 2) % kTexels];
|
||||
}
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -12.0f);
|
||||
const GLuint narrow = MakeTexture(GL_RGBA16_SNORM, GL_RGBA, GL_SHORT, seed.data());
|
||||
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
|
||||
if (narrow == 0 || wide == 0) return;
|
||||
|
||||
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rgba16_snorm, binding = 0) readonly uniform image2D narrow;
|
||||
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(wide, coord, imageLoad(narrow, coord));
|
||||
}
|
||||
)");
|
||||
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rgba16_snorm, binding = 0) writeonly uniform image2D narrow;
|
||||
uniform vec4 g_values[16];
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(narrow, coord, g_values[coord.y * 4 + coord.x]);
|
||||
}
|
||||
)");
|
||||
if (loadProgram == 0 || storeProgram == 0) return;
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGBA16_SNORM, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
const GLshort code = seed[texel * 4 + channel];
|
||||
const float expected =
|
||||
std::max(static_cast<float>(code) / static_cast<float>(kSnorm16Max), -1.0f);
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + channel], expected)
|
||||
<< "texel " << texel << " channel " << channel << " code " << code;
|
||||
}
|
||||
}
|
||||
|
||||
// THE STORE: c = round(clamp(f, -1, 1) * (2^(b-1) - 1)), ties away from zero on BOTH
|
||||
// sides - which is what makes -0.5 land on -16384 rather than on -16383.
|
||||
struct Boundary {
|
||||
float value;
|
||||
long code;
|
||||
};
|
||||
const Boundary boundaries[kTexels] = {
|
||||
{0.0f, 0}, {1.0f, 32767}, {-1.0f, -32767}, {0.5f, 16384},
|
||||
{-0.5f, -16384}, {2.0f, 32767}, {-2.0f, -32767}, {0.25f, 8192},
|
||||
{-0.25f, -8192}, {1.0f / 32767.0f, 1}, {-1.0f / 32767.0f, -1},
|
||||
// Three quarters of a code, not half: GL leaves the direction of a TIE to the
|
||||
// implementation ("if two values are equally near, the implementation may choose
|
||||
// either"), and Magma hands these formats to Vulkan unemulated, so a value exactly
|
||||
// on 0.5 of a code is the one thing the two backends are allowed to disagree
|
||||
// about. Every entry here is off a tie except the ones at 0.5 and 0.25 of the
|
||||
// RANGE, whose products (16383.5 and 8192) round the same way under either rule.
|
||||
{0.75f / 32767.0f, 1}, {-0.75f / 32767.0f, -1},
|
||||
{16383.0f / 32767.0f, 16383}, {-16383.0f / 32767.0f, -16383},
|
||||
{0.125f, 4096},
|
||||
};
|
||||
std::vector<float> values(static_cast<std::size_t>(kTexels) * 4u, 0.0f);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
values[texel * 4 + channel] = boundaries[texel].value;
|
||||
}
|
||||
}
|
||||
SetVec4Array(storeProgram, "g_values", values, kTexels);
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGBA16_SNORM, GL_WRITE_ONLY);
|
||||
Dispatch(storeProgram);
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGBA16_SNORM, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + channel] * kSnorm16Max),
|
||||
boundaries[texel].code)
|
||||
<< "texel " << texel << " channel " << channel << " stored "
|
||||
<< boundaries[texel].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GL_RGB10_A2, the one normalized format whose channels are not all the same width: three
|
||||
// of ten bits and one of two. A single denominator would be right for three quarters of
|
||||
// every texel and wildly wrong for the fourth - alpha 1.0 would come back as 3/1023.
|
||||
TEST_F(NonCoreImageFormatScenario, TenTenTenTwoImageUsesItsOwnPerChannelDenominators) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
constexpr int kTexels = kExtent * kExtent;
|
||||
|
||||
std::vector<GLuint> seed(static_cast<std::size_t>(kTexels), 0u);
|
||||
std::vector<GLuint> seedCodes(static_cast<std::size_t>(kTexels) * 4u, 0u);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
const GLuint r = static_cast<GLuint>(texel) * 67u;
|
||||
const GLuint g = 1023u - static_cast<GLuint>(texel) * 13u;
|
||||
const GLuint b = 341u + static_cast<GLuint>(texel);
|
||||
const GLuint a = static_cast<GLuint>(texel) % 4u;
|
||||
seed[texel] = r | (g << 10) | (b << 20) | (a << 30);
|
||||
seedCodes[texel * 4 + 0] = r;
|
||||
seedCodes[texel * 4 + 1] = g;
|
||||
seedCodes[texel * 4 + 2] = b;
|
||||
seedCodes[texel * 4 + 3] = a;
|
||||
}
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -1.0f);
|
||||
const GLuint narrow =
|
||||
MakeTexture(GL_RGB10_A2, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV, seed.data());
|
||||
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
|
||||
if (narrow == 0 || wide == 0) return;
|
||||
|
||||
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rgb10_a2, binding = 0) readonly uniform image2D narrow;
|
||||
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(wide, coord, imageLoad(narrow, coord));
|
||||
}
|
||||
)");
|
||||
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rgb10_a2, binding = 0) writeonly uniform image2D narrow;
|
||||
|
||||
void main()
|
||||
{
|
||||
imageStore(narrow, ivec2(gl_GlobalInvocationID.xy), vec4(0.0, 0.5, 1.0, 1.0));
|
||||
}
|
||||
)");
|
||||
if (loadProgram == 0 || storeProgram == 0) return;
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGB10_A2, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
const auto denominator = channel == 3 ? 3.0f : 1023.0f;
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + channel],
|
||||
static_cast<float>(seedCodes[texel * 4 + channel]) / denominator)
|
||||
<< "texel " << texel << " channel " << channel;
|
||||
}
|
||||
}
|
||||
|
||||
// 0.5 through a TWO-bit channel is 1.5 of a code and rounds away from zero to 2, which
|
||||
// is 2/3 back - a value only the two-bit denominator can produce.
|
||||
BindImage(kNarrowUnit, narrow, GL_RGB10_A2, GL_WRITE_ONLY);
|
||||
Dispatch(storeProgram);
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGB10_A2, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * 1023.0), 0) << "texel " << texel << " red";
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 1] * 1023.0), 512) << "texel " << texel << " green";
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 2] * 1023.0), 1023) << "texel " << texel << " blue";
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 3] * 3.0), 3) << "texel " << texel << " alpha";
|
||||
}
|
||||
}
|
||||
|
||||
// The same carrier on a GL_TEXTURE_CUBE_MAP_ARRAY, the target the allTargets walkers reach
|
||||
// last and the one whose ES equivalent is addressed differently from its GL name (six
|
||||
// layer-faces per cube, as array layers). Nothing about the format conversion changes with
|
||||
// the target - which is exactly the claim, since the storage widening, the layered bind and
|
||||
// the per-layer readback all have their own code paths for this target alone.
|
||||
TEST_F(NonCoreImageFormatScenario, NormalizedImageCarriesEveryLayerFaceOfACubeMapArray) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
GLint maxComputeImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
|
||||
constexpr int kCubes = 2;
|
||||
constexpr int kLayerFaces = 6 * kCubes;
|
||||
constexpr int kTexelsPerFace = kExtent * kExtent;
|
||||
constexpr int kTexels = kTexelsPerFace * kLayerFaces;
|
||||
constexpr double kUnorm16Max = 65535.0;
|
||||
|
||||
std::vector<GLushort> seed(static_cast<std::size_t>(kTexels), 0);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
seed[texel] = static_cast<GLushort>((texel * 5477u) & 0xFFFFu);
|
||||
}
|
||||
const GLuint narrow =
|
||||
MakeCubeArrayTexture(GL_R16, GL_RED, GL_UNSIGNED_SHORT, seed.data(), kCubes);
|
||||
if (narrow == 0) return;
|
||||
|
||||
// THE UPLOAD, read back through glGetTexImage across every layer-face. A carrier that
|
||||
// widened the storage but seeded only the first face leaves the rest at zero, which is
|
||||
// what the per-layer readback path is there to catch, and this target is the only one
|
||||
// whose readback goes layer by layer.
|
||||
const std::vector<float> uploaded =
|
||||
ReadFloatsFrom(GL_TEXTURE_CUBE_MAP_ARRAY, narrow, GL_RED, 1, kTexels);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(uploaded[texel] * kUnorm16Max), seed[texel])
|
||||
<< "texel " << texel << " of " << kTexels;
|
||||
}
|
||||
|
||||
// ...and through an imageCubeArray, which is the declaration the shader half has to
|
||||
// carry for this target: a layered bind, an ivec3 coordinate whose z is the
|
||||
// layer-face, and the same unpack as every other target.
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexelsPerFace) * 4u, -1.0f);
|
||||
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
|
||||
if (wide == 0) return;
|
||||
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (r16, binding = 0) readonly uniform imageCubeArray narrow;
|
||||
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
|
||||
uniform int g_layerFace;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(wide, coord, imageLoad(narrow, ivec3(coord, g_layerFace)));
|
||||
}
|
||||
)");
|
||||
if (loadProgram == 0) return;
|
||||
|
||||
// Two faces, one of them past the first cube, so a carrier that addressed only the
|
||||
// first six layer-faces cannot pass.
|
||||
for (const int layerFace : {1, 9}) {
|
||||
glUseProgram(loadProgram);
|
||||
const GLint location = glGetUniformLocation(loadProgram, "g_layerFace");
|
||||
ASSERT_GE(location, 0) << "g_layerFace was not reflected";
|
||||
glUniform1i(location, layerFace);
|
||||
glUseProgram(0);
|
||||
|
||||
BindLayeredImage(kNarrowUnit, narrow, GL_R16, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
const std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexelsPerFace; ++texel) {
|
||||
const int sourceTexel = layerFace * kTexelsPerFace + texel;
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * kUnorm16Max), seed[sourceTexel])
|
||||
<< "layer-face " << layerFace << " texel " << texel;
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 1], 0.0f)
|
||||
<< "layer-face " << layerFace << " texel " << texel
|
||||
<< ": imageLoad on a one-channel format must report 0 for green";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f)
|
||||
<< "layer-face " << layerFace << " texel " << texel
|
||||
<< ": imageLoad on a format without alpha must report 1";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The other consumer of the same texture. A widened texture's ES storage really does have
|
||||
// four channels, so a sampler reading it raw would see whatever the carrier holds; the
|
||||
// logical format's missing channels have to keep reading 0 and 1 (which Espryt arranges
|
||||
|
||||
@@ -4000,6 +4000,7 @@ namespace {
|
||||
constexpr Uint kGlR32f = 0x822E;
|
||||
constexpr Uint kGlRgb10A2ui = 0x906F;
|
||||
constexpr Uint kGlRgb10A2 = 0x8059;
|
||||
constexpr Uint kGlRgb8 = 0x8051; // not one of the forty image formats at all
|
||||
} // namespace
|
||||
|
||||
// The KHR-GL4x.packed_depth_stencil.stencil_texturing compute shader, reduced: one format-less
|
||||
@@ -4050,20 +4051,26 @@ void main() { imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(15u,
|
||||
// pass on the ESSL chain and re-declares them in a core carrier SPIRV-Cross does print, so for
|
||||
// those the module is the right place and the text completion would put back the narrow token no
|
||||
// ES driver accepts. r8ui - which the stencil half of the packed_depth_stencil case binds - is
|
||||
// one of the rescued ones; rgb10_a2, whose 10/10/10/2 NORMALIZED channels no core format has, is
|
||||
// not.
|
||||
// one of the rescued ones, and so, now that the carriers cover all twenty-six non-core formats,
|
||||
// is every other IMAGE format. What is left for the guard is a format that is not an image format
|
||||
// at all: it has no carrier and no ESSL image spelling either, so baking it would put a token in
|
||||
// the module that means nothing.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesOnlyTheFormatsNoCoreCarrierRescues) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR8ui))
|
||||
<< "if SPIRV-Cross ever learns to print r8ui for ES, this route can go";
|
||||
ASSERT_NE(ShaderCompiler::WidenedCoreEsslImageFormat(kGlR8ui), 0u);
|
||||
// rgb10_a2ui is unprintable too and IS rescued: its channels are unsigned INTEGER, so an
|
||||
// rgba16ui holds all four of them outright.
|
||||
// Unprintable and rescued anyway: rgb10_a2ui's channels are unsigned INTEGER, so an rgba16ui
|
||||
// holds all four outright, and rgb10_a2's are the same channels read as NORMALIZED, which the
|
||||
// same carrier holds as their codes.
|
||||
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlRgb10A2ui));
|
||||
ASSERT_NE(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2ui), 0u);
|
||||
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlRgb10A2));
|
||||
ASSERT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2), 0u);
|
||||
ASSERT_NE(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2), 0u);
|
||||
// ...and the one the guard still turns away.
|
||||
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlRgb8));
|
||||
ASSERT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb8), 0u);
|
||||
ASSERT_TRUE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR32ui));
|
||||
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(kGlR8ui), "r8ui");
|
||||
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(0x8051 /*GL_RGB8*/), "");
|
||||
@@ -4078,7 +4085,7 @@ void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
|
||||
|
||||
{ // Unprintable AND uncarriable: declined, module untouched, and the stage still transpiles.
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlRgb10A2}}, baked));
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlRgb8}}, baked));
|
||||
EXPECT_EQ(baked, spirv) << "a format nothing can carry must leave the module untouched";
|
||||
EXPECT_FALSE(DecompileToEssl(baked).empty());
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ namespace {
|
||||
struct StorageImageType {
|
||||
Uint32 resultId = 0u;
|
||||
Uint32 format = 0u;
|
||||
Uint32 sampledTypeId = 0u;
|
||||
};
|
||||
|
||||
Vector<StorageImageType> CollectStorageImageTypes(const Vector<Uint32>& spirv) {
|
||||
@@ -96,11 +97,27 @@ namespace {
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpTypeImage || wordCount < 9u) return;
|
||||
if (words[7] != 2u) return;
|
||||
types.push_back(StorageImageType{words[1], words[8]});
|
||||
types.push_back(StorageImageType{words[1], words[8], words[2]});
|
||||
});
|
||||
return types;
|
||||
}
|
||||
|
||||
// "float" / "uint" / "int" / "" for a scalar numeric type id, which is the one thing that says
|
||||
// whether a declaration is still an image2D or has become a uimage2D.
|
||||
String ScalarTypeSpellingOf(const Vector<Uint32>& spirv, Uint32 typeId) {
|
||||
String spelling;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (words[1] != typeId) return;
|
||||
// OpTypeFloat words: 1 result id, 2 width. OpTypeInt adds 3 signedness.
|
||||
if (opcode == spv::Op::OpTypeFloat && wordCount >= 3u) {
|
||||
spelling = "float";
|
||||
} else if (opcode == spv::Op::OpTypeInt && wordCount >= 4u) {
|
||||
spelling = words[3] != 0u ? "int" : "uint";
|
||||
}
|
||||
});
|
||||
return spelling;
|
||||
}
|
||||
|
||||
// OpVectorShuffle words: 0 opcode/count, 1 result type, 2 result id, 3 vector 1, 4 vector 2,
|
||||
// 5.. the component selectors.
|
||||
struct VectorShuffle {
|
||||
@@ -269,15 +286,40 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
// rg16 is one of the SEVEN with no core carrier at all - core ESSL has no 16-bit normalized
|
||||
// format, so every candidate loses range or changes the component type the texture presents.
|
||||
// It must be left alone and keep the honest "no GLSL ES spelling" diagnostic instead.
|
||||
// rg16: TWO unsigned-normalized 16-bit channels, which core ESSL has no image format of any
|
||||
// width for. Carried as its own CODES in an rgba16ui, so the declaration comes out a
|
||||
// uimage2D and every access is wrapped in GL 4.6 2.3.5 as well as masked.
|
||||
const char* const kRg16LoadStore = R"(#version 430 core
|
||||
layout(rg16, binding = 0) uniform image2D img;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
|
||||
imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, 2.0, 3.0, 4.0));
|
||||
imageStore(img, ivec2(gl_FragCoord.xy), vec4(0.25, 0.5, 0.75, 1.0));
|
||||
fragColor = texel;
|
||||
}
|
||||
)";
|
||||
|
||||
// rgba16_snorm: the signed twin, whose code is a two's-complement 16-bit integer sitting in an
|
||||
// UNSIGNED carrier channel - so the load has to sign-extend it back and the store has to mask
|
||||
// it down, neither of which the unsigned conversion does.
|
||||
const char* const kRgba16SnormLoadStore = R"(#version 430 core
|
||||
layout(rgba16_snorm, binding = 0) uniform image2D img;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
|
||||
imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, -1.0, 0.5, -0.5));
|
||||
fragColor = texel;
|
||||
}
|
||||
)";
|
||||
|
||||
// rgb10_a2: FOUR normalized channels that are not all the same width, so its denominator is
|
||||
// (1023, 1023, 1023, 3) and one number would be wrong for a quarter of every texel.
|
||||
const char* const kRgb10A2LoadStore = R"(#version 430 core
|
||||
layout(rgb10_a2, binding = 0) uniform image2D img;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
|
||||
imageStore(img, ivec2(gl_FragCoord.xy), vec4(0.25, 0.5, 0.75, 1.0));
|
||||
fragColor = texel;
|
||||
}
|
||||
)";
|
||||
@@ -287,7 +329,7 @@ void main() {
|
||||
// the shader rewrite, the ES texture storage and the glBindImageTexture argument. If it drifts
|
||||
// the three stop agreeing, and a narrow texture read through a wide image goes out of bounds
|
||||
// silently on every driver tested.
|
||||
TEST(WidenImageFormats, NineteenNonCoreFormatsHaveALosslessCoreCarrier) {
|
||||
TEST(WidenImageFormats, TwentySixNonCoreFormatsHaveALosslessCoreCarrier) {
|
||||
struct Case {
|
||||
Uint requested;
|
||||
Uint carrier;
|
||||
@@ -319,6 +361,16 @@ TEST(WidenImageFormats, NineteenNonCoreFormatsHaveALosslessCoreCarrier) {
|
||||
// FOUR channels: 10, 10, 10 and 2 bits of unsigned integer all fit in sixteen, so nothing
|
||||
// is masked at all and only the packed TRANSFER is re-encoded.
|
||||
{0x906F, 0x8D76, 4, "GL_RGB10_A2UI -> GL_RGBA16UI"},
|
||||
// The seven NORMALIZED formats, carried as their own channel CODES in the same rgba16ui.
|
||||
// These are the entries whose carrier changes the shader-visible TYPE as well, which is
|
||||
// why every access through them is wrapped in GL 4.6 2.3.5 rather than only masked.
|
||||
{0x805B, 0x8D76, 4, "GL_RGBA16 -> GL_RGBA16UI"},
|
||||
{0x822C, 0x8D76, 2, "GL_RG16 -> GL_RGBA16UI"},
|
||||
{0x822A, 0x8D76, 1, "GL_R16 -> GL_RGBA16UI"},
|
||||
{0x8059, 0x8D76, 4, "GL_RGB10_A2 -> GL_RGBA16UI"},
|
||||
{0x8F9B, 0x8D76, 4, "GL_RGBA16_SNORM -> GL_RGBA16UI"},
|
||||
{0x8F99, 0x8D76, 2, "GL_RG16_SNORM -> GL_RGBA16UI"},
|
||||
{0x8F98, 0x8D76, 1, "GL_R16_SNORM -> GL_RGBA16UI"},
|
||||
};
|
||||
for (const Case& testCase : cases) {
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(testCase.requested), testCase.carrier)
|
||||
@@ -334,7 +386,7 @@ TEST(WidenImageFormats, NineteenNonCoreFormatsHaveALosslessCoreCarrier) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(WidenImageFormats, CoreFormatsAndTheSevenWithoutALosslessCarrierAreRefused) {
|
||||
TEST(WidenImageFormats, CoreFormatsAreRefused) {
|
||||
// The thirteen GLSL ES already has: nothing to carry.
|
||||
for (const Uint coreFormat : {0x8814u /*RGBA32F*/, 0x881Au /*RGBA16F*/, 0x822Eu /*R32F*/,
|
||||
0x8058u /*RGBA8*/, 0x8F97u /*RGBA8_SNORM*/, 0x8D82u /*RGBA32I*/,
|
||||
@@ -344,23 +396,63 @@ TEST(WidenImageFormats, CoreFormatsAndTheSevenWithoutALosslessCarrierAreRefused)
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(coreFormat), 0u)
|
||||
<< "core format 0x" << std::hex << coreFormat;
|
||||
}
|
||||
// The seven with no LOSSLESS core carrier: core ESSL has no 16-bit normalized format and no
|
||||
// 10-bit one, so every candidate for these either loses range or changes the component type
|
||||
// the texture presents to anything that samples it. Deliberately left to the honest
|
||||
// diagnostic. r11f_g11f_b10f is NOT among them - rgba16f holds every value it can - and
|
||||
// neither is rgb10_a2ui, whose channels are INTEGER and fit an rgba16ui outright.
|
||||
for (const Uint hardFormat : {0x8059u /*RGB10_A2*/, 0x805Bu /*RGBA16*/, 0x822Cu /*RG16*/,
|
||||
0x822Au /*R16*/, 0x8F9Bu /*RGBA16_SNORM*/, 0x8F99u /*RG16_SNORM*/,
|
||||
0x8F98u /*R16_SNORM*/}) {
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(hardFormat), 0u)
|
||||
<< "format without an exact carrier 0x" << std::hex << hardFormat;
|
||||
}
|
||||
// Not an image format at all.
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(0x8051 /*GL_RGB8*/), 0u);
|
||||
EXPECT_EQ(ShaderCompiler::ImageFormatChannelCount(0x8051 /*GL_RGB8*/), 0u);
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(0), 0u);
|
||||
}
|
||||
|
||||
// The denominators of GL 4.6 2.3.5, which is the whole difference between a carrier that holds a
|
||||
// format's VALUES and one that holds its CODES. Both halves of DirectGLES's transfer read them
|
||||
// (the upload's synthetic alpha and glGetTexImage's divide), and so does the shader rewrite, so a
|
||||
// wrong entry here is wrong in three places at once and consistently - which is exactly the kind
|
||||
// of error a round-trip test cannot see.
|
||||
TEST(WidenImageFormats, OnlyTheNormalizedFormatsCarryCodesAndTheirDenominatorsAreTheFormatsOwn) {
|
||||
struct Case {
|
||||
Uint format;
|
||||
Uint32 channelMax[4];
|
||||
bool isSigned;
|
||||
const char* name;
|
||||
};
|
||||
const Case cases[] = {
|
||||
{0x805B, {65535u, 65535u, 65535u, 65535u}, false, "GL_RGBA16"},
|
||||
{0x822C, {65535u, 65535u, 65535u, 65535u}, false, "GL_RG16"},
|
||||
{0x822A, {65535u, 65535u, 65535u, 65535u}, false, "GL_R16"},
|
||||
// The one format whose channels are not all the same width, and the reason the answer is
|
||||
// four numbers rather than one: a two-bit alpha saturates at 3, not at 1023.
|
||||
{0x8059, {1023u, 1023u, 1023u, 3u}, false, "GL_RGB10_A2"},
|
||||
{0x8F9B, {32767u, 32767u, 32767u, 32767u}, true, "GL_RGBA16_SNORM"},
|
||||
{0x8F99, {32767u, 32767u, 32767u, 32767u}, true, "GL_RG16_SNORM"},
|
||||
{0x8F98, {32767u, 32767u, 32767u, 32767u}, true, "GL_R16_SNORM"},
|
||||
};
|
||||
for (const Case& testCase : cases) {
|
||||
Uint32 channelMax[4] = {0u, 0u, 0u, 0u};
|
||||
bool isSigned = !testCase.isSigned;
|
||||
EXPECT_TRUE(ShaderCompiler::NormalizedImageCarrierCodes(testCase.format, channelMax, isSigned))
|
||||
<< testCase.name;
|
||||
for (Uint channel = 0; channel < 4; ++channel) {
|
||||
EXPECT_EQ(channelMax[channel], testCase.channelMax[channel])
|
||||
<< testCase.name << " channel " << channel;
|
||||
}
|
||||
EXPECT_EQ(isSigned, testCase.isSigned) << testCase.name;
|
||||
}
|
||||
|
||||
// Everything else keeps its own component type in the carrier, so nothing is converted: an
|
||||
// rg8's carrier channel really is an 8-bit unsigned normalized one, and an rgb10_a2ui's
|
||||
// channel really does hold the integer the shader stored.
|
||||
for (const Uint direct : {0x8230u /*RG32F*/, 0x8229u /*R8*/, 0x8F94u /*R8_SNORM*/,
|
||||
0x8232u /*R8UI*/, 0x8C3Au /*R11F_G11F_B10F*/, 0x906Fu /*RGB10_A2UI*/,
|
||||
0x8814u /*RGBA32F*/, 0x8051u /*RGB8, not an image format*/}) {
|
||||
Uint32 channelMax[4] = {7u, 7u, 7u, 7u};
|
||||
bool isSigned = true;
|
||||
EXPECT_FALSE(ShaderCompiler::NormalizedImageCarrierCodes(direct, channelMax, isSigned))
|
||||
<< "format 0x" << std::hex << direct;
|
||||
for (Uint channel = 0; channel < 4; ++channel) {
|
||||
EXPECT_EQ(channelMax[channel], 7u) << "a refused format must leave the output alone";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(WidenImageFormats, TwoChannelFloatImageBecomesRgba32fWithBothAccessesMasked) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRg32fLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
@@ -679,25 +771,132 @@ TEST(WidenImageFormats, CoreFormatModuleIsHandedBackUntouched) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(WidenImageFormats, FormatWithoutAnExactCarrierIsLeftAlone) {
|
||||
// The normalized carrier, which is the one that does not merely re-DECLARE the image: a 16-bit
|
||||
// normalized channel has no core ESSL format of any width behind it, and no FLOAT carrier is
|
||||
// honest either (a half has eleven mantissa bits against its sixteen), so what the rgba16ui holds
|
||||
// is the format's own CODE. That changes the shader-visible TYPE, which is the thing to check -
|
||||
// an image2D whose format moved to rgba16ui but whose sampled type stayed float is not merely
|
||||
// wrong, it is invalid SPIR-V, and a module that kept the float type while the STORAGE became an
|
||||
// integer texture would read whole texels as garbage.
|
||||
TEST(WidenImageFormats, NormalizedImageBecomesAUimageWhoseAccessesConvertItsCodes) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRg16LoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
|
||||
const auto beforeTypes = CollectStorageImageTypes(spirv);
|
||||
ASSERT_EQ(beforeTypes.size(), 1u);
|
||||
EXPECT_EQ(beforeTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16));
|
||||
|
||||
// rg16 has no core format with 16-bit unsigned-normalized channels behind it. Anything wider
|
||||
// would requantize differently from what the application asked for, so the pass declines and
|
||||
// CollectImageFormatBakeInputs reports the format as unspellable instead.
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
EXPECT_EQ(ScalarTypeSpellingOf(spirv, beforeTypes.front().sampledTypeId), "float");
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
||||
/*enableSpirvValidation=*/true);
|
||||
if (!widened.empty()) {
|
||||
const auto afterTypes = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16));
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
||||
/*enableSpirvValidation=*/true));
|
||||
ASSERT_FALSE(widened.empty());
|
||||
EXPECT_TRUE(Validates(widened));
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(widened));
|
||||
|
||||
const auto afterTypes = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba16ui));
|
||||
EXPECT_EQ(ScalarTypeSpellingOf(widened, afterTypes.front().sampledTypeId), "uint")
|
||||
<< "the carrier's component type is unsigned integer, and spirv-val requires the image's "
|
||||
"Sampled Type to say so";
|
||||
|
||||
// The masks are still there and still say what a two-channel format's surplus channels are -
|
||||
// the conversion wraps them, it does not replace them.
|
||||
const auto shuffles = CollectVectorShuffles(widened);
|
||||
const auto texelIds = CollectImageWriteTexelIds(widened);
|
||||
ASSERT_EQ(texelIds.size(), 1u);
|
||||
// The texel is now the PACKED value, so the mask is one step further back: find the shuffle
|
||||
// by its component selectors instead.
|
||||
Bool sawTwoChannelMask = false;
|
||||
for (const VectorShuffle& shuffle : shuffles) {
|
||||
sawTwoChannelMask = sawTwoChannelMask || HasComponents(shuffle, {0u, 1u, 6u, 7u});
|
||||
}
|
||||
EXPECT_TRUE(sawTwoChannelMask) << "expected the (r, g, 0, 1) mask a two-channel format needs";
|
||||
|
||||
// ...and the ESSL says the whole story: a uimage2D holding rgba16ui, divided and multiplied
|
||||
// by the format's own 65535.
|
||||
const EsslAttempt after = EmitEssl(widened);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
EXPECT_NE(after.text.find("uimage2D"), String::npos) << after.text;
|
||||
EXPECT_NE(after.text.find("rgba16ui"), String::npos) << after.text;
|
||||
EXPECT_EQ(after.text.find("rg16"), String::npos)
|
||||
<< "the token no ES driver accepts is still in the emitted source:\n"
|
||||
<< after.text;
|
||||
EXPECT_NE(after.text.find("65535.0"), String::npos)
|
||||
<< "the unsigned-normalized denominator is 2^16 - 1:\n"
|
||||
<< after.text;
|
||||
EXPECT_EQ(after.text.find("32767.0"), String::npos)
|
||||
<< "an unsigned format must not take the SIGNED denominator:\n"
|
||||
<< after.text;
|
||||
}
|
||||
|
||||
// The signed half, which needs two things the unsigned one does not: the code is sign-extended
|
||||
// out of the unsigned carrier channel on the way in, and the decode is max(c / 32767, -1) rather
|
||||
// than the bare division - GL clamps -2^15/32767 up to exactly -1.
|
||||
TEST(WidenImageFormats, SignedNormalizedImageSignExtendsItsCodeAndClampsAtMinusOne) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRgba16SnormLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, false, true));
|
||||
ASSERT_FALSE(widened.empty());
|
||||
EXPECT_TRUE(Validates(widened));
|
||||
|
||||
const auto afterTypes = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba16ui));
|
||||
EXPECT_EQ(ScalarTypeSpellingOf(widened, afterTypes.front().sampledTypeId), "uint");
|
||||
|
||||
// The sign extension is a shift PAIR, and the arithmetic one is what makes it a sign
|
||||
// extension rather than a zero extension.
|
||||
Bool sawShiftLeft = false;
|
||||
Bool sawArithmeticShiftRight = false;
|
||||
ForEachInstruction(widened, [&](spv::Op opcode, const Uint32*, Uint32) {
|
||||
sawShiftLeft = sawShiftLeft || opcode == spv::Op::OpShiftLeftLogical;
|
||||
sawArithmeticShiftRight = sawArithmeticShiftRight || opcode == spv::Op::OpShiftRightArithmetic;
|
||||
});
|
||||
EXPECT_TRUE(sawShiftLeft);
|
||||
EXPECT_TRUE(sawArithmeticShiftRight)
|
||||
<< "a logical shift right would read every negative code as a large positive one";
|
||||
|
||||
const EsslAttempt after = EmitEssl(widened);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
EXPECT_NE(after.text.find("uimage2D"), String::npos) << after.text;
|
||||
EXPECT_NE(after.text.find("rgba16ui"), String::npos) << after.text;
|
||||
EXPECT_EQ(after.text.find("rgba16_snorm"), String::npos) << after.text;
|
||||
EXPECT_NE(after.text.find("32767.0"), String::npos)
|
||||
<< "the signed-normalized denominator is 2^15 - 1:\n"
|
||||
<< after.text;
|
||||
EXPECT_NE(after.text.find("-1.0"), String::npos)
|
||||
<< "GL clamps the signed decode at -1:\n"
|
||||
<< after.text;
|
||||
}
|
||||
|
||||
// rgb10_a2, whose four channels are 10, 10, 10 and 2 bits: the only entry where one denominator
|
||||
// would be wrong for a channel that IS present, rather than for one the mask discards anyway.
|
||||
TEST(WidenImageFormats, TenTenTenTwoImageTakesAPerChannelDenominator) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRgb10A2LoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, false, true));
|
||||
ASSERT_FALSE(widened.empty());
|
||||
EXPECT_TRUE(Validates(widened));
|
||||
|
||||
const auto afterTypes = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba16ui));
|
||||
EXPECT_EQ(ScalarTypeSpellingOf(widened, afterTypes.front().sampledTypeId), "uint");
|
||||
|
||||
const EsslAttempt after = EmitEssl(widened);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
EXPECT_NE(after.text.find("1023.0"), String::npos) << after.text;
|
||||
EXPECT_NE(after.text.find("3.0"), String::npos)
|
||||
<< "the two-bit alpha saturates at 3, not at 1023:\n"
|
||||
<< after.text;
|
||||
EXPECT_EQ(after.text.find("rgb10_a2)"), String::npos) << after.text;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user