[Merge] (ShaderTranspiler, DirectGLES): land the normalized-format carriers and the buffer-image split

This commit is contained in:
2026-08-22 04:47:39 -04:00
13 changed files with 2501 additions and 145 deletions
+99 -13
View File
@@ -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);
@@ -1483,14 +1483,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
// already calls that undefined, and inventing a carrier for it would only make the
// out-of-class read wider.
//
// A BUFFER texture is excluded on both sides: it has no storage of its own to widen
// (its texels are the application's buffer object), so WidenImageFormatsPass declines
// every buffer image and the bind must decline with it, or the driver would be handed
// a carrier the shader never addressed. See the Dim::Buffer guard there for the
// 32-byte GL_RG32F measurement that pinned it.
// A BUFFER texture is excluded from the WIDENING on both sides: it has no storage of
// its own to widen (its texels are the application's buffer object), so
// WidenImageFormatsPass declines to widen every buffer image and the bind must decline
// with it, or the driver would be handed a carrier the shader never addressed. See the
// Dim::Buffer guard there for the 32-byte GL_RG32F measurement that pinned it.
//
// What a buffer image takes instead is the SPLIT, which is the same three-layer move
// through a different door: the glTexBuffer view above and the bind below both name
// the single-channel base format, and the shader subscripts it two components per
// original texel. Same gate on both sides, so the two cannot disagree.
GLenum bindFormat = imageBinding.Format;
if (imageBinding.Texture->GetTarget() != TextureTarget::TextureBuffer &&
TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) {
if (imageBinding.Texture->GetTarget() == TextureTarget::TextureBuffer) {
if (TextureImpl::GetImageBindableBufferSplitFormat(imageBinding.Texture->GetFormat()) !=
GL_UNKNOWN_MGL) {
if (const GLenum boundFormatSplit = TextureImpl::GetImageBindableBufferSplitFormat(
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
boundFormatSplit != GL_UNKNOWN_MGL) {
bindFormat = boundFormatSplit;
}
}
} else if (TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) {
const auto boundFormatWidening = TextureImpl::GetImageBindableStorageWidening(
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
if (boundFormatWidening) {
@@ -7687,9 +7700,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 +7741,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 +7771,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 +7838,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 +8511,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 +8635,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 +8659,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;
}
+131 -12
View File
@@ -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
@@ -2901,19 +2943,63 @@ namespace MobileGL::MG_Backend::DirectGLES {
return widenedData.data();
}
// The rgb10_a2 / rgb10_a2ui shadow split into the four GL_UNSIGNED_SHORT channel CODES its
// GL_RGBA16UI carrier is uploaded as. GL_UNSIGNED_INT_2_10_10_10_REV puts the FIRST
// component in the LOW bits (that is what REV means), so red is bits 0-9, green 10-19,
// blue 20-29 and alpha 30-31.
//
// The same split serves both formats: an rgb10_a2ui channel's code IS its value, and an
// rgb10_a2 channel's code is the numerator of value = code / (2^b - 1) that the shader-side
// unpack divides out. Neither is scaled here - the carrier holds the format's own bits.
//
// Sized from the LEVEL, not the source, for the reason PrepareChannelWidenedUpload is: the
// driver reads a full width*height*depth*4 shorts for the transfer it was handed.
const void* PreparePackedIntWidenedUpload(const IntVec3& texelSize, const void* data,
SizeT byteSize, Vector<Uint8>& widenedData) {
constexpr SizeT kSourceTexelBytes = sizeof(Uint32);
if (data == nullptr || byteSize < kSourceTexelBytes) {
return data;
}
const SizeT texelCount = static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (texelCount == 0) {
return data;
}
const SizeT copyTexelCount = std::min(texelCount, byteSize / kSourceTexelBytes);
widenedData.assign(texelCount * 4u * sizeof(Uint16), 0);
const auto* src = static_cast<const Uint8*>(data);
auto* dst = reinterpret_cast<Uint16*>(widenedData.data());
for (SizeT i = 0; i < texelCount; ++i, dst += 4) {
Uint32 packed = 0;
if (i < copyTexelCount) {
// Through a memcpy rather than a Uint32 read of `src`: the shadow is a byte
// buffer with no alignment promise of its own.
Memcpy(&packed, src + i * kSourceTexelBytes, sizeof(packed));
}
dst[0] = static_cast<Uint16>(packed & 0x3FFu);
dst[1] = static_cast<Uint16>((packed >> 10u) & 0x3FFu);
dst[2] = static_cast<Uint16>((packed >> 20u) & 0x3FFu);
dst[3] = static_cast<Uint16>((packed >> 30u) & 0x3u);
}
return widenedData.data();
}
// The transfer half of the image-format widening: an image-bindable texture whose ES
// storage was widened to a core carrier is described to the driver as a four-component
// transfer, so its narrower client data has to be repacked the same way the three-channel
// colour-renderable widening repacks its own.
//
// Two shapes, because the carriers come in two kinds. Seventeen of the eighteen keep the
// frontend format's component TYPE and only add channels, so padding the shadow out to
// four components is the whole conversion. r11f_g11f_b10f does not: its shadow is one
// PACKED 32-bit word per texel and its carrier is GL_RGBA16F, so the word has to be
// DECODED into four floats. Reading it as three components of the carrier's type - what
// the repack below would do - would take twelve bytes from a four-byte texel and shear
// the level, which is what the allFormats LOAD walkers see and the STORE ones do not (a
// store overwrites every texel the upload got wrong).
// Three shapes, because the carriers come in three kinds. Most of them keep the frontend
// format's component TYPE and only add channels, so padding the shadow out to four
// components is the whole conversion. The two PACKED formats do not: their shadow is one
// 32-bit word per texel, so the word has to be split - into four floats for
// r11f_g11f_b10f's GL_RGBA16F, into four shorts for rgb10_a2ui's GL_RGBA16UI. Reading such
// a word as components of the carrier's type - what the repack below would do - takes
// twelve or sixteen bytes from a four-byte texel and shears the level, which is what the
// allFormats LOAD walkers see and the STORE ones do not (a store overwrites every texel
// the upload got wrong).
//
// Composes with PrepareFallbackUpload rather than replacing it, and the composition is a
// no-op by construction: none of the widened formats is one GetWidenableClientComponentCount
@@ -2926,14 +3012,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!widening || widening.SourceChannels == 0 || widening.SourceChannels > 4) {
return data;
}
if (widening.PackedFloatSource) {
switch (widening.SourceEncoding) {
case TextureImpl::ImageWidenSourceEncoding::PackedFloat11f11f10f:
return PreparePackedFloatWidenedUpload(texelSize, data, byteSize, widenedData);
case TextureImpl::ImageWidenSourceEncoding::PackedInt2101010Rev:
return PreparePackedIntWidenedUpload(texelSize, data, byteSize, widenedData);
case TextureImpl::ImageWidenSourceEncoding::Components:
break;
}
if (widening.SourceChannels == 4) {
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
@@ -3730,6 +3822,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateTextureFormatInfo(textureBufferObject->GetFormat(), &glInternalFormat, &glFormat,
&glType, TextureTarget::TextureBuffer);
// The view half of the buffer-image SPLIT. A buffer texture has no storage of its
// own to widen, but the VIEW its format describes can be re-described one
// component at a time over the same bytes - rg32f over N texels is r32f over 2N -
// and WidenImageFormatsPass rewrites every access to subscript it that way. Only
// for a texture that is actually image-bound: a sampled-only buffer texture keeps
// the format the application asked for (see GetImageBindableBufferSplitFormat).
if (m_imageBindableStorageRequired) {
if (const GLenum splitFormat =
TextureImpl::GetImageBindableBufferSplitFormat(textureBufferObject->GetFormat());
splitFormat != GL_UNKNOWN_MGL) {
glInternalFormat = splitFormat;
}
}
if (needsRegeneration) {
// Desktop GL has had buffer textures core since 3.1 and MobileGL advertises a
@@ -5395,6 +5500,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
// here whose carrier has a different per-channel layout. See
// WidenImageFormatsPass.h.
case glslang::ElfR11fG11fB10f: return 0x8C3A; // GL_R11F_G11F_B10F
// 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;
}
+52 -2
View File
@@ -745,9 +745,21 @@ 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
// bits 0-9, green 10-19, blue 20-29, alpha 30-31. Pure CPU and context-free so a unit test
// can pin the exact fields; `widenedData` is the caller's scratch and has to outlive the
// returned pointer.
const void* PreparePackedIntWidenedUpload(const IntVec3& texelSize, const void* data, SizeT byteSize,
Vector<Uint8>& widenedData);
struct StateTextureBasicInfo { // Used for tracking texture state changes
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
@@ -782,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;
@@ -1168,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;
+51 -10
View File
@@ -277,21 +277,62 @@ namespace MobileGL::MG_Backend::DirectGLES {
// its own; this call is only here to spell the transfer pair that describes it.
MG_Util::TextureFormatProcessor::NormalizePixelFormat(carrier, Flags<PixelFormatNormalizeOptionBit>{},
nullptr, &widening.Format, &widening.Type);
// r11f_g11f_b10f is the one carrier that is not a channel widening, and the transfer
// pair has to say so. Every other entry keeps the frontend format's own component
// type - a GL_RG16F shadow is halves and so is its GL_RGBA16F carrier, so padding the
// channels is the whole conversion. This shadow is a PACKED 32-bit word (GL_RGB with
// GL_UNSIGNED_INT_10F_11F_11F_REV, TextureFormatProcessor::NormalizePixelFormat), and
// no ES driver accepts that type for a GL_RGBA16F level. GL_FLOAT is asked for
// instead - legal for GL_RGBA16F, and the type the unpack in
// PrepareImageWidenedUpload writes - so the two sides name the same layout.
if (internalFormat == TextureInternalFormat::R11FG11FB10F) {
// The two carriers that are not channel widenings, whose transfer pair has to say so.
// Every other entry keeps the frontend format's own component type - a GL_RG16F shadow
// is halves and so is its GL_RGBA16F carrier, so padding the channels is the whole
// conversion. These two shadows are a PACKED 32-bit word per texel
// (TextureFormatProcessor::NormalizePixelFormat), and no ES driver accepts either
// packed type for the carrier's level, so the transfer names the carrier's own layout
// and PrepareImageWidenedUpload splits the word into it.
switch (internalFormat) {
case TextureInternalFormat::R11FG11FB10F:
// GL_UNSIGNED_INT_10F_11F_11F_REV -> GL_RGBA / GL_FLOAT, legal for GL_RGBA16F.
widening.Format = GL_RGBA;
widening.Type = GL_FLOAT;
widening.PackedFloatSource = true;
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. 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;
}
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat) {
const GLenum requested = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
const auto base = static_cast<GLenum>(
MG_Util::ShaderTranspiler::ShaderCompiler::SplitCoreEsslBufferImageFormat(requested));
if (base == 0) {
return GL_UNKNOWN_MGL;
}
// EXACTLY the arming WidenImageFormatsForEssl uses, for the reason the widening's is:
// the shader, the glTexBuffer view and the glBindImageTexture argument must all split
// or none of them may, or the shader subscripts a view the buffer is not described as.
if (g_GLESCapabilities.SupportsExtendedImageFormats &&
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(requested)) {
return GL_UNKNOWN_MGL;
}
return base;
}
} // namespace TextureImpl
namespace PrgramImpl {
String ProcessOutColorLocations(const String& glslCode) {
+52 -7
View File
@@ -102,6 +102,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
// would see them. Closing it needs the per-draw-buffer colour mask the three-channel
// widening already carries (FramebufferImpl::g_alphaWidenedDrawBufferMask) generalized
// from "alpha" to a channel count, which is its own change.
// How the FRONTEND's CPU shadow for a widened format is laid out relative to the carrier's
// transfer, i.e. what the upload has to do to it. Almost every entry is `Components`: the
// shadow already holds SourceChannels components of exactly the carrier's own type, so
// padding it out to four is the whole conversion. The packed entries do not - their shadow
// is ONE 32-bit word per texel - and reading such a word as components of the carrier's
// type takes twelve or sixteen bytes out of four and shears the level.
enum class ImageWidenSourceEncoding : Uint8 {
Components = 0,
// r11f_g11f_b10f: GL_UNSIGNED_INT_10F_11F_11F_REV -> four GL_FLOATs of an rgba16f.
PackedFloat11f11f10f,
// rgb10_a2 and rgb10_a2ui: GL_UNSIGNED_INT_2_10_10_10_REV -> four GL_UNSIGNED_SHORT
// channel CODES of an rgba16ui. The same split serves both: the two formats differ
// only in what the codes MEAN, which is the shader's business and not the transfer's.
PackedInt2101010Rev,
};
struct ImageBindableStorageWidening {
GLenum InternalFormat = GL_UNKNOWN_MGL;
GLenum Format = GL_UNKNOWN_MGL;
@@ -113,17 +129,46 @@ namespace MobileGL::MG_Backend::DirectGLES {
// transfer type cannot tell the two apart (GL_UNSIGNED_BYTE serves both RG8 and
// RG8UI), so the carrier decides.
Bool IntegerData = false;
// The frontend shadow is a PACKED word rather than SourceChannels separate components
// of the carrier's own type, so the upload has to DECODE it instead of padding it out
// (PrepareImageWidenedUpload). True only for r11f_g11f_b10f, whose shadow is one
// GL_UNSIGNED_INT_10F_11F_11F_REV per texel and whose carrier is GL_RGBA16F: the
// channel repack every other entry uses would read three floats out of a four-byte
// texel and shear the level.
Bool PackedFloatSource = false;
// 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);
// The single-channel core format an image-bindable BUFFER texture's view is SPLIT into, or
// GL_UNKNOWN_MGL for a format that needs no split (or has no core base).
//
// A buffer texture cannot be widened: its texels are the application's buffer object, at
// the size and layout the application gave it, and it is usually also a vertex, index or
// storage buffer whose bytes are not ours to restride. But an rg32f view of N texels and
// an r32f view of 2N texels describe exactly the SAME bytes, so the split changes only
// how the shader subscripts them - component j of texel i is texel 2i + j of the base
// view - which WidenImageFormatsPass rewrites every access to do. The same rule as the
// widening decides WHETHER: a driver that can spell rg32f for an imageBuffer needs
// nothing.
//
// KNOWN GAP, and the reason this is not applied to a texture that is merely sampled: a
// buffer texture that is BOTH image-bound and read through a samplerBuffer would have its
// sampled view split too, and the sampler side is not rewritten. Accepted for the same
// reason the storage widening's gaps are - on a driver where the split applies at all
// there is no legal ESSL for the image declaration, so such a program did not compile.
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat);
} // namespace TextureImpl
namespace FramebufferImpl {} // namespace FramebufferImpl
@@ -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);
@@ -351,6 +409,93 @@ void main()
}
}
// GL_RGB10_A2UI, the format all four allFormats walkers stop at once r11f_g11f_b10f is
// carried - and the only widening whose carrier has as MANY channels as the original, so
// GL leaves nothing to pin and neither access is rewritten. What it does need is the other
// packed transfer: its shadow is one GL_UNSIGNED_INT_2_10_10_10_REV word per texel, which
// the GL_RGBA16UI carrier is uploaded as four shorts.
//
// The seed is checked through an imageLoad BEFORE anything is stored, for the reason the
// r11f case is: a sheared split still produces plausible integers, and a store would
// overwrite every texel the upload got wrong. Every channel of every texel is distinct,
// and the alpha values walk the whole 0..3 a two-bit channel has - a widening that pinned
// alpha to GL's "1" the way a three-channel one must would pass for texel 1 alone.
TEST_F(NonCoreImageFormatScenario, PackedIntegerImageSplitsItsUploadAndKeepsAllFourChannels) {
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> expected(static_cast<std::size_t>(kTexels) * 4u, 0u);
for (int texel = 0; texel < kTexels; ++texel) {
const GLuint r = static_cast<GLuint>(texel) * 7u; // 0 .. 105
const GLuint g = 1023u - static_cast<GLuint>(texel) * 11u; // 1023 .. 858
const GLuint b = 512u + static_cast<GLuint>(texel); // 512 .. 527
const GLuint a = static_cast<GLuint>(texel) % 4u; // the whole 0..3
seed[texel] = r | (g << 10) | (b << 20) | (a << 30);
expected[texel * 4 + 0] = r;
expected[texel * 4 + 1] = g;
expected[texel * 4 + 2] = b;
expected[texel * 4 + 3] = a;
}
const std::vector<GLuint> wideSeed(static_cast<std::size_t>(kTexels) * 4u, 999u);
const GLuint narrow =
MakeTexture(GL_RGB10_A2UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV, seed.data());
const GLuint wide = MakeTexture(GL_RGBA32UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT, 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_a2ui, binding = 0) readonly uniform uimage2D narrow;
layout (rgba32ui, binding = 1) writeonly uniform uimage2D 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_a2ui, binding = 0) writeonly uniform uimage2D narrow;
void main()
{
imageStore(narrow, ivec2(gl_GlobalInvocationID.xy), uvec4(11u, 22u, 33u, 2u));
}
)");
if (loadProgram == 0 || storeProgram == 0) return;
BindImage(kNarrowUnit, narrow, GL_RGB10_A2UI, GL_READ_ONLY);
BindImage(kWideUnit, wide, GL_RGBA32UI, GL_WRITE_ONLY);
Dispatch(loadProgram);
const std::vector<GLuint> loaded = ReadUints(wide, GL_RGBA_INTEGER, 4);
for (int texel = 0; texel < kTexels; ++texel) {
EXPECT_EQ(loaded[texel * 4 + 0], expected[texel * 4 + 0]) << "texel " << texel << " red";
EXPECT_EQ(loaded[texel * 4 + 1], expected[texel * 4 + 1]) << "texel " << texel << " green";
EXPECT_EQ(loaded[texel * 4 + 2], expected[texel * 4 + 2]) << "texel " << texel << " blue";
EXPECT_EQ(loaded[texel * 4 + 3], expected[texel * 4 + 3]) << "texel " << texel << " alpha";
}
// THE STORE. All four channels survive - this is the one widened format where GL drops
// nothing, so a mask here would be a bug rather than the emulation.
BindImage(kNarrowUnit, narrow, GL_RGB10_A2UI, GL_WRITE_ONLY);
Dispatch(storeProgram);
const std::vector<GLuint> stored = ReadUints(narrow, GL_RGBA_INTEGER, 4);
for (int texel = 0; texel < kTexels; ++texel) {
EXPECT_EQ(stored[texel * 4 + 0], 11u) << "texel " << texel << " red";
EXPECT_EQ(stored[texel * 4 + 1], 22u) << "texel " << texel << " green";
EXPECT_EQ(stored[texel * 4 + 2], 33u) << "texel " << texel << " blue";
EXPECT_EQ(stored[texel * 4 + 3], 2u) << "texel " << texel << " alpha";
}
}
// GL_R8UI: the only format KHR-GL43.shader_image_load_store.single-byte_data_alignment
// declares, and one SPIRV-Cross refuses to print for ESSL at all, so before the emulation
// no text was produced for the stage and the dispatch could not run.
@@ -419,6 +564,517 @@ 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";
}
}
}
// A BUFFER image, which takes neither of the emulations above. Its texels are the
// application's buffer object - at the size and layout the application gave it, and
// usually also a vertex, index or storage buffer - so there is nothing to reallocate a
// carrier in. What CAN be done is a SPLIT: rg32f over N texels and r32f over 2N texels
// describe exactly the same bytes, so the view is re-declared and every subscript is
// doubled (WidenImageFormatsPass, and the matching glTexBuffer/glBindImageTexture format
// in TextureImpl).
//
// THE NUMBERS HERE ARE THE ONES THAT PINNED THE OLD BUG. Widening a buffer image instead
// leaves the shader striding 16 bytes through 8-byte texels: measured on an Adreno 830
// with this exact 32-byte GL_RG32F buffer and this exact shader, the readback came back
// [1,100] [0,1] [2,100] [0,1] - texels 0 and 1 landed on top of all four, and texels 2 and
// 3 were written past the end of the application's buffer.
//
// This runs on every backend, and on a driver that CAN spell rg32f for an imageBuffer
// (Mesa's, which the software lanes use) nothing is split at all - which is the other half
// of the claim: the arming has to agree with the shader, so a split that fired where the
// driver needed none would double every subscript and fail here just as loudly.
TEST_F(NonCoreImageFormatScenario, BufferImageAddressesTheApplicationsOwnTexels) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
GLint maxTextureBufferSize = 0;
glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTextureBufferSize);
while (glGetError() != GL_NO_ERROR) {
}
if (maxTextureBufferSize <= 0) GTEST_SKIP() << "no buffer textures on this driver";
constexpr int kBufferTexels = 4;
const std::vector<float> seed(static_cast<std::size_t>(kBufferTexels) * 2u, -1.0f);
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(seed.size() * sizeof(float)), seed.data(),
GL_DYNAMIC_DRAW);
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_BUFFER, texture);
glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, buffer);
if (const GLenum error = FirstGLError()) {
glDeleteBuffers(1, &buffer);
GTEST_SKIP() << "glTexBuffer(GL_RG32F) errored with " << GLErrorName(error);
}
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (rg32f, binding = 0) writeonly uniform imageBuffer narrow;
void main()
{
int texel = int(gl_GlobalInvocationID.x);
imageStore(narrow, texel, vec4(float(texel + 1), 100.0, 3.0, 4.0));
}
)");
if (storeProgram == 0) {
glDeleteBuffers(1, &buffer);
return;
}
BindImage(kNarrowUnit, texture, GL_RG32F, GL_WRITE_ONLY);
glUseProgram(storeProgram);
glDispatchCompute(kBufferTexels, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
glUseProgram(0);
std::vector<float> readback(seed.size(), -12345.0f);
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
glGetBufferSubData(GL_TEXTURE_BUFFER, 0,
static_cast<GLsizeiptr>(readback.size() * sizeof(float)), readback.data());
EXPECT_EQ(FirstGLError(), 0u) << "reading the buffer back errored";
for (int texel = 0; texel < kBufferTexels; ++texel) {
EXPECT_FLOAT_EQ(readback[texel * 2 + 0], static_cast<float>(texel + 1))
<< "texel " << texel << " red";
EXPECT_FLOAT_EQ(readback[texel * 2 + 1], 100.0f) << "texel " << texel << " green";
}
glBindBuffer(GL_TEXTURE_BUFFER, 0);
glDeleteBuffers(1, &buffer);
while (glGetError() != GL_NO_ERROR) {
}
}
// 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
+16 -3
View File
@@ -3999,6 +3999,8 @@ namespace {
constexpr Uint kGlR8ui = 0x8232;
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
@@ -4049,15 +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_a2ui, whose 10/10/10/2 channel widths 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);
// 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_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2ui), 0u);
ASSERT_NE(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2ui), 0u);
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlRgb10A2));
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*/), "");
@@ -4072,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", kGlRgb10A2ui}}, 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 {
@@ -243,8 +260,9 @@ void main() {
}
)";
// rg32f again, but as a BUFFER image. Same format, same carrier on paper - and it must be
// left alone anyway, because a buffer image's texels are the application's buffer object.
// rg32f again, but as a BUFFER image. Same format, and NOT the same emulation: a buffer
// image's texels are the application's buffer object, so there is nothing to reallocate a
// carrier in - but the same bytes can be VIEWED as twice as many r32f texels, which is exact.
const char* const kRg32fBufferLoadStore = R"(#version 430 core
layout(rg32f, binding = 0) uniform imageBuffer img;
out vec4 fragColor;
@@ -255,15 +273,76 @@ void main() {
}
)";
// rg16 is one of the EIGHT 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.
// ...and one that asks the image how big it is, which the split has to halve: the ES view has
// twice the texels the application's format describes.
const char* const kRg32fBufferSize = R"(#version 430 core
layout(rg32f, binding = 0) uniform imageBuffer img;
out vec4 fragColor;
void main() {
fragColor = vec4(float(imageSize(img)));
}
)";
// rg16f as a buffer image: two channels of 16-bit float, whose single-channel base r16f core
// ESSL does not have. Nothing to split it into, so it keeps the honest failure.
const char* const kRg16fBufferLoadStore = R"(#version 430 core
layout(rg16f, binding = 0) uniform imageBuffer img;
out vec4 fragColor;
void main() {
vec4 texel = imageLoad(img, int(gl_FragCoord.x));
imageStore(img, int(gl_FragCoord.x), vec4(1.0, 2.0, 3.0, 4.0));
fragColor = texel;
}
)";
// rgb10_a2ui: FOUR unsigned-integer channels of 10, 10, 10 and 2 bits, carried in an rgba16ui
// that gives each of them sixteen. The only widening whose carrier has as many channels as the
// original, so it is the only one where GL leaves NOTHING to pin and both accesses must come
// out exactly as glslang emitted them.
const char* const kRgb10A2uiLoadStore = R"(#version 430 core
layout(rgb10_a2ui, binding = 0) uniform uimage2D img;
out vec4 fragColor;
void main() {
uvec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
imageStore(img, ivec2(gl_FragCoord.xy), uvec4(7u, 8u, 9u, 3u));
fragColor = vec4(texel);
}
)";
// 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;
}
)";
@@ -273,7 +352,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, EighteenNonCoreFormatsHaveALosslessCoreCarrier) {
TEST(WidenImageFormats, TwentySixNonCoreFormatsHaveALosslessCoreCarrier) {
struct Case {
Uint requested;
Uint carrier;
@@ -302,6 +381,19 @@ TEST(WidenImageFormats, EighteenNonCoreFormatsHaveALosslessCoreCarrier) {
// is e5m5 against a half's s1e5m10, so the carrier is still lossless - and three channels,
// so the mask has to pin only alpha.
{0x8C3A, 0x881A, 3, "GL_R11F_G11F_B10F -> GL_RGBA16F"},
// 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)
@@ -317,7 +409,7 @@ TEST(WidenImageFormats, EighteenNonCoreFormatsHaveALosslessCoreCarrier) {
}
}
TEST(WidenImageFormats, CoreFormatsAndTheEightWithoutALosslessCarrierAreRefused) {
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*/,
@@ -327,24 +419,63 @@ TEST(WidenImageFormats, CoreFormatsAndTheEightWithoutALosslessCarrierAreRefused)
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(coreFormat), 0u)
<< "core format 0x" << std::hex << coreFormat;
}
// The eight 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, so it is
// carried above.
for (const Uint hardFormat : {0x8059u /*RGB10_A2*/,
0x906Fu /*RGB10_A2UI*/, 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());
@@ -429,6 +560,38 @@ TEST(WidenImageFormats, ThreeChannelPackedFloatImageBecomesRgba16fWithOnlyAlphaP
EXPECT_TRUE(HasComponents(*loadMask, {0u, 1u, 2u, 7u}));
}
// The four-channel case, which is the whole of rgb10_a2ui's shader-side emulation: the carrier has
// as many channels as the original, every value of every channel fits, and GL therefore defines
// NOTHING about a surplus channel because there is none. So both accesses have to come out
// untouched - a pass that masked here would replace the alpha the application stored (0..3 of a
// two-bit channel, which the CTS walker writes as 3) with the constant 1 and drop blue outright.
TEST(WidenImageFormats, FourChannelIntegerImageBecomesRgba16uiWithNeitherAccessMasked) {
const Vector<Uint32> spirv = CompileFragment(kRgb10A2uiLoadStore);
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::Rgb10a2ui));
Vector<Uint32> widened;
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));
// The declaration moved and nothing else did.
EXPECT_EQ(CollectVectorShuffles(widened).size(), CollectVectorShuffles(spirv).size())
<< "a carrier with as many channels as the original must add no mask";
EXPECT_EQ(CollectImageReadResultIds(widened).size(), CollectImageReadResultIds(spirv).size())
<< "the imageLoad was duplicated for a rewrite that has nothing to rewrite";
}
// ...and the same module through the emitter, which is where the failure actually showed: ESSL has
// no `r11f_g11f_b10f` token, SPIRV-Cross throws for it, and the throw took every image uniform
// declared in the same stage with it.
@@ -451,37 +614,157 @@ TEST(WidenImageFormats, PackedFloatImageOnlyReachesEsslThroughTheCarrier) {
EXPECT_EQ(after.text.find("r11f_g11f_b10f"), String::npos) << after.text;
}
// A BUFFER image is declined whatever its format, and the format alone cannot say so - rg32f is
// carried exactly when it is an image2D. What makes the difference is that widening REALLOCATES
// the texture behind the image in the carrier, and a buffer image has no texture storage to
// reallocate: its texels are the application's buffer object, usually also a vertex, index or
// storage buffer. Widening one leaves the shader striding 16 bytes through 8-byte texels - the
// measured symptom on an Adreno 830 was a 32-byte GL_RG32F buffer reading back
// A BUFFER image is never WIDENED, whatever its format, and the format alone cannot say so -
// rg32f is carried in an rgba32f when it is an image2D. What makes the difference is that widening
// REALLOCATES the texture behind the image in the carrier, and a buffer image has no texture
// storage to reallocate: its texels are the application's buffer object, usually also a vertex,
// index or storage buffer. Widening one leaves the shader striding 16 bytes through 8-byte texels
// - the measured symptom on an Adreno 830 was a 32-byte GL_RG32F buffer reading back
// [1,100] [0,1] [2,100] [0,1] instead of [1,100] [2,100] [3,100] [4,100], with the last two texels
// written past the end of the application's buffer.
TEST(WidenImageFormats, BufferImagesAreDeclinedEvenWhenTheirFormatHasACarrier) {
//
// It is SPLIT instead, which is the opposite move: the bytes stay exactly where they are and the
// SUBSCRIPT changes. rg32f over N texels and r32f over 2N texels describe the same memory, so
// component j of texel i is texel 2i + j, and the base format is one of the thirteen ES has.
TEST(WidenImageFormats, BufferImagesAreSplitByTheSubscriptRatherThanWidened) {
const Vector<Uint32> spirv = CompileFragment(kRg32fBufferLoadStore);
ASSERT_FALSE(spirv.empty());
const auto beforeTypes = CollectStorageImageTypes(spirv);
ASSERT_EQ(beforeTypes.size(), 1u);
EXPECT_EQ(beforeTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg32f))
<< "the fixture stopped declaring the format this test is about";
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
Vector<Uint32> split;
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, split, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
/*enableSpirvValidation=*/true));
ASSERT_FALSE(split.empty());
EXPECT_TRUE(Validates(split));
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(split));
const auto afterTypes = CollectStorageImageTypes(split);
ASSERT_EQ(afterTypes.size(), 1u);
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::R32f))
<< "the base format is the SINGLE-channel one, not the four-channel carrier a 2D image "
"would take - a buffer image that gained texel width would run off the end of the "
"application's buffer";
// ONE imageLoad became TWO, and ONE imageStore became two as well: each component of the
// original texel is its own texel of the base view.
EXPECT_EQ(CollectImageReadResultIds(split).size(), 2u * CollectImageReadResultIds(spirv).size());
EXPECT_EQ(CollectImageWriteTexelIds(split).size(), 2u * CollectImageWriteTexelIds(spirv).size());
// ...and the store's two texels are the two components, not the same one twice.
const auto shuffles = CollectVectorShuffles(split);
const auto texelIds = CollectImageWriteTexelIds(split);
ASSERT_EQ(texelIds.size(), 2u);
const VectorShuffle* firstTexel = FindShuffleWithResult(shuffles, texelIds[0]);
const VectorShuffle* secondTexel = FindShuffleWithResult(shuffles, texelIds[1]);
ASSERT_NE(firstTexel, nullptr);
ASSERT_NE(secondTexel, nullptr);
EXPECT_TRUE(HasComponents(*firstTexel, {0u, 4u, 4u, 7u}))
<< "expected (r, 0, 0, 1) - component 0 of the texel into a one-channel base format";
EXPECT_TRUE(HasComponents(*secondTexel, {1u, 4u, 4u, 7u}))
<< "expected (g, 0, 0, 1) - component 1 into the NEXT base texel";
// The subscript arithmetic itself: one multiply and one add per access.
Uint32 multiplies = 0;
Uint32 adds = 0;
ForEachInstruction(split, [&](spv::Op opcode, const Uint32*, Uint32) {
if (opcode == spv::Op::OpIMul) ++multiplies;
if (opcode == spv::Op::OpIAdd) ++adds;
});
EXPECT_GE(multiplies, 2u) << "2i, once for the load and once for the store";
EXPECT_GE(adds, 2u) << "2i + 1, once for the load and once for the store";
// And what reaches the driver names a format ES has.
const EsslAttempt after = EmitEssl(split);
ASSERT_TRUE(after.succeeded) << after.error;
EXPECT_NE(after.text.find("r32f"), String::npos) << after.text;
EXPECT_EQ(after.text.find("rg32f"), String::npos)
<< "the token no ES driver accepts is still in the emitted source:\n"
<< after.text;
}
// imageSize() has to be halved with everything else: the ES view really does have twice the texels
// the application's format describes, so a shader that walks the buffer by its own size would run
// off the end of it - or, on a well-behaved driver, spend half its invocations past the data.
TEST(WidenImageFormats, ASplitBufferImageReportsTheSizeItsOwnFormatDescribes) {
const Vector<Uint32> spirv = CompileFragment(kRg32fBufferSize);
ASSERT_FALSE(spirv.empty());
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
Vector<Uint32> split;
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, split, false, true));
ASSERT_FALSE(split.empty());
EXPECT_TRUE(Validates(split));
Uint32 sizeQueries = 0;
Uint32 divisions = 0;
ForEachInstruction(split, [&](spv::Op opcode, const Uint32*, Uint32) {
if (opcode == spv::Op::OpImageQuerySize) ++sizeQueries;
if (opcode == spv::Op::OpSDiv || opcode == spv::Op::OpUDiv) ++divisions;
});
EXPECT_EQ(sizeQueries, 1u) << "the query itself is not duplicated, only divided";
EXPECT_EQ(divisions, 1u);
const EsslAttempt after = EmitEssl(split);
ASSERT_TRUE(after.succeeded) << after.error;
EXPECT_NE(after.text.find("imageSize"), String::npos) << after.text;
EXPECT_NE(after.text.find("/ 2"), String::npos)
<< "the reported size must be the application's, not the base view's:\n"
<< after.text;
}
// A buffer image whose base format is NOT core ESSL has nothing to split into, and must keep the
// honest "no GLSL ES spelling" failure rather than take a wider one: rg16f's components are 16-bit
// floats and core ESSL has no r16f, so a split would have to change the component type.
TEST(WidenImageFormats, ABufferImageWithNoCoreBaseFormatIsLeftAlone) {
const Vector<Uint32> spirv = CompileFragment(kRg16fBufferLoadStore);
ASSERT_FALSE(spirv.empty());
const auto types = CollectStorageImageTypes(spirv);
ASSERT_EQ(types.size(), 1u);
EXPECT_EQ(types.front().format, static_cast<Uint32>(spv::ImageFormat::Rg32f))
<< "the fixture stopped declaring the format this test is about";
EXPECT_EQ(types.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16f));
// The gate says no, so the optimizer is never even run for it...
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
// ...and running it anyway changes nothing, which is what keeps the gate and the pass from
// disagreeing about a module.
Vector<Uint32> widened;
ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
/*enableSpirvValidation=*/true);
EXPECT_TRUE(widened.empty() || widened == spirv) << "a buffer image was rewritten";
Vector<Uint32> split;
ShaderCompiler::WidenImageFormatsForEssl(spirv, split, false, true);
if (!split.empty()) {
const auto afterTypes = CollectStorageImageTypes(split);
ASSERT_EQ(afterTypes.size(), 1u);
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16f));
}
}
// The same format in a NON-buffer image still widens, or this test would pass for the wrong
// reason - a widening that had simply stopped working.
const Vector<Uint32> planar = CompileFragment(kRg32fLoadStore);
ASSERT_FALSE(planar.empty());
EXPECT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(planar));
// The table the three layers share, from the other side: only the 32-bit component family has a
// core single-channel base, and a two-dimensional image never takes this route.
TEST(WidenImageFormats, OnlyTheThirtyTwoBitTwoChannelFormatsSplitAsBufferImages) {
struct Case {
Uint format;
Uint base;
const char* name;
};
const Case cases[] = {
{0x8230, 0x822E, "GL_RG32F -> GL_R32F"},
{0x823B, 0x8235, "GL_RG32I -> GL_R32I"},
{0x823C, 0x8236, "GL_RG32UI -> GL_R32UI"},
};
for (const Case& testCase : cases) {
EXPECT_EQ(ShaderCompiler::SplitCoreEsslBufferImageFormat(testCase.format), testCase.base)
<< testCase.name;
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(testCase.base)) << testCase.name;
EXPECT_EQ(ShaderCompiler::ImageFormatChannelCount(testCase.base), 1u) << testCase.name;
}
// No core single-channel base of the right component type, so no split.
for (const Uint refused : {0x822Fu /*RG16F*/, 0x8239u /*RG16I*/, 0x823Au /*RG16UI*/, 0x822Bu /*RG8*/,
0x8F95u /*RG8_SNORM*/, 0x822Cu /*RG16*/, 0x8237u /*RG8I*/, 0x8238u /*RG8UI*/,
// Already core, or four-channel, or not an image format at all.
0x8814u /*RGBA32F*/, 0x822Eu /*R32F*/, 0x8051u /*RGB8*/, 0u}) {
EXPECT_EQ(ShaderCompiler::SplitCoreEsslBufferImageFormat(refused), 0u)
<< "format 0x" << std::hex << refused;
}
}
TEST(WidenImageFormats, SingleChannelUnsignedImageBecomesRgba8uiWithBothAccessesMasked) {
@@ -631,25 +914,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;
}
+50
View File
@@ -5286,3 +5286,53 @@ TEST_F(TextureTest, ImageWidenedUploadExpandsOneAndTwoChannelDataWithGLsMissingC
EXPECT_TRUE(widened.empty());
}
}
// The OTHER transfer shape the image widening needs, and the one a channel repack cannot serve:
// GL_RGB10_A2UI's shadow is ONE 32-bit word per texel, not four components of the GL_RGBA16UI
// carrier's own type. Repacking it as components would take sixteen bytes out of a four-byte texel
// and shear the level - which only a LOAD notices, because a store overwrites whatever the upload
// got wrong.
//
// GL_UNSIGNED_INT_2_10_10_10_REV puts the FIRST component in the LOW bits, which is the whole
// content of the word "REV" and the single thing this can get backwards, so every field here is a
// different value and the boundary codes (0, the 10-bit maximum, the 2-bit maximum) are pinned
// exactly rather than compared with a tolerance.
TEST_F(TextureTest, ImageWidenedUploadSplitsAPacked2101010RevShadowIntoFourChannelCodes) {
using MobileGL::MG_Backend::DirectGLES::TextureImpl::PreparePackedIntWidenedUpload;
const IntVec3 texelSize(3, 1, 1);
// r=1, g=2, b=3, a=1 | r=1023, g=0, b=1023, a=3 | r=0, g=1023, b=0, a=0
const Uint32 source[] = {
1u | (2u << 10) | (3u << 20) | (1u << 30),
1023u | (0u << 10) | (1023u << 20) | (3u << 30),
0u | (1023u << 10) | (0u << 20) | (0u << 30),
};
Vector<Uint8> widened;
const auto* result = static_cast<const Uint16*>(
PreparePackedIntWidenedUpload(texelSize, source, sizeof(source), widened));
ASSERT_NE(result, static_cast<const void*>(source));
ASSERT_EQ(widened.size(), 12 * sizeof(Uint16));
const Uint16 expected[] = {1, 2, 3, 1, 1023, 0, 1023, 3, 0, 1023, 0, 0};
for (SizeT i = 0; i < 12; ++i) {
EXPECT_EQ(result[i], expected[i]) << "component " << i;
}
// Sized from the LEVEL, never from the source: the driver reads a full width*height*4 shorts
// for the transfer it was handed, so a short source still has to leave a full destination.
{
Vector<Uint8> shortWidened;
const auto* shortResult = static_cast<const Uint16*>(
PreparePackedIntWidenedUpload(texelSize, source, sizeof(Uint32), shortWidened));
ASSERT_EQ(shortWidened.size(), 12 * sizeof(Uint16));
for (SizeT i = 4; i < 12; ++i) {
EXPECT_EQ(shortResult[i], 0u) << "component " << i << " past the source must be zero";
}
}
// Nothing to split.
{
Vector<Uint8> empty;
EXPECT_EQ(PreparePackedIntWidenedUpload(texelSize, nullptr, 0, empty), nullptr);
EXPECT_TRUE(empty.empty());
}
}
@@ -1024,6 +1024,16 @@ namespace MobileGL {
return WidenImageFormatsPass::ImageFormatChannelCount(glInternalFormat);
}
bool ShaderCompiler::NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
bool& outSignedNormalized) {
return WidenImageFormatsPass::NormalizedImageCarrierCodes(glInternalFormat, outChannelMax,
outSignedNormalized);
}
Uint ShaderCompiler::SplitCoreEsslBufferImageFormat(Uint glInternalFormat) {
return WidenImageFormatsPass::SplitCoreEsslBufferImageFormat(glInternalFormat);
}
bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
const std::set<String>& blockNames,
std::set<String>& flattenedBlockNames,
@@ -298,6 +298,17 @@ 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);
// The single-channel core format a non-core BUFFER image is SPLIT into, or 0. See
// WidenImageFormatsPass::SplitCoreEsslBufferImageFormat - DirectGLES asks it for
// glTexBuffer's internal format and for glBindImageTexture's.
static Uint SplitCoreEsslBufferImageFormat(Uint glInternalFormat);
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
File diff suppressed because it is too large Load Diff
@@ -65,12 +65,18 @@ namespace MobileGL {
// the SPIRV-Cross throw takes the whole stage, every image uniform declared beside it
// included.
//
// The other EIGHT (rgb10_a2, rgb10_a2ui, rgba16, rg16, r16, rgba16_snorm, rg16_snorm,
// r16_snorm) are deliberately NOT widened here: core ESSL has no 16-bit normalized
// format at all and no 10-bit one, so every carrier for them either loses range or
// changes the component TYPE the texture a `sampler2D` would read presents. They keep
// the honest "no GLSL ES spelling" diagnostic instead of silently changing an
// application's numeric domain.
// rgb10_a2ui takes rgba16ui for a simpler reason still: its channels are 10, 10, 10 and
// 2 bits of UNSIGNED INTEGER, and rgba16ui gives each of them sixteen. Same component
// type, same channel COUNT, every value representable - so no access is rewritten at
// all, and only the TRANSFER differs (its shadow is one packed 32-bit word per texel,
// which the upload splits into four shorts).
//
// The other SEVEN (rgb10_a2, rgba16, rg16, r16, rgba16_snorm, rg16_snorm, r16_snorm)
// are deliberately NOT widened here: core ESSL has no 16-bit normalized format at all
// and no 10-bit one, so every carrier for them either loses range or changes the
// component TYPE the texture a `sampler2D` would read presents. They keep the honest
// "no GLSL ES spelling" diagnostic instead of silently changing an application's
// numeric domain.
//
// MUST MOVE WITH THE OTHER TWO LAYERS. The widening is not a shader-local rewrite: the
// ES texture behind the image has to be allocated in the carrier format too, and
@@ -130,6 +136,31 @@ 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);
// The core-ESSL single-channel format a non-core BUFFER image is SPLIT into, or 0
// when the format needs no split or has no core single-channel base. A buffer
// image cannot be WIDENED - its texels are the application's buffer object, which
// has no room to restride - but rg32f over N texels and r32f over 2N texels
// describe exactly the same bytes, so the shader reads and writes each component
// by itself at 2i and 2i+1 instead. DirectGLES asks this for glTexBuffer's
// internal format and for glBindImageTexture's, which have to name the same view
// the shader addresses.
static Uint SplitCoreEsslBufferImageFormat(Uint glInternalFormat);
static spvtools::Optimizer::PassToken CreateWidenImageFormatsPass(
bool onlyFormatsSpirvCrossRefusesToPrint = false);