diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index d91b12ff..69fb6772 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -1088,7 +1088,7 @@ namespace MobileGL::MG_Backend::DirectGLES { backendObj = MakeShared(); } if (imageBindableStorageRequired) { - backendObj->RequireImageBindableStorage(); + backendObj->RequireImageBindableStorage(textureObject); } backendObj->SyncTextureParamsToBackend(textureObject); backendObj->SyncBuiltinSamplerToBackend(textureObject); @@ -7687,9 +7687,25 @@ namespace MobileGL::MG_Backend::DirectGLES { // scratch framebuffer, so the frontend's READ binding describes a different image entirely - // consulting it there would both miss real widenings and corrupt readbacks of ordinary // textures taken while some unrelated widened attachment happened to be bound. + // The image-format widening's READ half, for the seven normalized formats whose carrier holds + // their channels as INTEGER CODES (GL_RGBA16 stored as a GL_RGBA16UI - see + // TextureImpl::GetImageBindableStorageWidening). Nothing else in the readback would get those + // right: the attachment is an integer one while the application's format is normalized, so the + // class check below would refuse the read outright, and a repack that got past it would hand + // back 65535.0 where GL owes 1.0. + // + // Inactive (ChannelMax all zero) for every other read, which is all but a handful. + struct NormalizedImageCarrierRead { + Uint ChannelMax[4] = {0u, 0u, 0u, 0u}; + Bool SignedNormalized = false; + + Bool Active() const { return ChannelMax[0] != 0u; } + }; + static Bool ReadPixelsViaFormatConversion(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels, Bool honorPackImageParams, - Bool applyFixedPointReadClamp, Bool forceOpaqueAlpha) { + Bool applyFixedPointReadClamp, Bool forceOpaqueAlpha, + const NormalizedImageCarrierRead& normalizedCarrier = {}) { ReadbackChannelMapping mapping{}; if (!GetReadbackChannelMapping(format, mapping)) { return false; @@ -7712,7 +7728,17 @@ namespace MobileGL::MG_Backend::DirectGLES { const GLenum attachmentComponentType = QueryReadAttachmentComponentType(); const Bool integerAttachment = attachmentComponentType == GL_INT || attachmentComponentType == GL_UNSIGNED_INT; - if (mapping.isInteger != integerAttachment) { + // A normalized image carrier is EXACTLY the case where the two disagree on purpose, and + // it is the caller - which knows the TEXTURE being read, not just the attachment - that + // says so. An integer client format through such a carrier is not a shape GL can ask for + // (the frontend format is normalized), so it is refused here rather than converted. + if (normalizedCarrier.Active() && (mapping.isInteger || !integerAttachment)) { + MGLOG_E_ONCE("Readback conversion: a normalized image carrier was read as %s, which is not a " + "normalized client format; skipping", + MG_Util::ConvertGLEnumToString(format).c_str()); + return true; + } + if (!normalizedCarrier.Active() && mapping.isInteger != integerAttachment) { MGLOG_E_ONCE("Readback conversion: integer-ness of format %s does not match the read buffer, skipping", MG_Util::ConvertGLEnumToString(format).c_str()); return true; @@ -7732,7 +7758,12 @@ namespace MobileGL::MG_Backend::DirectGLES { }; WideReadCandidate candidates[4]; Int candidateCount = 0; - if (mapping.isInteger) { + if (normalizedCarrier.Active()) { + // The storage IS an integer texture, whatever the application's format says, so the + // only read that can answer is the integer one. The codes it hands back are turned + // into the floats the client asked for below. + candidates[candidateCount++] = {GL_RGBA_INTEGER, GL_UNSIGNED_INT}; + } else if (mapping.isInteger) { if (GetWideReadChannelCount(static_cast(implFormat)) > 0 && IsIntegerReadFormat(implFormat) && (implType == GL_INT || implType == GL_UNSIGNED_INT)) { candidates[candidateCount++] = {static_cast(implFormat), static_cast(implType)}; @@ -7794,6 +7825,32 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } + if (normalizedCarrier.Active()) { + // GL 4.6 2.3.5, the same conversion the shader-side unpack does and with the same + // denominators, so a texel an imageStore wrote and a texel the upload seeded read back + // identically: f = c / (2^b - 1) unsigned, f = max(c / (2^(b-1) - 1), -1) signed, with + // the signed code recovered from the low sixteen bits of the unsigned channel. + const SizeT pixelCount = static_cast(width) * static_cast(height); + Vector floatWide(pixelCount * 4 * sizeof(Float)); + auto* dst = reinterpret_cast(floatWide.data()); + const auto* src = reinterpret_cast(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(normalizedCarrier.ChannelMax[channel]); + if (normalizedCarrier.SignedNormalized) { + const auto signedCode = static_cast(static_cast(code)); + dst[i * 4 + channel] = + std::max(static_cast(signedCode) / denominator, -1.0f); + } else { + dst[i * 4 + channel] = static_cast(code) / denominator; + } + } + } + wide = Move(floatWide); + wideType = GL_FLOAT; + readChannels = 4; + } if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) { // Unpack the packed words into a float wide buffer (full 10-bit precision on e.g. // GL_RGB10_A2 attachments, whose implementation read pair is RGBA/2_10_10_10_REV). @@ -8441,6 +8498,21 @@ namespace MobileGL::MG_Backend::DirectGLES { // GL_READ_FRAMEBUFFER, so the widening question has to be asked of the texture. const Bool forceOpaqueAlpha = TextureImpl::BackendTextureFormatAddsAlpha(textureObject->GetFormat(), textureObject->GetTarget()); + // An image-bindable texture in one of the seven normalized formats has its ES storage + // in a GL_RGBA16UI, holding the format's own channel CODES. glGetTexImage still owes + // the application the NORMALIZED value, so the conversion has to be undone here - and + // it can only be asked of the TEXTURE, which is why it is not derived from the + // attachment the scratch framebuffer happens to hold. + NormalizedImageCarrierRead normalizedCarrier; + if (const auto imageWidening = + TextureImpl::GetImageBindableStorageWidening(textureObject->GetFormat()); + imageWidening && imageWidening.CarriesNormalizedCodes() && + (*backendTextureSlot)->RequiresImageBindableStorage()) { + for (SizeT channel = 0; channel < 4; ++channel) { + normalizedCarrier.ChannelMax[channel] = imageWidening.ChannelMax[channel]; + } + normalizedCarrier.SignedNormalized = imageWidening.SignedNormalized; + } // GL_PACK_IMAGE_HEIGHT/GL_PACK_SKIP_IMAGES only apply to 3D/array image // readbacks (cube-map arrays address as arrays); 2D targets must ignore // them (GL 3.3 section 6.1.4). A 1D ARRAY is one of those 2D targets: GL hands it back @@ -8550,7 +8622,8 @@ namespace MobileGL::MG_Backend::DirectGLES { void* sliceDst = static_cast(pixels) + sliceOffset; if (!ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, sliceDst, /*honorPackImageParams=*/false, - /*applyFixedPointReadClamp=*/false, forceOpaqueAlpha)) { + /*applyFixedPointReadClamp=*/false, forceOpaqueAlpha, + normalizedCarrier)) { allSlicesRead = false; break; } @@ -8573,7 +8646,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (tempFBOComplete && ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, pixels, applyPackImageParams, /*applyFixedPointReadClamp=*/false, - forceOpaqueAlpha)) { + forceOpaqueAlpha, normalizedCarrier)) { MGLOG_D("GetTexImage: finished via client-format conversion"); return; } diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index b34e3cf3..e2456b9b 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2408,12 +2408,34 @@ namespace MobileGL::MG_Backend::DirectGLES { return m_backendTextureId; } - void BackendTextureObject::RequireImageBindableStorage() { + void BackendTextureObject::RequireImageBindableStorage( + const SharedPtr& 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& widenedData, Bool integerData) { + Vector& 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(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(alphaOneCodeOverride); + Memcpy(oneBits, &one, sizeof(one)); + } + } const SizeT srcTexelBytes = componentSize * componentCount; // Sized from the level, never from the source: the driver reads a full @@ -2982,7 +3024,8 @@ namespace MobileGL::MG_Backend::DirectGLES { return data; } return PrepareChannelWidenedUpload(widening.SourceChannels, texelSize, data, byteSize, widening.Type, - widenedData, widening.IntegerData); + widenedData, widening.IntegerData, + widening.CarriesNormalizedCodes() ? widening.ChannelMax[3] : 0u); } // Overwrites the (internal format, format, type) triple GenerateTextureFormatInfo chose @@ -5447,6 +5490,17 @@ namespace MobileGL::MG_Backend::DirectGLES { // 10/10/10/2 unsigned INTEGER channels in an rgba16ui: same component type, same // channel count, every value representable. Only the transfer is re-encoded. case glslang::ElfRgb10a2ui: return 0x906F; // GL_RGB10_A2UI + // The seven NORMALIZED formats, carried in an rgba16ui as their own channel CODES. + // These are the entries whose carrier changes the shader-visible type as well as + // the qualifier (image2D becomes uimage2D), so every access through them is + // wrapped in the GL 4.6 2.3.5 conversion - see WidenImageFormatsPass.h. + case glslang::ElfRgba16: return 0x805B; // GL_RGBA16 + case glslang::ElfRg16: return 0x822C; // GL_RG16 + case glslang::ElfR16: return 0x822A; // GL_R16 + case glslang::ElfRgb10A2: return 0x8059; // GL_RGB10_A2 + case glslang::ElfRgba16Snorm: return 0x8F9B; // GL_RGBA16_SNORM + case glslang::ElfRg16Snorm: return 0x8F99; // GL_RG16_SNORM + case glslang::ElfR16Snorm: return 0x8F98; // GL_R16_SNORM default: return 0; } diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 4cfcf886..6921c66d 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -745,9 +745,13 @@ namespace MobileGL::MG_Backend::DirectGLES { // Returns `data` untouched when no widening applies. Pure CPU and context-free so a unit // test can exercise the exact packing the driver is handed; `widenedData` is the caller's // scratch buffer and has to outlive the returned pointer. + // `alphaOneCodeOverride`, when non-zero, replaces the value written into the synthetic + // alpha channel: an image carrier that holds a NORMALIZED format's channel CODES has to + // pad alpha with that channel's saturated CODE (65535, 32767, 3), which neither of the + // transfer type's own "ones" is. const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data, SizeT byteSize, GLenum uploadType, Vector& widenedData, - Bool integerData = false); + Bool integerData = false, Uint32 alphaOneCodeOverride = 0u); // Splits a GL_UNSIGNED_INT_2_10_10_10_REV shadow (rgb10_a2, rgb10_a2ui) into the four // GL_UNSIGNED_SHORT channel CODES its GL_RGBA16UI image carrier is uploaded as: red in @@ -790,7 +794,17 @@ namespace MobileGL::MG_Backend::DirectGLES { void SyncMipmapsToBackend(const SharedPtr& stateTextureObject); void SyncBuiltinSamplerToBackend(const SharedPtr& stateTextureObject); void SyncTextureParamsToBackend(const SharedPtr& 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& stateTextureObject); + // Whether this texture's ES storage was minted in an image carrier rather than in the + // frontend format's own layout - the readback has to ask, because for a NORMALIZED + // carrier the storage is an integer texture holding codes and glGetTexImage still owes + // the application floats. + Bool RequiresImageBindableStorage() const { return m_imageBindableStorageRequired; } void Bind(GLenum target, Uint unit = TempTextureUnit); Uint GetBackendTextureId() const; @@ -1176,23 +1190,51 @@ namespace MobileGL::MG_Backend::DirectGLES { // Image uniforms take their unit from the layout(binding=N) qualifier baked into // the transpiled ESSL; unlike samplers they must not (and in ES cannot) be // assigned through glUniform1i. + // + // ALL THIRTY-THREE of them, in the one contiguous block ARB_shader_image_load_store allocated + // (GL_IMAGE_1D 0x904C through GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY 0x906C). The list + // used to hold only the fifteen whose TARGET exists in ES, which read as a reasonable + // shortcut and was two bugs: an image uniform this says "no" to is one + // CollectImageFormatBakeInputs never walks, so its non-core format is neither baked nor + // widened and SPIRV-Cross throws for the whole stage ("Attempting to use image format not + // supported in ES profile"), and it is also one SyncToBackend then treats as a SAMPLER and + // assigns with glUniform1i, which ES makes an INVALID_OPERATION. A GL_TEXTURE_CUBE_MAP_ARRAY + // image - which ES 3.2 has in core, so it is not even an emulated target - hit both. inline Bool IsImageUniformType(GLenum type) { switch (type) { + case 0x904C: /*GL_IMAGE_1D*/ case 0x904D: /*GL_IMAGE_2D*/ case 0x904E: /*GL_IMAGE_3D*/ + case 0x904F: /*GL_IMAGE_2D_RECT*/ case 0x9050: /*GL_IMAGE_CUBE*/ case 0x9051: /*GL_IMAGE_BUFFER*/ + case 0x9052: /*GL_IMAGE_1D_ARRAY*/ case 0x9053: /*GL_IMAGE_2D_ARRAY*/ + case 0x9054: /*GL_IMAGE_CUBE_MAP_ARRAY*/ + case 0x9055: /*GL_IMAGE_2D_MULTISAMPLE*/ + case 0x9056: /*GL_IMAGE_2D_MULTISAMPLE_ARRAY*/ + case 0x9057: /*GL_INT_IMAGE_1D*/ case 0x9058: /*GL_INT_IMAGE_2D*/ case 0x9059: /*GL_INT_IMAGE_3D*/ + case 0x905A: /*GL_INT_IMAGE_2D_RECT*/ case 0x905B: /*GL_INT_IMAGE_CUBE*/ case 0x905C: /*GL_INT_IMAGE_BUFFER*/ + case 0x905D: /*GL_INT_IMAGE_1D_ARRAY*/ case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/ + case 0x905F: /*GL_INT_IMAGE_CUBE_MAP_ARRAY*/ + case 0x9060: /*GL_INT_IMAGE_2D_MULTISAMPLE*/ + case 0x9061: /*GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY*/ + case 0x9062: /*GL_UNSIGNED_INT_IMAGE_1D*/ case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/ case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/ + case 0x9065: /*GL_UNSIGNED_INT_IMAGE_2D_RECT*/ case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/ case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/ + case 0x9068: /*GL_UNSIGNED_INT_IMAGE_1D_ARRAY*/ case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/ + case 0x906A: /*GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY*/ + case 0x906B: /*GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE*/ + case 0x906C: /*GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY*/ return true; default: return false; diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index 4f8ecdcb..c0f0a84e 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -292,13 +292,28 @@ namespace MobileGL::MG_Backend::DirectGLES { widening.SourceEncoding = ImageWidenSourceEncoding::PackedFloat11f11f10f; break; case TextureInternalFormat::RGB10A2UI: + case TextureInternalFormat::RGB10A2: // GL_UNSIGNED_INT_2_10_10_10_REV -> the GL_RGBA_INTEGER / GL_UNSIGNED_SHORT the - // GL_RGBA16UI carrier already asked for above; only the split is new. + // GL_RGBA16UI carrier already asked for above; only the split is new. The two + // formats share it: rgb10_a2's channel codes are the same fields rgb10_a2ui's are, + // and what the shader divides them by is not the transfer's business. widening.SourceEncoding = ImageWidenSourceEncoding::PackedInt2101010Rev; break; default: break; } + // The seven normalized formats whose carrier holds CODES rather than values. Both + // halves of the transfer need to know: a missing alpha is padded with the saturated + // code rather than the integer 1, and glGetTexImage has to divide the codes back out. + bool signedNormalized = false; + Uint32 channelMax[4] = {0u, 0u, 0u, 0u}; + if (MG_Util::ShaderTranspiler::ShaderCompiler::NormalizedImageCarrierCodes(requested, channelMax, + signedNormalized)) { + for (SizeT channel = 0; channel < 4; ++channel) { + widening.ChannelMax[channel] = channelMax[channel]; + } + widening.SignedNormalized = signedNormalized; + } return widening; } } // namespace TextureImpl diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index 8ba33ff5..8a214048 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -132,7 +132,21 @@ namespace MobileGL::MG_Backend::DirectGLES { // What the upload has to do to the frontend shadow before it describes the level to // the driver (PrepareImageWidenedUpload). ImageWidenSourceEncoding SourceEncoding = ImageWidenSourceEncoding::Components; + // Non-zero when the carrier holds this format's channels as the INTEGER CODES of a + // NORMALIZED value - the seven 16-bit and 10-bit normalized formats, which core ESSL + // has no image format of any width for and which a float carrier would requantise. + // Each entry is the largest code that channel can hold, i.e. the denominator of GL 4.6 + // 2.3.5; SignedNormalized picks which of the two conversions it is the denominator of. + // + // Two things depend on it, both because the ES storage no longer shares the frontend + // format's component class: the upload pads a missing alpha with ChannelMax[3] instead + // of the transfer type's own "one" (through a uint carrier the saturated field IS the + // one), and glGetTexImage divides the codes back out into the floats the application + // is still owed. + Uint ChannelMax[4] = {0u, 0u, 0u, 0u}; + Bool SignedNormalized = false; + Bool CarriesNormalizedCodes() const { return ChannelMax[0] != 0u; } explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; } }; ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat); diff --git a/MobileGL/MG_IntegrationTest/Scenarios/NonCoreImageFormatScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/NonCoreImageFormatScenario.cpp index 550fb6fb..ddd8f5fa 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/NonCoreImageFormatScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/NonCoreImageFormatScenario.cpp @@ -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 +#include #include #include #include @@ -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& 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 ReadFloatsFrom(GLenum target, GLuint texture, GLenum format, + int componentsPerTexel, int texelCount) { + std::vector texels(static_cast(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 ReadUints(GLuint texture, GLenum format, int componentsPerTexel) { std::vector texels(static_cast(kExtent) * kExtent * componentsPerTexel, 0xFFFFFFFFu); @@ -506,6 +564,430 @@ void main() } } + // GL_RG16, the first of the seven NORMALIZED formats and the first carrier that changes the + // shader-visible TYPE: core ESSL has no 16-bit normalized image format of any width, and + // no float carrier is honest either (a half has eleven mantissa bits against sixteen), so + // the rgba16ui behind it holds the format's own CODES and every access converts. + // + // Both directions of GL 4.6 2.3.5 are checked, and the STORE direction is checked as exact + // INTEGER CODES rather than as floats within a tolerance - which is the point of a code + // carrier over a float one, and the only thing that would catch a rounding rule that was + // merely close. The values are chosen so that the products are exact in float32: 0.25 and + // 0.75 land off a tie, 0.5 lands exactly ON one (0.5 * 65535 = 32767.5), and the two + // out-of-range values must be clamped before they are rounded rather than after. + TEST_F(NonCoreImageFormatScenario, UnsignedNormalizedImageCarriesItsCodesBothWays) { + if (!Ready()) GTEST_SKIP() << "no GL context"; + if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver"; + + constexpr int kTexels = kExtent * kExtent; + constexpr double kUnorm16Max = 65535.0; + + // THE UPLOAD. Distinct per texel, and the codes are the shadow's own 16-bit words: a + // widening that padded or sheared them still produces plausible normalized floats. + std::vector seed(static_cast(kTexels) * 2u, 0); + for (int texel = 0; texel < kTexels; ++texel) { + seed[texel * 2 + 0] = static_cast(texel * 4001); + seed[texel * 2 + 1] = static_cast(65535 - texel * 3001); + } + const std::vector wideSeed(static_cast(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 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 values(static_cast(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 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 seed(static_cast(kTexels) * 4u, 0); + for (int texel = 0; texel < kTexels; ++texel) { + seed[texel * 4 + 0] = seedCodes[texel]; + seed[texel * 4 + 1] = static_cast(-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 wideSeed(static_cast(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 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(code) / static_cast(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 values(static_cast(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 seed(static_cast(kTexels), 0u); + std::vector seedCodes(static_cast(kTexels) * 4u, 0u); + for (int texel = 0; texel < kTexels; ++texel) { + const GLuint r = static_cast(texel) * 67u; + const GLuint g = 1023u - static_cast(texel) * 13u; + const GLuint b = 341u + static_cast(texel); + const GLuint a = static_cast(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 wideSeed(static_cast(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 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(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 seed(static_cast(kTexels), 0); + for (int texel = 0; texel < kTexels; ++texel) { + seed[texel] = static_cast((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 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 wideSeed(static_cast(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 loaded = ReadFloats(wide, GL_RGBA, 4); + for (int texel = 0; texel < kTexelsPerFace; ++texel) { + const int sourceTexel = layerFace * kTexelsPerFace + texel; + EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * kUnorm16Max), seed[sourceTexel]) + << "layer-face " << layerFace << " texel " << texel; + EXPECT_FLOAT_EQ(loaded[texel * 4 + 1], 0.0f) + << "layer-face " << layerFace << " texel " << texel + << ": imageLoad on a one-channel format must report 0 for green"; + EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f) + << "layer-face " << layerFace << " texel " << texel + << ": imageLoad on a format without alpha must report 1"; + } + } + } + // The other consumer of the same texture. A widened texture's ES storage really does have // four channels, so a sampler reading it raw would see whatever the carrier holds; the // logical format's missing channels have to keep reading 0 and 1 (which Espryt arranges diff --git a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp index 33b137dc..33dbc414 100644 --- a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp @@ -4000,6 +4000,7 @@ namespace { constexpr Uint kGlR32f = 0x822E; constexpr Uint kGlRgb10A2ui = 0x906F; constexpr Uint kGlRgb10A2 = 0x8059; + constexpr Uint kGlRgb8 = 0x8051; // not one of the forty image formats at all } // namespace // The KHR-GL4x.packed_depth_stencil.stencil_texturing compute shader, reduced: one format-less @@ -4050,20 +4051,26 @@ void main() { imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(15u, // pass on the ESSL chain and re-declares them in a core carrier SPIRV-Cross does print, so for // those the module is the right place and the text completion would put back the narrow token no // ES driver accepts. r8ui - which the stencil half of the packed_depth_stencil case binds - is -// one of the rescued ones; rgb10_a2, whose 10/10/10/2 NORMALIZED channels no core format has, is -// not. +// one of the rescued ones, and so, now that the carriers cover all twenty-six non-core formats, +// is every other IMAGE format. What is left for the guard is a format that is not an image format +// at all: it has no carrier and no ESSL image spelling either, so baking it would put a token in +// the module that means nothing. TEST_F(ProgramUtilTest, BakeImageFormatsLeavesOnlyTheFormatsNoCoreCarrierRescues) { using namespace MG_Util::ShaderTranspiler; ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR8ui)) << "if SPIRV-Cross ever learns to print r8ui for ES, this route can go"; ASSERT_NE(ShaderCompiler::WidenedCoreEsslImageFormat(kGlR8ui), 0u); - // rgb10_a2ui is unprintable too and IS rescued: its channels are unsigned INTEGER, so an - // rgba16ui holds all four of them outright. + // Unprintable and rescued anyway: rgb10_a2ui's channels are unsigned INTEGER, so an rgba16ui + // holds all four outright, and rgb10_a2's are the same channels read as NORMALIZED, which the + // same carrier holds as their codes. ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlRgb10A2ui)); ASSERT_NE(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2ui), 0u); ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlRgb10A2)); - ASSERT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2), 0u); + ASSERT_NE(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2), 0u); + // ...and the one the guard still turns away. + ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlRgb8)); + ASSERT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb8), 0u); ASSERT_TRUE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR32ui)); EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(kGlR8ui), "r8ui"); EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(0x8051 /*GL_RGB8*/), ""); @@ -4078,7 +4085,7 @@ void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); } { // Unprintable AND uncarriable: declined, module untouched, and the stage still transpiles. Vector baked; - ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlRgb10A2}}, baked)); + ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlRgb8}}, baked)); EXPECT_EQ(baked, spirv) << "a format nothing can carry must leave the module untouched"; EXPECT_FALSE(DecompileToEssl(baked).empty()); } diff --git a/MobileGL/MG_Test/ShaderTranspiler/WidenImageFormatsTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/WidenImageFormatsTest.cpp index a6b98723..3a604acf 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/WidenImageFormatsTest.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/WidenImageFormatsTest.cpp @@ -89,6 +89,7 @@ namespace { struct StorageImageType { Uint32 resultId = 0u; Uint32 format = 0u; + Uint32 sampledTypeId = 0u; }; Vector CollectStorageImageTypes(const Vector& 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& spirv, Uint32 typeId) { + String spelling; + ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) { + if (words[1] != typeId) return; + // OpTypeFloat words: 1 result id, 2 width. OpTypeInt adds 3 signedness. + if (opcode == spv::Op::OpTypeFloat && wordCount >= 3u) { + spelling = "float"; + } else if (opcode == spv::Op::OpTypeInt && wordCount >= 4u) { + spelling = words[3] != 0u ? "int" : "uint"; + } + }); + return spelling; + } + // OpVectorShuffle words: 0 opcode/count, 1 result type, 2 result id, 3 vector 1, 4 vector 2, // 5.. the component selectors. struct VectorShuffle { @@ -269,15 +286,40 @@ void main() { } )"; - // rg16 is one of the SEVEN with no core carrier at all - core ESSL has no 16-bit normalized - // format, so every candidate loses range or changes the component type the texture presents. - // It must be left alone and keep the honest "no GLSL ES spelling" diagnostic instead. + // rg16: TWO unsigned-normalized 16-bit channels, which core ESSL has no image format of any + // width for. Carried as its own CODES in an rgba16ui, so the declaration comes out a + // uimage2D and every access is wrapped in GL 4.6 2.3.5 as well as masked. const char* const kRg16LoadStore = R"(#version 430 core layout(rg16, binding = 0) uniform image2D img; out vec4 fragColor; void main() { vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy)); - imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, 2.0, 3.0, 4.0)); + imageStore(img, ivec2(gl_FragCoord.xy), vec4(0.25, 0.5, 0.75, 1.0)); + fragColor = texel; +} +)"; + + // rgba16_snorm: the signed twin, whose code is a two's-complement 16-bit integer sitting in an + // UNSIGNED carrier channel - so the load has to sign-extend it back and the store has to mask + // it down, neither of which the unsigned conversion does. + const char* const kRgba16SnormLoadStore = R"(#version 430 core +layout(rgba16_snorm, binding = 0) uniform image2D img; +out vec4 fragColor; +void main() { + vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy)); + imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, -1.0, 0.5, -0.5)); + fragColor = texel; +} +)"; + + // rgb10_a2: FOUR normalized channels that are not all the same width, so its denominator is + // (1023, 1023, 1023, 3) and one number would be wrong for a quarter of every texel. + const char* const kRgb10A2LoadStore = R"(#version 430 core +layout(rgb10_a2, binding = 0) uniform image2D img; +out vec4 fragColor; +void main() { + vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy)); + imageStore(img, ivec2(gl_FragCoord.xy), vec4(0.25, 0.5, 0.75, 1.0)); fragColor = texel; } )"; @@ -287,7 +329,7 @@ void main() { // the shader rewrite, the ES texture storage and the glBindImageTexture argument. If it drifts // the three stop agreeing, and a narrow texture read through a wide image goes out of bounds // silently on every driver tested. -TEST(WidenImageFormats, NineteenNonCoreFormatsHaveALosslessCoreCarrier) { +TEST(WidenImageFormats, TwentySixNonCoreFormatsHaveALosslessCoreCarrier) { struct Case { Uint requested; Uint carrier; @@ -319,6 +361,16 @@ TEST(WidenImageFormats, NineteenNonCoreFormatsHaveALosslessCoreCarrier) { // FOUR channels: 10, 10, 10 and 2 bits of unsigned integer all fit in sixteen, so nothing // is masked at all and only the packed TRANSFER is re-encoded. {0x906F, 0x8D76, 4, "GL_RGB10_A2UI -> GL_RGBA16UI"}, + // The seven NORMALIZED formats, carried as their own channel CODES in the same rgba16ui. + // These are the entries whose carrier changes the shader-visible TYPE as well, which is + // why every access through them is wrapped in GL 4.6 2.3.5 rather than only masked. + {0x805B, 0x8D76, 4, "GL_RGBA16 -> GL_RGBA16UI"}, + {0x822C, 0x8D76, 2, "GL_RG16 -> GL_RGBA16UI"}, + {0x822A, 0x8D76, 1, "GL_R16 -> GL_RGBA16UI"}, + {0x8059, 0x8D76, 4, "GL_RGB10_A2 -> GL_RGBA16UI"}, + {0x8F9B, 0x8D76, 4, "GL_RGBA16_SNORM -> GL_RGBA16UI"}, + {0x8F99, 0x8D76, 2, "GL_RG16_SNORM -> GL_RGBA16UI"}, + {0x8F98, 0x8D76, 1, "GL_R16_SNORM -> GL_RGBA16UI"}, }; for (const Case& testCase : cases) { EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(testCase.requested), testCase.carrier) @@ -334,7 +386,7 @@ TEST(WidenImageFormats, NineteenNonCoreFormatsHaveALosslessCoreCarrier) { } } -TEST(WidenImageFormats, CoreFormatsAndTheSevenWithoutALosslessCarrierAreRefused) { +TEST(WidenImageFormats, CoreFormatsAreRefused) { // The thirteen GLSL ES already has: nothing to carry. for (const Uint coreFormat : {0x8814u /*RGBA32F*/, 0x881Au /*RGBA16F*/, 0x822Eu /*R32F*/, 0x8058u /*RGBA8*/, 0x8F97u /*RGBA8_SNORM*/, 0x8D82u /*RGBA32I*/, @@ -344,23 +396,63 @@ TEST(WidenImageFormats, CoreFormatsAndTheSevenWithoutALosslessCarrierAreRefused) EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(coreFormat), 0u) << "core format 0x" << std::hex << coreFormat; } - // The seven with no LOSSLESS core carrier: core ESSL has no 16-bit normalized format and no - // 10-bit one, so every candidate for these either loses range or changes the component type - // the texture presents to anything that samples it. Deliberately left to the honest - // diagnostic. r11f_g11f_b10f is NOT among them - rgba16f holds every value it can - and - // neither is rgb10_a2ui, whose channels are INTEGER and fit an rgba16ui outright. - for (const Uint hardFormat : {0x8059u /*RGB10_A2*/, 0x805Bu /*RGBA16*/, 0x822Cu /*RG16*/, - 0x822Au /*R16*/, 0x8F9Bu /*RGBA16_SNORM*/, 0x8F99u /*RG16_SNORM*/, - 0x8F98u /*R16_SNORM*/}) { - EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(hardFormat), 0u) - << "format without an exact carrier 0x" << std::hex << hardFormat; - } // Not an image format at all. EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(0x8051 /*GL_RGB8*/), 0u); EXPECT_EQ(ShaderCompiler::ImageFormatChannelCount(0x8051 /*GL_RGB8*/), 0u); EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(0), 0u); } +// The denominators of GL 4.6 2.3.5, which is the whole difference between a carrier that holds a +// format's VALUES and one that holds its CODES. Both halves of DirectGLES's transfer read them +// (the upload's synthetic alpha and glGetTexImage's divide), and so does the shader rewrite, so a +// wrong entry here is wrong in three places at once and consistently - which is exactly the kind +// of error a round-trip test cannot see. +TEST(WidenImageFormats, OnlyTheNormalizedFormatsCarryCodesAndTheirDenominatorsAreTheFormatsOwn) { + struct Case { + Uint format; + Uint32 channelMax[4]; + bool isSigned; + const char* name; + }; + const Case cases[] = { + {0x805B, {65535u, 65535u, 65535u, 65535u}, false, "GL_RGBA16"}, + {0x822C, {65535u, 65535u, 65535u, 65535u}, false, "GL_RG16"}, + {0x822A, {65535u, 65535u, 65535u, 65535u}, false, "GL_R16"}, + // The one format whose channels are not all the same width, and the reason the answer is + // four numbers rather than one: a two-bit alpha saturates at 3, not at 1023. + {0x8059, {1023u, 1023u, 1023u, 3u}, false, "GL_RGB10_A2"}, + {0x8F9B, {32767u, 32767u, 32767u, 32767u}, true, "GL_RGBA16_SNORM"}, + {0x8F99, {32767u, 32767u, 32767u, 32767u}, true, "GL_RG16_SNORM"}, + {0x8F98, {32767u, 32767u, 32767u, 32767u}, true, "GL_R16_SNORM"}, + }; + for (const Case& testCase : cases) { + Uint32 channelMax[4] = {0u, 0u, 0u, 0u}; + bool isSigned = !testCase.isSigned; + EXPECT_TRUE(ShaderCompiler::NormalizedImageCarrierCodes(testCase.format, channelMax, isSigned)) + << testCase.name; + for (Uint channel = 0; channel < 4; ++channel) { + EXPECT_EQ(channelMax[channel], testCase.channelMax[channel]) + << testCase.name << " channel " << channel; + } + EXPECT_EQ(isSigned, testCase.isSigned) << testCase.name; + } + + // Everything else keeps its own component type in the carrier, so nothing is converted: an + // rg8's carrier channel really is an 8-bit unsigned normalized one, and an rgb10_a2ui's + // channel really does hold the integer the shader stored. + for (const Uint direct : {0x8230u /*RG32F*/, 0x8229u /*R8*/, 0x8F94u /*R8_SNORM*/, + 0x8232u /*R8UI*/, 0x8C3Au /*R11F_G11F_B10F*/, 0x906Fu /*RGB10_A2UI*/, + 0x8814u /*RGBA32F*/, 0x8051u /*RGB8, not an image format*/}) { + Uint32 channelMax[4] = {7u, 7u, 7u, 7u}; + bool isSigned = true; + EXPECT_FALSE(ShaderCompiler::NormalizedImageCarrierCodes(direct, channelMax, isSigned)) + << "format 0x" << std::hex << direct; + for (Uint channel = 0; channel < 4; ++channel) { + EXPECT_EQ(channelMax[channel], 7u) << "a refused format must leave the output alone"; + } + } +} + TEST(WidenImageFormats, TwoChannelFloatImageBecomesRgba32fWithBothAccessesMasked) { const Vector spirv = CompileFragment(kRg32fLoadStore); ASSERT_FALSE(spirv.empty()); @@ -679,25 +771,132 @@ TEST(WidenImageFormats, CoreFormatModuleIsHandedBackUntouched) { } } -TEST(WidenImageFormats, FormatWithoutAnExactCarrierIsLeftAlone) { +// The normalized carrier, which is the one that does not merely re-DECLARE the image: a 16-bit +// normalized channel has no core ESSL format of any width behind it, and no FLOAT carrier is +// honest either (a half has eleven mantissa bits against its sixteen), so what the rgba16ui holds +// is the format's own CODE. That changes the shader-visible TYPE, which is the thing to check - +// an image2D whose format moved to rgba16ui but whose sampled type stayed float is not merely +// wrong, it is invalid SPIR-V, and a module that kept the float type while the STORAGE became an +// integer texture would read whole texels as garbage. +TEST(WidenImageFormats, NormalizedImageBecomesAUimageWhoseAccessesConvertItsCodes) { const Vector 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(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 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(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(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 spirv = CompileFragment(kRgba16SnormLoadStore); + ASSERT_FALSE(spirv.empty()); + ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv)); + + Vector 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(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 spirv = CompileFragment(kRgb10A2LoadStore); + ASSERT_FALSE(spirv.empty()); + ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv)); + + Vector 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(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; } diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index c8355f24..142bfd69 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -1024,6 +1024,12 @@ namespace MobileGL { return WidenImageFormatsPass::ImageFormatChannelCount(glInternalFormat); } + bool ShaderCompiler::NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4], + bool& outSignedNormalized) { + return WidenImageFormatsPass::NormalizedImageCarrierCodes(glInternalFormat, outChannelMax, + outSignedNormalized); + } + bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector& inputBinary, const std::set& blockNames, std::set& flattenedBlockNames, diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 2b0d7506..a95125d1 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -298,6 +298,13 @@ namespace MobileGL { // Channels a GL image internal format really has (1-4), 0 when it is not one of // the forty image formats. static Uint ImageFormatChannelCount(Uint glInternalFormat); + // Whether the carrier holds the format's channels as the INTEGER CODES of a + // normalized value, and the largest code each channel can hold. See + // WidenImageFormatsPass::NormalizedImageCarrierCodes - DirectGLES needs it for + // both halves of the transfer, which no longer share the frontend format's + // component class with the ES storage. + static bool NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4], + bool& outSignedNormalized); static bool RebaseInstanceIndexForVulkan(const Vector& inputBinary, Vector& outputBinary, bool enableSpirvValidation = false); diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp index 4105c9d5..c8770d70 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp @@ -22,9 +22,12 @@ #include "source/opt/type_manager.h" #include "source/opt/types.h" #include "source/util/make_unique.h" +#include "source/util/string_utils.h" +#include #include #include +#include #include namespace MobileGL { @@ -88,18 +91,46 @@ namespace MobileGL { // program holding eight images). // // The remaining seven - rgb10_a2, rgba16, rg16, r16, rgba16_snorm, rg16_snorm and - // r16_snorm - stay absent, and for a stronger reason than quantisation: core ESSL - // has no 16-bit normalized format at all and no 10-bit one, so every candidate - // carrier for them either loses range or changes the component TYPE the texture - // presents. They keep the honest "no GLSL ES spelling" diagnostic rather than a - // silent approximation. + // r16_snorm - are NORMALIZED, and core ESSL has no 16-bit normalized format at all + // and no 10-bit one. There is no carrier that keeps their component type, and no + // FLOAT carrier that is honest either: a half has eleven mantissa bits against a + // 16-bit normalized channel's sixteen, so rgba16f would quantise. What DOES hold + // every one of their values exactly is the format's own CODE: a normalized channel + // of b bits is an integer in [0, 2^b-1] (unsigned) or [-(2^(b-1)-1), 2^(b-1)-1] + // (signed), and rgba16ui gives every channel of all seven sixteen bits to hold + // that integer in - bit for bit, with the SAME quantisation grid the real format + // has, which is the one thing a float carrier could not reproduce. + // + // The price is that the carrier changes the SHADER-VISIBLE TYPE: an image2D + // becomes a uimage2D, so every imageLoad has to divide the code back out and every + // imageStore has to round a value onto it (GL 4.6 2.3.5). ChannelMax below is the + // denominator that conversion uses, per channel - the same number for all four of + // a 16-bit format and (1023, 1023, 1023, 3) for rgb10_a2, whose channels are not + // all the same width. + // + // What this carrier gives up, and it is real: the ES texture behind the image is + // now an INTEGER texture, so a `sampler2D` bound to it reads codes rather than the + // normalized value, and it can no longer be filtered. Measured against the + // alternative, which is not a truer sampler but no program at all - the stage that + // declares one of these seven has no legal ESSL, so before this it did not compile + // and nothing sampled anything. struct ImageFormatWidening { spv::ImageFormat Carrier = spv::ImageFormat::Unknown; uint32_t Channels = 0; + // Non-zero when the carrier holds the format's channels as the INTEGER CODES + // of a NORMALIZED value rather than as the values themselves: the largest code + // each channel can hold, i.e. 2^b - 1 for an unsigned normalized channel of b + // bits and 2^(b-1) - 1 for a signed one. + uint32_t ChannelMax[4] = {0u, 0u, 0u, 0u}; + bool SignedNormalized = false; + bool CarriesNormalizedCodes() const { return ChannelMax[0] != 0u; } explicit operator bool() const { return Carrier != spv::ImageFormat::Unknown; } }; + constexpr uint32_t kUnorm16Max = 65535u; + constexpr uint32_t kSnorm16Max = 32767u; + ImageFormatWidening WideningOfSpirvImageFormat(spv::ImageFormat format) { switch (format) { // Float. @@ -130,6 +161,32 @@ namespace MobileGL { // FOUR channels, so there is no surplus channel to mask and no access is // rewritten - 10, 10, 10 and 2 bits of unsigned integer all fit in sixteen. case spv::ImageFormat::Rgb10a2ui: return {spv::ImageFormat::Rgba16ui, 4}; + // Unsigned normalized, carried as codes in [0, 2^b - 1]. + case spv::ImageFormat::Rgba16: + return {spv::ImageFormat::Rgba16ui, 4, + {kUnorm16Max, kUnorm16Max, kUnorm16Max, kUnorm16Max}, false}; + case spv::ImageFormat::Rg16: + return {spv::ImageFormat::Rgba16ui, 2, + {kUnorm16Max, kUnorm16Max, kUnorm16Max, kUnorm16Max}, false}; + case spv::ImageFormat::R16: + return {spv::ImageFormat::Rgba16ui, 1, + {kUnorm16Max, kUnorm16Max, kUnorm16Max, kUnorm16Max}, false}; + // The one entry whose channels are not all the same width, which is the whole + // reason ChannelMax is per channel rather than one number. + case spv::ImageFormat::Rgb10A2: + return {spv::ImageFormat::Rgba16ui, 4, {1023u, 1023u, 1023u, 3u}, false}; + // Signed normalized, carried as the two's-complement code in [-(2^(b-1) - 1), + // 2^(b-1) - 1]. The carrier's channel is UNSIGNED, so the code's sixteen bits + // are stored verbatim and sign-extended again on the way out. + case spv::ImageFormat::Rgba16Snorm: + return {spv::ImageFormat::Rgba16ui, 4, + {kSnorm16Max, kSnorm16Max, kSnorm16Max, kSnorm16Max}, true}; + case spv::ImageFormat::Rg16Snorm: + return {spv::ImageFormat::Rgba16ui, 2, + {kSnorm16Max, kSnorm16Max, kSnorm16Max, kSnorm16Max}, true}; + case spv::ImageFormat::R16Snorm: + return {spv::ImageFormat::Rgba16ui, 1, + {kSnorm16Max, kSnorm16Max, kSnorm16Max, kSnorm16Max}, true}; default: return {}; } @@ -287,6 +344,180 @@ namespace MobileGL { } return true; } + + // GLSL.std.450 instruction numbers (see 3rdparty/glslang/SPIRV/GLSL.std.450.h). + constexpr uint32_t kGlslFSign = 6u; + constexpr uint32_t kGlslFMax = 40u; + constexpr uint32_t kGlslFClamp = 43u; + + // The module's GLSL.std.450 import, creating it when the module has none. glslang + // emits one for all but the most trivial shaders, but a module that reached here + // without one still has to be carriable. 0 means no id was available and NOTHING + // was added, so the caller can still hand the module back untouched. + uint32_t EnsureGlslStd450Import(IRContext* context) { + for (const Instruction& import : context->module()->ext_inst_imports()) { + if (spvtools::utils::MakeString(import.GetInOperand(0).words) == "GLSL.std.450") { + return import.result_id(); + } + } + const uint32_t importId = context->TakeNextId(); + if (importId == 0u) return 0u; + context->AddExtInstImport(spvtools::MakeUnique( + context, spv::Op::OpExtInstImport, 0, importId, + Instruction::OperandList{ + {SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector("GLSL.std.450")}})); + return importId; + } + + // Every type and constant the normalized-code rewrite emits, resolved ONCE before + // any instruction is inserted. The type and constant managers append to the + // module's globals and keep their own def-use bookkeeping straight; the rewrite + // below does not (this pass drops every analysis at the end instead), so a manager + // consulted after the first insertion would be reading a def-use map that no + // longer describes the function bodies. + struct NormalizedCarrierMaterial { + uint32_t Glsl450Id = 0; + uint32_t FloatTypeId = 0; // the component types, kept only so the declaration + uint32_t IntTypeId = 0; // order below can put each vector after its own + uint32_t UintTypeId = 0; // component - and the last is the image's new Sampled Type + uint32_t UvecTypeId = 0; // uvec4: what an OpImageRead of the carrier yields + uint32_t IvecTypeId = 0; // ivec4: the sign-extended snorm code + uint32_t FvecTypeId = 0; // vec4: what the shader asked for + uint32_t ShiftWidthId = 0; // ivec4(16), the snorm sign extension + uint32_t LowWordMaskId = 0; // uvec4(0xFFFF) + uint32_t ZeroId = 0; // vec4(0.0) + uint32_t OneId = 0; // vec4(1.0) + uint32_t MinusOneId = 0; // vec4(-1.0) + uint32_t HalfId = 0; // vec4(0.5) + + explicit operator bool() const { return UvecTypeId != 0u; } + }; + + // A four-component constant of `typeId` from four component ids. + uint32_t MakeVec4Constant(IRContext* context, uint32_t typeId, const uint32_t (&componentIds)[4]) { + auto* constantMgr = context->get_constant_mgr(); + analysis::Type* vectorType = context->get_type_mgr()->GetType(typeId); + if (vectorType == nullptr) return 0u; + // A vector constant's "literal words" are the IDS of its components + // (ConstantManager::CreateConstant -> GetConstantsFromIds). + const analysis::Constant* constant = constantMgr->GetConstant( + vectorType, {componentIds[0], componentIds[1], componentIds[2], componentIds[3]}); + if (constant == nullptr) return 0u; + const Instruction* definition = constantMgr->GetDefiningInstruction(constant); + return definition == nullptr ? 0u : definition->result_id(); + } + + uint32_t MakeScalarConstant(IRContext* context, analysis::Type* scalarType, uint32_t word) { + const analysis::Constant* constant = context->get_constant_mgr()->GetConstant(scalarType, {word}); + if (constant == nullptr) return 0u; + const Instruction* definition = + context->get_constant_mgr()->GetDefiningInstruction(constant); + return definition == nullptr ? 0u : definition->result_id(); + } + + uint32_t MakeSplatVec4Constant(IRContext* context, uint32_t vectorTypeId, + analysis::Type* scalarType, uint32_t word) { + const uint32_t scalarId = MakeScalarConstant(context, scalarType, word); + if (scalarId == 0u) return 0u; + const uint32_t componentIds[4] = {scalarId, scalarId, scalarId, scalarId}; + return MakeVec4Constant(context, vectorTypeId, componentIds); + } + + uint32_t FloatBits(float value) { + uint32_t bits = 0; + static_assert(sizeof(bits) == sizeof(value), "float is not 32 bits"); + std::memcpy(&bits, &value, sizeof(bits)); + return bits; + } + + NormalizedCarrierMaterial ResolveNormalizedCarrierMaterial(IRContext* context) { + NormalizedCarrierMaterial material; + auto* typeMgr = context->get_type_mgr(); + + analysis::Integer uintScalar(32, false); + analysis::Integer intScalar(32, true); + analysis::Float floatScalar(32); + analysis::Type* uintReg = typeMgr->GetRegisteredType(&uintScalar); + analysis::Type* intReg = typeMgr->GetRegisteredType(&intScalar); + analysis::Type* floatReg = typeMgr->GetRegisteredType(&floatScalar); + if (uintReg == nullptr || intReg == nullptr || floatReg == nullptr) return {}; + + analysis::Vector uintVector(uintReg, 4); + analysis::Vector intVector(intReg, 4); + analysis::Vector floatVector(floatReg, 4); + const uint32_t uintTypeId = typeMgr->GetTypeInstruction(&uintScalar); + const uint32_t uvecTypeId = typeMgr->GetTypeInstruction(&uintVector); + const uint32_t ivecTypeId = typeMgr->GetTypeInstruction(&intVector); + const uint32_t fvecTypeId = typeMgr->GetTypeInstruction(&floatVector); + if (uintTypeId == 0u || uvecTypeId == 0u || ivecTypeId == 0u || fvecTypeId == 0u) return {}; + + const uint32_t glsl450Id = EnsureGlslStd450Import(context); + if (glsl450Id == 0u) return {}; + + material.Glsl450Id = glsl450Id; + material.FloatTypeId = typeMgr->GetTypeInstruction(&floatScalar); + material.IntTypeId = typeMgr->GetTypeInstruction(&intScalar); + material.UintTypeId = uintTypeId; + material.UvecTypeId = uvecTypeId; + material.IvecTypeId = ivecTypeId; + material.FvecTypeId = fvecTypeId; + material.ShiftWidthId = MakeSplatVec4Constant(context, ivecTypeId, intReg, 16u); + material.LowWordMaskId = MakeSplatVec4Constant(context, uvecTypeId, uintReg, 0xFFFFu); + material.ZeroId = MakeSplatVec4Constant(context, fvecTypeId, floatReg, FloatBits(0.0f)); + material.OneId = MakeSplatVec4Constant(context, fvecTypeId, floatReg, FloatBits(1.0f)); + material.MinusOneId = MakeSplatVec4Constant(context, fvecTypeId, floatReg, FloatBits(-1.0f)); + material.HalfId = MakeSplatVec4Constant(context, fvecTypeId, floatReg, FloatBits(0.5f)); + if (material.ShiftWidthId == 0u || material.LowWordMaskId == 0u || material.ZeroId == 0u || + material.OneId == 0u || material.MinusOneId == 0u || material.HalfId == 0u) { + return {}; + } + return material; + } + + // spirv-tools' type manager APPENDS a new type declaration to the END of the + // module's type section - which is fine for a type only function bodies name, and + // NOT fine for the uint32 an OpTypeImage further up is about to take as its + // Sampled Type. SPIR-V requires an id to be defined before it is used, and + // spirv-tools' own RemoveDuplicates - which the caller runs immediately after this + // pass - walks the section in order and dereferences each image type's sampled + // type as it goes, so an out-of-order declaration is a null dereference inside the + // type manager rather than a diagnostic. + // + // `typeIdsInDependencyOrder` must list a component type before any vector of it: + // each move lands immediately in front of `target`, so the order they are + // processed in is the order they end up in. + void HoistTypeDeclarationsBefore(IRContext* context, Instruction* target, + const std::vector& typeIdsInDependencyOrder) { + std::set definedBeforeTarget; + for (Instruction& declaration : context->module()->types_values()) { + if (&declaration == target) break; + definedBeforeTarget.insert(declaration.result_id()); + } + for (const uint32_t typeId : typeIdsInDependencyOrder) { + if (typeId == 0u || definedBeforeTarget.count(typeId) != 0u) continue; + Instruction* declaration = context->get_def_use_mgr()->GetDef(typeId); + if (declaration == nullptr || declaration == target) continue; + // IntrusiveNodeBase::InsertBefore MOVES the node it is called on - it + // unlinks it from wherever it is first - which is the opposite convention + // to Instruction::InsertBefore(unique_ptr), used everywhere else here. + declaration->InsertBefore(target); + } + } + + // vec4(ChannelMax), the denominator of the format's own normalized conversion. + uint32_t ResolveDenominatorConstant(IRContext* context, const NormalizedCarrierMaterial& material, + const uint32_t (&channelMax)[4]) { + analysis::Float floatScalar(32); + analysis::Type* floatReg = context->get_type_mgr()->GetRegisteredType(&floatScalar); + if (floatReg == nullptr) return 0u; + uint32_t componentIds[4] = {0u, 0u, 0u, 0u}; + for (uint32_t i = 0; i < 4; ++i) { + componentIds[i] = MakeScalarConstant(context, floatReg, + FloatBits(static_cast(channelMax[i]))); + if (componentIds[i] == 0u) return 0u; + } + return MakeVec4Constant(context, material.FvecTypeId, componentIds); + } } // namespace Uint WidenImageFormatsPass::WidenedCoreEsslImageFormat(Uint glInternalFormat) { @@ -296,6 +527,17 @@ namespace MobileGL { return GLInternalFormatOfSpirvImageFormat(widening.Carrier); } + bool WidenImageFormatsPass::NormalizedImageCarrierCodes(Uint glInternalFormat, + Uint32 (&outChannelMax)[4], + bool& outSignedNormalized) { + const ImageFormatWidening widening = + WideningOfSpirvImageFormat(SpirvImageFormatOfGL(glInternalFormat)); + if (!widening || !widening.CarriesNormalizedCodes()) return false; + for (Uint i = 0; i < 4; ++i) outChannelMax[i] = widening.ChannelMax[i]; + outSignedNormalized = widening.SignedNormalized; + return true; + } + Uint WidenImageFormatsPass::ImageFormatChannelCount(Uint glInternalFormat) { return ChannelsOfSpirvImageFormat(SpirvImageFormatOfGL(glInternalFormat)); } @@ -344,15 +586,23 @@ namespace MobileGL { spv::ImageFormat Carrier = spv::ImageFormat::Unknown; uint32_t Channels = 0; uint32_t SampledTypeId = 0; + // 0 unless the carrier holds NORMALIZED CODES, in which case it is the vec4 of + // per-channel denominators the conversion divides by and multiplies back up. + uint32_t DenominatorId = 0; + bool SignedNormalized = false; }; std::map widenedByTypeId; + Bool anyNormalizedCarrier = false; for (Instruction* type : imageTypes) { const auto format = static_cast(type->GetSingleWordInOperand(kImageFormatOperand)); const ImageFormatWidening widening = WideningOfSpirvImageFormat(format); - widenedByTypeId.emplace(type->result_id(), - WidenedImage{widening.Carrier, widening.Channels, - type->GetSingleWordInOperand(kImageSampledTypeOperand)}); + anyNormalizedCarrier = anyNormalizedCarrier || widening.CarriesNormalizedCodes(); + widenedByTypeId.emplace( + type->result_id(), + WidenedImage{widening.Carrier, widening.Channels, + type->GetSingleWordInOperand(kImageSampledTypeOperand), 0u, + widening.SignedNormalized}); } // Collect the accesses BEFORE anything is mutated, and refuse the whole rewrite if @@ -517,6 +767,196 @@ namespace MobileGL { } } + // ...and the same for the normalized carriers, whose rewrite needs a good deal + // more of both: the uvec4 an OpImageRead of the carrier yields, the ivec4 the + // signed code is sign-extended in, the GLSL.std.450 import the clamp and the sign + // come from, and one vec4 of denominators per DISTINCT channel-width set (all + // 65535 for the unsigned 16-bit formats, all 32767 for the signed ones, and + // (1023, 1023, 1023, 3) for rgb10_a2, whose channels are not all the same width). + NormalizedCarrierMaterial normalizedMaterial; + if (anyNormalizedCarrier) { + normalizedMaterial = ResolveNormalizedCarrierMaterial(irContext); + if (!normalizedMaterial) { + return Status::SuccessWithoutChange; + } + Instruction* firstNormalizedImageType = nullptr; + for (Instruction* type : imageTypes) { + const auto widenedIt = widenedByTypeId.find(type->result_id()); + if (widenedIt == widenedByTypeId.end()) continue; + const ImageFormatWidening widening = WideningOfSpirvImageFormat( + static_cast(type->GetSingleWordInOperand(kImageFormatOperand))); + if (!widening.CarriesNormalizedCodes()) continue; + // The shader asked for a gvec4 of the ORIGINAL sampled type, and for these + // seven that type is float. An integer image declared with a normalized + // format is not something glslang can produce, so a module that somehow + // holds one is refused rather than converted through a type it never had. + const Instruction* sampledType = defUseMgr->GetDef(widenedIt->second.SampledTypeId); + if (sampledType == nullptr || sampledType->opcode() != spv::Op::OpTypeFloat || + sampledType->GetSingleWordInOperand(0) != 32) { + return Status::SuccessWithoutChange; + } + const uint32_t denominatorId = + ResolveDenominatorConstant(irContext, normalizedMaterial, widening.ChannelMax); + if (denominatorId == 0u) { + return Status::SuccessWithoutChange; + } + widenedIt->second.DenominatorId = denominatorId; + if (firstNormalizedImageType == nullptr) firstNormalizedImageType = type; + } + // ...and the declarations have to reach the module in the right ORDER, not + // just exist. See HoistTypeDeclarationsBefore: the type manager appends, and + // the image type that names the new uint32 is already further up. + if (firstNormalizedImageType != nullptr) { + HoistTypeDeclarationsBefore( + irContext, firstNormalizedImageType, + {normalizedMaterial.FloatTypeId, normalizedMaterial.IntTypeId, + normalizedMaterial.UintTypeId, normalizedMaterial.FvecTypeId, + normalizedMaterial.IvecTypeId, normalizedMaterial.UvecTypeId}); + } + } + + // The two halves of GL 4.6 2.3.5 for a normalized carrier, spelled as SPIR-V. + // + // UNPACK (imageLoad), unsigned: f = c / (2^b - 1) + // UNPACK (imageLoad), signed: f = max(c / (2^(b-1) - 1), -1) + // PACK (imageStore), unsigned: c = round(clamp(f, 0, 1) * (2^b - 1)) + // PACK (imageStore), signed: c = round(clamp(f, -1, 1) * (2^(b-1) - 1)) + // + // `round` is round-to-NEAREST, and GL leaves the tie direction to the + // implementation ("if two values are equally near, the implementation may choose + // either"). This one always rounds a tie AWAY FROM ZERO, which is a legal choice + // and, unlike GLSL's own round(), a deterministic one - so the boundary cases can + // be pinned by a test rather than described. It is spelled as a truncation of + // x + 0.5*sign(x), because OpConvertFToU/OpConvertFToS truncate toward zero. + // + // A signed code is stored in an UNSIGNED carrier channel, so pack masks it to the + // low sixteen bits (a negative uint32 is out of an rgba16ui channel's range, and + // what a store does with an out-of-range integer is not defined) and unpack + // sign-extends it back with a shift pair. + // + // Both operate on all FOUR channels at once, including the surplus ones a one- or + // two-channel format does not have: the mask shuffle runs on the float side either + // way, so whatever the surplus channels hold is discarded on the way out and + // written as the format's own 0 and 1 on the way in. + auto insertUnpack = [&](Instruction* before, const WidenedImage& widened, + uint32_t rawId) -> uint32_t { + const auto emit = [&](spv::Op opcode, uint32_t typeId, + Instruction::OperandList operands) -> uint32_t { + const uint32_t resultId = irContext->TakeNextId(); + if (resultId == 0u) return 0u; + before->InsertBefore(spvtools::MakeUnique(irContext, opcode, typeId, resultId, + Move(operands))); + return resultId; + }; + // The raw texel through an OpCopyObject before anything reads it, which costs + // nothing in SPIR-V and is load-bearing in the ESSL: SPIRV-Cross emits a copy + // of a non-opaque value as a real `uvec4 _n = imageLoad(...);` statement, + // where the read's own result is FORWARDED into whatever consumes it. Mesa's + // llvmpipe compiler miscompiles the forwarded form - `vec4(imageLoad(img, c))` + // written straight into an expression comes back as zeroes, and 0xFFFF comes + // back as a NaN, while the identical arithmetic on a named uvec4 is correct. + // Measured with hand-written core-format ESSL (an rgba16ui uimage2D read into + // an rgba32f image2D), so it is the driver rather than anything this pass or + // the emitter does; the copy is the cheapest way to stay out of it, and every + // other driver folds it away. + const uint32_t texelId = + emit(spv::Op::OpCopyObject, normalizedMaterial.UvecTypeId, + {{SPV_OPERAND_TYPE_ID, {rawId}}}); + if (texelId == 0u) return 0u; + uint32_t codeId = 0; + if (widened.SignedNormalized) { + const uint32_t asIntId = + emit(spv::Op::OpBitcast, normalizedMaterial.IvecTypeId, + {{SPV_OPERAND_TYPE_ID, {texelId}}}); + if (asIntId == 0u) return 0u; + const uint32_t shiftedUpId = + emit(spv::Op::OpShiftLeftLogical, normalizedMaterial.IvecTypeId, + {{SPV_OPERAND_TYPE_ID, {asIntId}}, + {SPV_OPERAND_TYPE_ID, {normalizedMaterial.ShiftWidthId}}}); + if (shiftedUpId == 0u) return 0u; + const uint32_t signExtendedId = + emit(spv::Op::OpShiftRightArithmetic, normalizedMaterial.IvecTypeId, + {{SPV_OPERAND_TYPE_ID, {shiftedUpId}}, + {SPV_OPERAND_TYPE_ID, {normalizedMaterial.ShiftWidthId}}}); + if (signExtendedId == 0u) return 0u; + codeId = emit(spv::Op::OpConvertSToF, normalizedMaterial.FvecTypeId, + {{SPV_OPERAND_TYPE_ID, {signExtendedId}}}); + } else { + codeId = emit(spv::Op::OpConvertUToF, normalizedMaterial.FvecTypeId, + {{SPV_OPERAND_TYPE_ID, {texelId}}}); + } + if (codeId == 0u) return 0u; + const uint32_t normalizedId = + emit(spv::Op::OpFDiv, normalizedMaterial.FvecTypeId, + {{SPV_OPERAND_TYPE_ID, {codeId}}, {SPV_OPERAND_TYPE_ID, {widened.DenominatorId}}}); + if (normalizedId == 0u || !widened.SignedNormalized) return normalizedId; + // -2^(b-1) is representable in the code but GL clamps it to -1: the signed + // decode is max(c / (2^(b-1) - 1), -1), not the bare division. + return emit(spv::Op::OpExtInst, normalizedMaterial.FvecTypeId, + {{SPV_OPERAND_TYPE_ID, {normalizedMaterial.Glsl450Id}}, + {SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER, {kGlslFMax}}, + {SPV_OPERAND_TYPE_ID, {normalizedId}}, + {SPV_OPERAND_TYPE_ID, {normalizedMaterial.MinusOneId}}}); + }; + + auto insertPack = [&](Instruction* before, const WidenedImage& widened, + uint32_t valueId) -> uint32_t { + const auto emit = [&](spv::Op opcode, uint32_t typeId, + Instruction::OperandList operands) -> uint32_t { + const uint32_t resultId = irContext->TakeNextId(); + if (resultId == 0u) return 0u; + before->InsertBefore(spvtools::MakeUnique(irContext, opcode, typeId, resultId, + Move(operands))); + return resultId; + }; + const uint32_t lowBoundId = + widened.SignedNormalized ? normalizedMaterial.MinusOneId : normalizedMaterial.ZeroId; + const uint32_t clampedId = + emit(spv::Op::OpExtInst, normalizedMaterial.FvecTypeId, + {{SPV_OPERAND_TYPE_ID, {normalizedMaterial.Glsl450Id}}, + {SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER, {kGlslFClamp}}, + {SPV_OPERAND_TYPE_ID, {valueId}}, + {SPV_OPERAND_TYPE_ID, {lowBoundId}}, + {SPV_OPERAND_TYPE_ID, {normalizedMaterial.OneId}}}); + if (clampedId == 0u) return 0u; + const uint32_t scaledId = + emit(spv::Op::OpFMul, normalizedMaterial.FvecTypeId, + {{SPV_OPERAND_TYPE_ID, {clampedId}}, + {SPV_OPERAND_TYPE_ID, {widened.DenominatorId}}}); + if (scaledId == 0u) return 0u; + uint32_t biasId = normalizedMaterial.HalfId; + if (widened.SignedNormalized) { + // 0.5 * sign(x), so the truncation below rounds a tie away from zero on + // both sides. sign(0) is 0, which leaves an exact zero exactly zero. + const uint32_t signId = emit(spv::Op::OpExtInst, normalizedMaterial.FvecTypeId, + {{SPV_OPERAND_TYPE_ID, {normalizedMaterial.Glsl450Id}}, + {SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER, {kGlslFSign}}, + {SPV_OPERAND_TYPE_ID, {scaledId}}}); + if (signId == 0u) return 0u; + biasId = emit(spv::Op::OpFMul, normalizedMaterial.FvecTypeId, + {{SPV_OPERAND_TYPE_ID, {signId}}, + {SPV_OPERAND_TYPE_ID, {normalizedMaterial.HalfId}}}); + if (biasId == 0u) return 0u; + } + const uint32_t roundedId = + emit(spv::Op::OpFAdd, normalizedMaterial.FvecTypeId, + {{SPV_OPERAND_TYPE_ID, {scaledId}}, {SPV_OPERAND_TYPE_ID, {biasId}}}); + if (roundedId == 0u) return 0u; + if (!widened.SignedNormalized) { + return emit(spv::Op::OpConvertFToU, normalizedMaterial.UvecTypeId, + {{SPV_OPERAND_TYPE_ID, {roundedId}}}); + } + const uint32_t signedCodeId = emit(spv::Op::OpConvertFToS, normalizedMaterial.IvecTypeId, + {{SPV_OPERAND_TYPE_ID, {roundedId}}}); + if (signedCodeId == 0u) return 0u; + const uint32_t asUintId = emit(spv::Op::OpBitcast, normalizedMaterial.UvecTypeId, + {{SPV_OPERAND_TYPE_ID, {signedCodeId}}}); + if (asUintId == 0u) return 0u; + return emit(spv::Op::OpBitwiseAnd, normalizedMaterial.UvecTypeId, + {{SPV_OPERAND_TYPE_ID, {asUintId}}, + {SPV_OPERAND_TYPE_ID, {normalizedMaterial.LowWordMaskId}}}); + }; + // Masks first, while every image type still carries its ORIGINAL format: the // rewrite below only touches the format operand, so the accesses' types do not // move and the order is free either way - but doing it first keeps a failed @@ -524,10 +964,13 @@ namespace MobileGL { for (Instruction* write : writes) { const WidenedImage* widened = widenedOf(write); if (widened == nullptr) continue; + const Bool normalized = widened->DenominatorId != 0u; // A carrier with as many channels as the original (rgb10_a2ui in rgba16ui) has // no surplus channel to pin, and the shuffle would select (0, 1, 2, 3) from the - // texel - an identity the emitter would still print. Left out entirely. - if (widened->Channels >= 4) continue; + // texel - an identity the emitter would still print. Left out entirely, unless + // the texel still has to be PACKED, in which case the store is rewritten + // anyway and only the shuffle is skipped. + if (widened->Channels >= 4 && !normalized) continue; uint32_t zeroOneId = 0; uint32_t vec4TypeId = 0; if (!resolveMaskMaterial(widened->SampledTypeId, zeroOneId, vec4TypeId)) { @@ -540,22 +983,35 @@ namespace MobileGL { if (texel == nullptr || texel->type_id() != vec4TypeId) { return Status::SuccessWithoutChange; } - const uint32_t maskedId = irContext->TakeNextId(); - if (maskedId == 0) return Status::Failure; - Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {texelId}}, - {SPV_OPERAND_TYPE_ID, {zeroOneId}}}; - for (const Operand& component : maskComponents(widened->Channels)) { - shuffleOperands.push_back(component); + uint32_t storedId = texelId; + if (widened->Channels < 4) { + const uint32_t maskedId = irContext->TakeNextId(); + if (maskedId == 0) return Status::Failure; + Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {texelId}}, + {SPV_OPERAND_TYPE_ID, {zeroOneId}}}; + for (const Operand& component : maskComponents(widened->Channels)) { + shuffleOperands.push_back(component); + } + write->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpVectorShuffle, vec4TypeId, maskedId, shuffleOperands)); + storedId = maskedId; } - write->InsertBefore(spvtools::MakeUnique( - irContext, spv::Op::OpVectorShuffle, vec4TypeId, maskedId, shuffleOperands)); - write->SetInOperand(kImageWriteTexelOperand, {maskedId}); + // The pack goes AFTER the mask, so the carrier's surplus channels are written + // as the codes GL's own 0 and 1 quantise to (0 and the channel maximum) rather + // than as raw zeroes and ones - which is what a later imageLoad, and the + // upload that seeds an untouched level, both have to agree with. + if (normalized) { + storedId = insertPack(write, *widened, storedId); + if (storedId == 0u) return Status::Failure; + } + write->SetInOperand(kImageWriteTexelOperand, {storedId}); } for (Instruction* read : reads) { const WidenedImage* widened = widenedOf(read); if (widened == nullptr) continue; - if (widened->Channels >= 4) continue; // see the store loop + const Bool normalized = widened->DenominatorId != 0u; + if (widened->Channels >= 4 && !normalized) continue; // see the store loop uint32_t zeroOneId = 0; uint32_t vec4TypeId = 0; if (!resolveMaskMaterial(widened->SampledTypeId, zeroOneId, vec4TypeId)) { @@ -569,6 +1025,11 @@ namespace MobileGL { // existing use of the read stays intact without a ReplaceAllUsesWith that // would also rewrite the shuffle's own operand (the idiom // EmulateNoPerspectivePass uses for the same reason). + // + // Under a normalized carrier the copy reads a uvec4 rather than the vec4 the + // shader asked for - that is the whole point of the carrier - and the unpack + // in between brings it back. Every extra instruction is inserted in front of + // the original too, so the chain stays in order. const uint32_t rawReadId = irContext->TakeNextId(); if (rawReadId == 0) return Status::Failure; Instruction::OperandList readOperands; @@ -576,9 +1037,18 @@ namespace MobileGL { readOperands.push_back(read->GetInOperand(i)); } read->InsertBefore(spvtools::MakeUnique( - irContext, spv::Op::OpImageRead, vec4TypeId, rawReadId, readOperands)); + irContext, spv::Op::OpImageRead, + normalized ? normalizedMaterial.UvecTypeId : vec4TypeId, rawReadId, readOperands)); + uint32_t loadedId = rawReadId; + if (normalized) { + loadedId = insertUnpack(read, *widened, rawReadId); + if (loadedId == 0u) return Status::Failure; + } + // Always a shuffle, even at four channels: it is what carries the ORIGINAL + // result id, which every existing use still names. At four channels the + // selectors are the identity (0, 1, 2, 3), so nothing is substituted. read->SetOpcode(spv::Op::OpVectorShuffle); - Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {rawReadId}}, + Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {loadedId}}, {SPV_OPERAND_TYPE_ID, {zeroOneId}}}; for (const Operand& component : maskComponents(widened->Channels)) { shuffleOperands.push_back(component); @@ -586,13 +1056,20 @@ namespace MobileGL { read->SetInOperands(Move(shuffleOperands)); } - // The declaration itself, last. Only the format operand moves: the carrier has the - // same component type as the original by construction, so the OpTypeImage's - // Sampled Type still agrees with it (which is what spirv-val checks) and no - // pointer, array or access-chain type has to be rebuilt. + // The declaration itself, last. For most carriers only the format operand moves: + // the carrier has the same component type as the original by construction, so the + // OpTypeImage's Sampled Type still agrees with it (which is what spirv-val checks) + // and no pointer, array or access-chain type has to be rebuilt. + // + // A NORMALIZED carrier moves the Sampled Type too - float32 to uint32, which is + // what turns an image2D into a uimage2D - and it has to, because spirv-val + // requires the Sampled Type to match the format's component class. Nothing above + // the OpTypeImage has to change with it: the pointer, the variable and every + // OpLoad name the image type by ID, and the id is being mutated in place. // // Two image types can COLLIDE here - `layout(rg32f)` and `layout(rgba32f)` in one - // module both become Rgba32f - and duplicate non-aggregate type declarations are + // module both become Rgba32f, and so do `layout(rgba16)` image2D and + // `layout(rgba16ui)` uimage2D - and duplicate non-aggregate type declarations are // invalid SPIR-V. The caller runs spirv-tools' RemoveDuplicates pass immediately // after this one, which joins them (and cascades to the pointer and array types // that named them) rather than this pass carrying its own join. @@ -604,6 +1081,9 @@ namespace MobileGL { // module that has since grown instructions it was never told about. Every // analysis is dropped below instead. type->SetInOperand(kImageFormatOperand, {static_cast(widenedIt->second.Carrier)}); + if (widenedIt->second.DenominatorId != 0u) { + type->SetInOperand(kImageSampledTypeOperand, {normalizedMaterial.UintTypeId}); + } } // StorageImageExtendedFormats is deliberately left declared even though every diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h index 76d22e36..aeee3f1f 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h @@ -136,6 +136,21 @@ namespace MobileGL { // forty image formats. The count the widened accesses are masked back to. static Uint ImageFormatChannelCount(Uint glInternalFormat); + // Whether the carrier holds this format's channels as the INTEGER CODES of a + // NORMALIZED value rather than as the values themselves - true for the seven + // 16-bit and 10-bit normalized formats and nothing else. `outChannelMax` takes the + // largest code each channel can hold (2^b - 1 unsigned, 2^(b-1) - 1 signed), which + // is the denominator of GL 4.6 2.3.5 for that channel; `outSignedNormalized` says + // which of the two conversions applies. + // + // DirectGLES asks this on both sides of the transfer: the upload pads a missing + // alpha with outChannelMax[3] rather than the transfer type's own 1 (through a + // uint carrier "one" is the saturated CODE, not the integer one), and + // glGetTexImage divides the codes back out, because the ES storage is an integer + // texture the client still expects to read as floats. + static bool NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4], + bool& outSignedNormalized); + static spvtools::Optimizer::PassToken CreateWidenImageFormatsPass( bool onlyFormatsSpirvCrossRefusesToPrint = false);