mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
Compare commits
8
Commits
e4f41e0fd3
...
7d68a17774
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d68a17774 | ||
|
|
415645ccdd | ||
|
|
e6d03eb2a1 | ||
|
|
5e29e7d266 | ||
|
|
7eac33d17b | ||
|
|
8f19ce6fa7 | ||
|
|
d0f7fb99db | ||
|
|
d4247db6c3 |
@@ -445,12 +445,36 @@ namespace MobileGL {
|
||||
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
|
||||
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
|
||||
}
|
||||
// Whether this backend can CONSUME a shader module that still declares 64-bit floats,
|
||||
// i.e. whether `double` survives the transpile instead of being narrowed to `float`
|
||||
// (ShaderTranspiler::DemoteFloat64Pass). Detected, never assumed:
|
||||
// * DirectVulkan sets it from VkPhysicalDeviceFeatures::shaderFloat64, the feature
|
||||
// VUID-VkShaderModuleCreateInfo-pCode-08740 requires before a module declaring
|
||||
// OpCapability Float64 may be created at all. lavapipe has it; Adreno and Mali
|
||||
// both report VK_FALSE, so no real mobile device does.
|
||||
// * DirectGLES can NEVER have it. GLSL ES has no 64-bit float type in any version
|
||||
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES
|
||||
// profile") and the demotion there is mathematically mandatory, always.
|
||||
// Defaults to false so a backend that never sets it - and the no-backend case, which
|
||||
// is what standalone shader compiles and the unit tests run under - keeps the
|
||||
// demotion, which is the behaviour that works everywhere.
|
||||
Bool SupportsShaderFloat64 = false;
|
||||
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
|
||||
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
|
||||
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
|
||||
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
|
||||
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
|
||||
// all. Defaults to false so a backend that never sets it gets the conservative answer.
|
||||
//
|
||||
// INDEPENDENT of SupportsShaderFloat64, and it has to be: this flag decides a VkFormat
|
||||
// from the VAO ATTRIBUTE alone, which does not know what type the shader declared, and
|
||||
// glVertexAttribFormat(GL_DOUBLE) feeding a plain `in vec4` is both legal and common
|
||||
// (KHR-GL43.vertex_attrib_binding.basic-input-case4/5, advanced-bindingUpdate). A
|
||||
// backend with native fp64 that still cannot FETCH 64 bits keeps this false and relies
|
||||
// on the per-MODULE rule in ShaderCompiler::SanitizeAndOptimizeBinary instead: a vertex
|
||||
// module that declares a 64-bit float INPUT is demoted whole, so the two shader-side
|
||||
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
|
||||
// still see one consistent world.
|
||||
Bool SupportsFloat64VertexAttributes = false;
|
||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
Uint32 SubgroupSize = 0;
|
||||
|
||||
@@ -1331,9 +1331,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||
}
|
||||
}
|
||||
// Not a driver question and never will be: OpenGL ES has no double-precision vertex format
|
||||
// and ESSL has no fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to
|
||||
// land on this backend regardless of what the driver underneath happens to support.
|
||||
// Not a driver question and never will be: GLSL ES has no 64-bit float type in ANY version
|
||||
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES profile") and a
|
||||
// module that still declared Float64 would never reach the driver at all. The demotion is
|
||||
// mathematically mandatory here, on every device, forever - which is why this stays false
|
||||
// regardless of what the driver underneath happens to support.
|
||||
m_dynamicParameters.SupportsShaderFloat64 = false;
|
||||
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
|
||||
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||
|
||||
@@ -1088,7 +1088,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
backendObj = MakeShared<BackendTextureObject>();
|
||||
}
|
||||
if (imageBindableStorageRequired) {
|
||||
backendObj->RequireImageBindableStorage();
|
||||
backendObj->RequireImageBindableStorage(textureObject);
|
||||
}
|
||||
backendObj->SyncTextureParamsToBackend(textureObject);
|
||||
backendObj->SyncBuiltinSamplerToBackend(textureObject);
|
||||
@@ -1483,21 +1483,45 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// already calls that undefined, and inventing a carrier for it would only make the
|
||||
// out-of-class read wider.
|
||||
//
|
||||
// A BUFFER texture is excluded on both sides: it has no storage of its own to widen
|
||||
// (its texels are the application's buffer object), so WidenImageFormatsPass declines
|
||||
// every buffer image and the bind must decline with it, or the driver would be handed
|
||||
// a carrier the shader never addressed. See the Dim::Buffer guard there for the
|
||||
// 32-byte GL_RG32F measurement that pinned it.
|
||||
// A BUFFER texture is excluded from the WIDENING on both sides: it has no storage of
|
||||
// its own to widen (its texels are the application's buffer object), so
|
||||
// WidenImageFormatsPass declines to widen every buffer image and the bind must decline
|
||||
// with it, or the driver would be handed a carrier the shader never addressed. See the
|
||||
// Dim::Buffer guard there for the 32-byte GL_RG32F measurement that pinned it.
|
||||
//
|
||||
// What a buffer image takes instead is the SPLIT, which is the same three-layer move
|
||||
// through a different door: a private glTexBuffer view names the single-channel base
|
||||
// format, the bind below names it too, and the shader subscripts it two components per
|
||||
// original texel. Same gate on all three, so they cannot disagree.
|
||||
//
|
||||
// The split view is a SEPARATE texture name over the same buffer, and the bind has to
|
||||
// name it rather than the application's own: the application's texture keeps the
|
||||
// format it asked for so that a samplerBuffer reading the same buffer texture - which
|
||||
// is NOT subscript-rewritten - still sees whole texels. See
|
||||
// BackendTextureObject::m_bufferImageSplitViewId.
|
||||
GLenum bindFormat = imageBinding.Format;
|
||||
if (imageBinding.Texture->GetTarget() != TextureTarget::TextureBuffer &&
|
||||
TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) {
|
||||
GLuint bindTextureId = backendTexture->GetBackendTextureId();
|
||||
if (imageBinding.Texture->GetTarget() == TextureTarget::TextureBuffer) {
|
||||
if (TextureImpl::GetImageBindableBufferSplitFormat(imageBinding.Texture->GetFormat()) !=
|
||||
GL_UNKNOWN_MGL) {
|
||||
if (const GLenum boundFormatSplit = TextureImpl::GetImageBindableBufferSplitFormat(
|
||||
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
|
||||
boundFormatSplit != GL_UNKNOWN_MGL) {
|
||||
bindFormat = boundFormatSplit;
|
||||
if (const Uint splitViewId = backendTexture->GetBufferImageSplitViewId();
|
||||
splitViewId != 0) {
|
||||
bindTextureId = splitViewId;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) {
|
||||
const auto boundFormatWidening = TextureImpl::GetImageBindableStorageWidening(
|
||||
MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format));
|
||||
if (boundFormatWidening) {
|
||||
bindFormat = boundFormatWidening.InternalFormat;
|
||||
}
|
||||
}
|
||||
g_GLESFuncs.glBindImageTexture(unit, backendTexture->GetBackendTextureId(), imageBinding.Level,
|
||||
g_GLESFuncs.glBindImageTexture(unit, bindTextureId, imageBinding.Level,
|
||||
layered, layer, imageBinding.Access, bindFormat);
|
||||
}
|
||||
|
||||
@@ -7687,9 +7711,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 +7752,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 +7782,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
};
|
||||
WideReadCandidate candidates[4];
|
||||
Int candidateCount = 0;
|
||||
if (mapping.isInteger) {
|
||||
if (normalizedCarrier.Active()) {
|
||||
// The storage IS an integer texture, whatever the application's format says, so the
|
||||
// only read that can answer is the integer one. The codes it hands back are turned
|
||||
// into the floats the client asked for below.
|
||||
candidates[candidateCount++] = {GL_RGBA_INTEGER, GL_UNSIGNED_INT};
|
||||
} else if (mapping.isInteger) {
|
||||
if (GetWideReadChannelCount(static_cast<GLenum>(implFormat)) > 0 && IsIntegerReadFormat(implFormat) &&
|
||||
(implType == GL_INT || implType == GL_UNSIGNED_INT)) {
|
||||
candidates[candidateCount++] = {static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)};
|
||||
@@ -7794,6 +7849,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedCarrier.Active()) {
|
||||
// GL 4.6 2.3.5, the same conversion the shader-side unpack does and with the same
|
||||
// denominators, so a texel an imageStore wrote and a texel the upload seeded read back
|
||||
// identically: f = c / (2^b - 1) unsigned, f = max(c / (2^(b-1) - 1), -1) signed, with
|
||||
// the signed code recovered from the low sixteen bits of the unsigned channel.
|
||||
const SizeT pixelCount = static_cast<SizeT>(width) * static_cast<SizeT>(height);
|
||||
Vector<Uint8> floatWide(pixelCount * 4 * sizeof(Float));
|
||||
auto* dst = reinterpret_cast<Float*>(floatWide.data());
|
||||
const auto* src = reinterpret_cast<const Uint32*>(wide.data());
|
||||
for (SizeT i = 0; i < pixelCount; ++i) {
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
const Uint32 code = src[i * 4 + channel];
|
||||
const auto denominator = static_cast<Float>(normalizedCarrier.ChannelMax[channel]);
|
||||
if (normalizedCarrier.SignedNormalized) {
|
||||
const auto signedCode = static_cast<Int16>(static_cast<Uint16>(code));
|
||||
dst[i * 4 + channel] =
|
||||
std::max(static_cast<Float>(signedCode) / denominator, -1.0f);
|
||||
} else {
|
||||
dst[i * 4 + channel] = static_cast<Float>(code) / denominator;
|
||||
}
|
||||
}
|
||||
}
|
||||
wide = Move(floatWide);
|
||||
wideType = GL_FLOAT;
|
||||
readChannels = 4;
|
||||
}
|
||||
if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) {
|
||||
// Unpack the packed words into a float wide buffer (full 10-bit precision on e.g.
|
||||
// GL_RGB10_A2 attachments, whose implementation read pair is RGBA/2_10_10_10_REV).
|
||||
@@ -8441,6 +8522,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 +8646,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void* sliceDst = static_cast<Uint8*>(pixels) + sliceOffset;
|
||||
if (!ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, sliceDst,
|
||||
/*honorPackImageParams=*/false,
|
||||
/*applyFixedPointReadClamp=*/false, forceOpaqueAlpha)) {
|
||||
/*applyFixedPointReadClamp=*/false, forceOpaqueAlpha,
|
||||
normalizedCarrier)) {
|
||||
allSlicesRead = false;
|
||||
break;
|
||||
}
|
||||
@@ -8573,7 +8670,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;
|
||||
}
|
||||
|
||||
@@ -2382,8 +2382,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteTextures) {
|
||||
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
|
||||
if (m_bufferImageSplitViewId != 0) {
|
||||
g_GLESFuncs.glDeleteTextures(1, &m_bufferImageSplitViewId);
|
||||
}
|
||||
}
|
||||
m_backendTextureId = 0;
|
||||
m_bufferImageSplitViewId = 0;
|
||||
}
|
||||
|
||||
void BackendTextureObject::Bind(GLenum target, Uint unit) {
|
||||
@@ -2408,12 +2412,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return m_backendTextureId;
|
||||
}
|
||||
|
||||
void BackendTextureObject::RequireImageBindableStorage() {
|
||||
void BackendTextureObject::RequireImageBindableStorage(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
||||
if (m_imageBindableStorageRequired) {
|
||||
return;
|
||||
}
|
||||
m_imageBindableStorageRequired = true;
|
||||
m_isInitialized = false;
|
||||
// Every level this object has ALREADY uploaded has to be replayed, because the
|
||||
// regeneration this transition schedules re-mints the storage in the image carrier and
|
||||
// only uploads levels the shadow still calls dirty - which, for a texture that was
|
||||
// synced before its first glBindImageTexture, is none of them. The new storage would
|
||||
// come out ALLOCATED AND EMPTY, and every texel the application defined before that
|
||||
// bind would be gone: the shader reads zeroes and the shadow still holds the data, so
|
||||
// glGetTexImage (which falls back to the shadow) keeps answering correctly and only
|
||||
// the image loads are wrong. Reached whenever anything syncs the texture first - a
|
||||
// glGetTexImage, a draw that samples it, an FBO attach - which is why it survived so
|
||||
// long: the scenario that binds the image immediately after uploading never sees it.
|
||||
if (auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject.get())) {
|
||||
const auto levelCount = mipmapObject->GetMipmapLevelCount();
|
||||
for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) {
|
||||
for (Uint level = 0; level < levelCount; ++level) {
|
||||
const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level);
|
||||
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0) continue;
|
||||
if (mipmapObject->GetMipmapByteSize(uploadTarget, level) == 0) continue;
|
||||
mipmapObject->MarkStorageDirty(uploadTarget, level, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
// The storage this re-mints may also be CHANNEL WIDENED (a GL_RG32F image is not
|
||||
// bindable on this driver at all, so it becomes a GL_RGBA32F carrying two channels),
|
||||
// and a widened texture's sampled view has to answer the channels the logical format
|
||||
@@ -2716,7 +2742,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// runs after any type conversion (which keeps the component count) has already happened.
|
||||
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize,
|
||||
const void* data, SizeT byteSize, GLenum uploadType,
|
||||
Vector<Uint8>& widenedData, Bool integerData) {
|
||||
Vector<Uint8>& widenedData, Bool integerData,
|
||||
Uint32 alphaOneCodeOverride) {
|
||||
Uint8 oneBits[8] = {};
|
||||
SizeT componentSize = 0;
|
||||
// One and two source components as well as three: the image-format widening carries
|
||||
@@ -2728,6 +2755,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
!GetUploadComponentOneBits(uploadType, integerData, oneBits, &componentSize)) {
|
||||
return data;
|
||||
}
|
||||
// ...except where the carrier holds CODES of a normalized value (GL_R16 in a
|
||||
// GL_RGBA16UI), where the transfer type says GL_UNSIGNED_SHORT and neither of that
|
||||
// type's two "ones" is right: the integer 1 is a code for 1/65535 and the saturated
|
||||
// 0xFFFF is only right for the UNSIGNED 16-bit formats, not the signed ones, whose
|
||||
// saturated code is 0x7FFF. The caller passes the channel's own maximum instead.
|
||||
// Written through a value of the component's own width rather than as the low
|
||||
// `componentSize` bytes of the Uint32, so the encoding does not turn on the host's
|
||||
// byte order.
|
||||
if (alphaOneCodeOverride != 0u) {
|
||||
if (componentSize == sizeof(Uint16)) {
|
||||
const auto one = static_cast<Uint16>(alphaOneCodeOverride);
|
||||
Memcpy(oneBits, &one, sizeof(one));
|
||||
} else if (componentSize == sizeof(Uint32)) {
|
||||
Memcpy(oneBits, &alphaOneCodeOverride, sizeof(alphaOneCodeOverride));
|
||||
} else if (componentSize == sizeof(Uint8)) {
|
||||
const auto one = static_cast<Uint8>(alphaOneCodeOverride);
|
||||
Memcpy(oneBits, &one, sizeof(one));
|
||||
}
|
||||
}
|
||||
|
||||
const SizeT srcTexelBytes = componentSize * componentCount;
|
||||
// Sized from the level, never from the source: the driver reads a full
|
||||
@@ -2901,19 +2947,63 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return widenedData.data();
|
||||
}
|
||||
|
||||
// The rgb10_a2 / rgb10_a2ui shadow split into the four GL_UNSIGNED_SHORT channel CODES its
|
||||
// GL_RGBA16UI carrier is uploaded as. GL_UNSIGNED_INT_2_10_10_10_REV puts the FIRST
|
||||
// component in the LOW bits (that is what REV means), so red is bits 0-9, green 10-19,
|
||||
// blue 20-29 and alpha 30-31.
|
||||
//
|
||||
// The same split serves both formats: an rgb10_a2ui channel's code IS its value, and an
|
||||
// rgb10_a2 channel's code is the numerator of value = code / (2^b - 1) that the shader-side
|
||||
// unpack divides out. Neither is scaled here - the carrier holds the format's own bits.
|
||||
//
|
||||
// Sized from the LEVEL, not the source, for the reason PrepareChannelWidenedUpload is: the
|
||||
// driver reads a full width*height*depth*4 shorts for the transfer it was handed.
|
||||
const void* PreparePackedIntWidenedUpload(const IntVec3& texelSize, const void* data,
|
||||
SizeT byteSize, Vector<Uint8>& widenedData) {
|
||||
constexpr SizeT kSourceTexelBytes = sizeof(Uint32);
|
||||
if (data == nullptr || byteSize < kSourceTexelBytes) {
|
||||
return data;
|
||||
}
|
||||
const SizeT texelCount = static_cast<SizeT>(std::max(texelSize.x(), 0)) *
|
||||
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
|
||||
static_cast<SizeT>(std::max(texelSize.z(), 1));
|
||||
if (texelCount == 0) {
|
||||
return data;
|
||||
}
|
||||
const SizeT copyTexelCount = std::min(texelCount, byteSize / kSourceTexelBytes);
|
||||
|
||||
widenedData.assign(texelCount * 4u * sizeof(Uint16), 0);
|
||||
const auto* src = static_cast<const Uint8*>(data);
|
||||
auto* dst = reinterpret_cast<Uint16*>(widenedData.data());
|
||||
for (SizeT i = 0; i < texelCount; ++i, dst += 4) {
|
||||
Uint32 packed = 0;
|
||||
if (i < copyTexelCount) {
|
||||
// Through a memcpy rather than a Uint32 read of `src`: the shadow is a byte
|
||||
// buffer with no alignment promise of its own.
|
||||
Memcpy(&packed, src + i * kSourceTexelBytes, sizeof(packed));
|
||||
}
|
||||
dst[0] = static_cast<Uint16>(packed & 0x3FFu);
|
||||
dst[1] = static_cast<Uint16>((packed >> 10u) & 0x3FFu);
|
||||
dst[2] = static_cast<Uint16>((packed >> 20u) & 0x3FFu);
|
||||
dst[3] = static_cast<Uint16>((packed >> 30u) & 0x3u);
|
||||
}
|
||||
return widenedData.data();
|
||||
}
|
||||
|
||||
// The transfer half of the image-format widening: an image-bindable texture whose ES
|
||||
// storage was widened to a core carrier is described to the driver as a four-component
|
||||
// transfer, so its narrower client data has to be repacked the same way the three-channel
|
||||
// colour-renderable widening repacks its own.
|
||||
//
|
||||
// Two shapes, because the carriers come in two kinds. Seventeen of the eighteen keep the
|
||||
// frontend format's component TYPE and only add channels, so padding the shadow out to
|
||||
// four components is the whole conversion. r11f_g11f_b10f does not: its shadow is one
|
||||
// PACKED 32-bit word per texel and its carrier is GL_RGBA16F, so the word has to be
|
||||
// DECODED into four floats. Reading it as three components of the carrier's type - what
|
||||
// the repack below would do - would take twelve bytes from a four-byte texel and shear
|
||||
// the level, which is what the allFormats LOAD walkers see and the STORE ones do not (a
|
||||
// store overwrites every texel the upload got wrong).
|
||||
// Three shapes, because the carriers come in three kinds. Most of them keep the frontend
|
||||
// format's component TYPE and only add channels, so padding the shadow out to four
|
||||
// components is the whole conversion. The two PACKED formats do not: their shadow is one
|
||||
// 32-bit word per texel, so the word has to be split - into four floats for
|
||||
// r11f_g11f_b10f's GL_RGBA16F, into four shorts for rgb10_a2ui's GL_RGBA16UI. Reading such
|
||||
// a word as components of the carrier's type - what the repack below would do - takes
|
||||
// twelve or sixteen bytes from a four-byte texel and shears the level, which is what the
|
||||
// allFormats LOAD walkers see and the STORE ones do not (a store overwrites every texel
|
||||
// the upload got wrong).
|
||||
//
|
||||
// Composes with PrepareFallbackUpload rather than replacing it, and the composition is a
|
||||
// no-op by construction: none of the widened formats is one GetWidenableClientComponentCount
|
||||
@@ -2926,14 +3016,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!widening || widening.SourceChannels == 0 || widening.SourceChannels > 4) {
|
||||
return data;
|
||||
}
|
||||
if (widening.PackedFloatSource) {
|
||||
switch (widening.SourceEncoding) {
|
||||
case TextureImpl::ImageWidenSourceEncoding::PackedFloat11f11f10f:
|
||||
return PreparePackedFloatWidenedUpload(texelSize, data, byteSize, widenedData);
|
||||
case TextureImpl::ImageWidenSourceEncoding::PackedInt2101010Rev:
|
||||
return PreparePackedIntWidenedUpload(texelSize, data, byteSize, widenedData);
|
||||
case TextureImpl::ImageWidenSourceEncoding::Components:
|
||||
break;
|
||||
}
|
||||
if (widening.SourceChannels == 4) {
|
||||
return data;
|
||||
}
|
||||
return PrepareChannelWidenedUpload(widening.SourceChannels, texelSize, data, byteSize, widening.Type,
|
||||
widenedData, widening.IntegerData);
|
||||
widenedData, widening.IntegerData,
|
||||
widening.CarriesNormalizedCodes() ? widening.ChannelMax[3] : 0u);
|
||||
}
|
||||
|
||||
// Overwrites the (internal format, format, type) triple GenerateTextureFormatInfo chose
|
||||
@@ -3730,6 +3826,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLenum glInternalFormat, glType, glFormat;
|
||||
TextureImpl::GenerateTextureFormatInfo(textureBufferObject->GetFormat(), &glInternalFormat, &glFormat,
|
||||
&glType, TextureTarget::TextureBuffer);
|
||||
// The view half of the buffer-image SPLIT. A buffer texture has no storage of its
|
||||
// own to widen, but the VIEW its format describes can be re-described one
|
||||
// component at a time over the same bytes - rg32f over N texels is r32f over 2N -
|
||||
// and WidenImageFormatsPass rewrites every access to subscript it that way. Only
|
||||
// for a texture that is actually image-bound: a sampled-only buffer texture keeps
|
||||
// the format the application asked for (see GetImageBindableBufferSplitFormat).
|
||||
//
|
||||
// The split goes on a SEPARATE name (m_bufferImageSplitViewId), not on this one.
|
||||
// Re-describing the application's own texture also re-describes what a
|
||||
// samplerBuffer reading it sees, and the sampler side is not subscript-rewritten -
|
||||
// so texelFetch(s, i) started returning component 2i of the base view instead of
|
||||
// texel i. rg32f is a legal SAMPLED buffer-texture format in ES 3.2; only the
|
||||
// IMAGE binding needs the split, so only the image binding's name carries it.
|
||||
const GLenum bufferImageSplitFormat =
|
||||
m_imageBindableStorageRequired
|
||||
? TextureImpl::GetImageBindableBufferSplitFormat(textureBufferObject->GetFormat())
|
||||
: GL_UNKNOWN_MGL;
|
||||
|
||||
if (needsRegeneration) {
|
||||
// Desktop GL has had buffer textures core since 3.1 and MobileGL advertises a
|
||||
@@ -3785,6 +3898,37 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
func, file, line, MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
|
||||
backendId, MG_Util::ConvertGLEnumToString(err).c_str());
|
||||
});
|
||||
|
||||
// The image half of the SPLIT, on its own name over the same buffer. Minted
|
||||
// lazily - only a texture that is both image-bound AND holds a format with no
|
||||
// ESSL image spelling ever gets one - and re-pointed here, in the same
|
||||
// regeneration gate as the view above, so the two never describe different
|
||||
// buffers or different windows of one.
|
||||
if (bufferImageSplitFormat != GL_UNKNOWN_MGL) {
|
||||
if (m_bufferImageSplitViewId == 0) {
|
||||
g_GLESFuncs.glGenTextures(1, &m_bufferImageSplitViewId);
|
||||
}
|
||||
if (m_bufferImageSplitViewId == 0) {
|
||||
MGLOG_E_ONCE("Failed to generate the buffer-image split view for texture %u; "
|
||||
"its image binding will read the unsplit view.",
|
||||
stateTextureObject->GetExternalIndex());
|
||||
} else {
|
||||
g_GLESFuncs.glBindTexture(GL_TEXTURE_BUFFER, m_bufferImageSplitViewId);
|
||||
if (rangeOffset == 0 && rangeSize == buffer->GetSize()) {
|
||||
CallTexBuffer(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId);
|
||||
} else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId,
|
||||
static_cast<GLintptr>(rangeOffset),
|
||||
static_cast<GLsizeiptr>(rangeSize))) {
|
||||
CallTexBuffer(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId);
|
||||
}
|
||||
// The raw bind above went behind Bind()'s shadow, which tracks objects
|
||||
// rather than names: leaving it claiming THIS object is bound would
|
||||
// make the next Bind(GL_TEXTURE_BUFFER) a no-op and leave the split
|
||||
// view bound in the application texture's place.
|
||||
g_boundTexturesCache[g_activeTextureUnit][static_cast<SizeT>(
|
||||
TextureTarget::TextureBuffer)] = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -5395,6 +5539,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// here whose carrier has a different per-channel layout. See
|
||||
// WidenImageFormatsPass.h.
|
||||
case glslang::ElfR11fG11fB10f: return 0x8C3A; // GL_R11F_G11F_B10F
|
||||
// 10/10/10/2 unsigned INTEGER channels in an rgba16ui: same component type, same
|
||||
// channel count, every value representable. Only the transfer is re-encoded.
|
||||
case glslang::ElfRgb10a2ui: return 0x906F; // GL_RGB10_A2UI
|
||||
// The seven NORMALIZED formats, carried in an rgba16ui as their own channel CODES.
|
||||
// These are the entries whose carrier changes the shader-visible type as well as
|
||||
// the qualifier (image2D becomes uimage2D), so every access through them is
|
||||
// wrapped in the GL 4.6 2.3.5 conversion - see WidenImageFormatsPass.h.
|
||||
case glslang::ElfRgba16: return 0x805B; // GL_RGBA16
|
||||
case glslang::ElfRg16: return 0x822C; // GL_RG16
|
||||
case glslang::ElfR16: return 0x822A; // GL_R16
|
||||
case glslang::ElfRgb10A2: return 0x8059; // GL_RGB10_A2
|
||||
case glslang::ElfRgba16Snorm: return 0x8F9B; // GL_RGBA16_SNORM
|
||||
case glslang::ElfRg16Snorm: return 0x8F99; // GL_RG16_SNORM
|
||||
case glslang::ElfR16Snorm: return 0x8F98; // GL_R16_SNORM
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -745,9 +745,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Returns `data` untouched when no widening applies. Pure CPU and context-free so a unit
|
||||
// test can exercise the exact packing the driver is handed; `widenedData` is the caller's
|
||||
// scratch buffer and has to outlive the returned pointer.
|
||||
// `alphaOneCodeOverride`, when non-zero, replaces the value written into the synthetic
|
||||
// alpha channel: an image carrier that holds a NORMALIZED format's channel CODES has to
|
||||
// pad alpha with that channel's saturated CODE (65535, 32767, 3), which neither of the
|
||||
// transfer type's own "ones" is.
|
||||
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data,
|
||||
SizeT byteSize, GLenum uploadType, Vector<Uint8>& widenedData,
|
||||
Bool integerData = false);
|
||||
Bool integerData = false, Uint32 alphaOneCodeOverride = 0u);
|
||||
|
||||
// Splits a GL_UNSIGNED_INT_2_10_10_10_REV shadow (rgb10_a2, rgb10_a2ui) into the four
|
||||
// GL_UNSIGNED_SHORT channel CODES its GL_RGBA16UI image carrier is uploaded as: red in
|
||||
// bits 0-9, green 10-19, blue 20-29, alpha 30-31. Pure CPU and context-free so a unit test
|
||||
// can pin the exact fields; `widenedData` is the caller's scratch and has to outlive the
|
||||
// returned pointer.
|
||||
const void* PreparePackedIntWidenedUpload(const IntVec3& texelSize, const void* data, SizeT byteSize,
|
||||
Vector<Uint8>& widenedData);
|
||||
|
||||
struct StateTextureBasicInfo { // Used for tracking texture state changes
|
||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||
@@ -782,10 +794,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
void RequireImageBindableStorage();
|
||||
// Marks the texture as one whose ES storage has to be image-bindable, which for a
|
||||
// non-core image format means re-minting it in the widening's carrier. Takes the state
|
||||
// object because the levels already uploaded have to be marked dirty again: the
|
||||
// re-mint allocates fresh storage and only replays what the shadow still calls dirty.
|
||||
void RequireImageBindableStorage(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||
// Whether this texture's ES storage was minted in an image carrier rather than in the
|
||||
// frontend format's own layout - the readback has to ask, because for a NORMALIZED
|
||||
// carrier the storage is an integer texture holding codes and glGetTexImage still owes
|
||||
// the application floats.
|
||||
Bool RequiresImageBindableStorage() const { return m_imageBindableStorageRequired; }
|
||||
void Bind(GLenum target, Uint unit = TempTextureUnit);
|
||||
Uint GetBackendTextureId() const;
|
||||
|
||||
// The id to hand glBindImageTexture for a SPLIT buffer image, or 0 when this texture
|
||||
// takes no split. See m_bufferImageSplitViewId.
|
||||
Uint GetBufferImageSplitViewId() const { return m_bufferImageSplitViewId; }
|
||||
|
||||
// Aggregate first-level clean gate for the per-draw trio
|
||||
// SyncTextureParamsToBackend + SyncBuiltinSamplerToBackend +
|
||||
// SyncMipmapsToBackend: EXACTLY the conjunction of their own early-outs
|
||||
@@ -822,6 +848,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void RecreateBackendTexture();
|
||||
|
||||
Uint m_backendTextureId = 0;
|
||||
// A SECOND buffer-texture name over the SAME buffer object, viewed in the split's
|
||||
// single-channel base format, used only as the glBindImageTexture target.
|
||||
//
|
||||
// The split needs the view to say r32f where the application said rg32f, but a buffer
|
||||
// texture that is image-bound may ALSO be read through a samplerBuffer - and the
|
||||
// sampler side is not subscript-rewritten, so re-describing the application's own
|
||||
// texture broke it: texelFetch(s, i) returned component 2i of the base view instead of
|
||||
// texel i's pair. That is exactly and only
|
||||
// KHR-GL42/43.shader_image_load_store.advanced-sync-imageAccess, which image-stores
|
||||
// into a GL_RG32F buffer texture and then reads the same texture through both an
|
||||
// imageBuffer and a samplerBuffer in one shader, comparing the two.
|
||||
//
|
||||
// Two names over one buffer cost nothing and alias exactly: a buffer texture owns no
|
||||
// storage, so both views are the application's bytes, and the split's whole premise is
|
||||
// that the two describe the same memory. The application's own name therefore keeps
|
||||
// the format it asked for - rg32f IS a legal SAMPLED buffer-texture format in ES 3.2,
|
||||
// it is only the IMAGE binding ES cannot spell - and the private name below carries
|
||||
// the split the shader was rewritten against. 0 when this texture takes no split.
|
||||
Uint m_bufferImageSplitViewId = 0;
|
||||
// ES context generation the id was created under; a dtor running after
|
||||
// that context died must not delete a foreign (recycled) name.
|
||||
Uint m_contextGeneration = 0;
|
||||
@@ -1168,23 +1213,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;
|
||||
|
||||
@@ -277,21 +277,62 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// its own; this call is only here to spell the transfer pair that describes it.
|
||||
MG_Util::TextureFormatProcessor::NormalizePixelFormat(carrier, Flags<PixelFormatNormalizeOptionBit>{},
|
||||
nullptr, &widening.Format, &widening.Type);
|
||||
// r11f_g11f_b10f is the one carrier that is not a channel widening, and the transfer
|
||||
// pair has to say so. Every other entry keeps the frontend format's own component
|
||||
// type - a GL_RG16F shadow is halves and so is its GL_RGBA16F carrier, so padding the
|
||||
// channels is the whole conversion. This shadow is a PACKED 32-bit word (GL_RGB with
|
||||
// GL_UNSIGNED_INT_10F_11F_11F_REV, TextureFormatProcessor::NormalizePixelFormat), and
|
||||
// no ES driver accepts that type for a GL_RGBA16F level. GL_FLOAT is asked for
|
||||
// instead - legal for GL_RGBA16F, and the type the unpack in
|
||||
// PrepareImageWidenedUpload writes - so the two sides name the same layout.
|
||||
if (internalFormat == TextureInternalFormat::R11FG11FB10F) {
|
||||
// The two carriers that are not channel widenings, whose transfer pair has to say so.
|
||||
// Every other entry keeps the frontend format's own component type - a GL_RG16F shadow
|
||||
// is halves and so is its GL_RGBA16F carrier, so padding the channels is the whole
|
||||
// conversion. These two shadows are a PACKED 32-bit word per texel
|
||||
// (TextureFormatProcessor::NormalizePixelFormat), and no ES driver accepts either
|
||||
// packed type for the carrier's level, so the transfer names the carrier's own layout
|
||||
// and PrepareImageWidenedUpload splits the word into it.
|
||||
switch (internalFormat) {
|
||||
case TextureInternalFormat::R11FG11FB10F:
|
||||
// GL_UNSIGNED_INT_10F_11F_11F_REV -> GL_RGBA / GL_FLOAT, legal for GL_RGBA16F.
|
||||
widening.Format = GL_RGBA;
|
||||
widening.Type = GL_FLOAT;
|
||||
widening.PackedFloatSource = true;
|
||||
widening.SourceEncoding = ImageWidenSourceEncoding::PackedFloat11f11f10f;
|
||||
break;
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
case TextureInternalFormat::RGB10A2:
|
||||
// GL_UNSIGNED_INT_2_10_10_10_REV -> the GL_RGBA_INTEGER / GL_UNSIGNED_SHORT the
|
||||
// GL_RGBA16UI carrier already asked for above; only the split is new. The two
|
||||
// formats share it: rgb10_a2's channel codes are the same fields rgb10_a2ui's are,
|
||||
// and what the shader divides them by is not the transfer's business.
|
||||
widening.SourceEncoding = ImageWidenSourceEncoding::PackedInt2101010Rev;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// The seven normalized formats whose carrier holds CODES rather than values. Both
|
||||
// halves of the transfer need to know: a missing alpha is padded with the saturated
|
||||
// code rather than the integer 1, and glGetTexImage has to divide the codes back out.
|
||||
bool signedNormalized = false;
|
||||
Uint32 channelMax[4] = {0u, 0u, 0u, 0u};
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::NormalizedImageCarrierCodes(requested, channelMax,
|
||||
signedNormalized)) {
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
widening.ChannelMax[channel] = channelMax[channel];
|
||||
}
|
||||
widening.SignedNormalized = signedNormalized;
|
||||
}
|
||||
return widening;
|
||||
}
|
||||
|
||||
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat) {
|
||||
const GLenum requested = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||
const auto base = static_cast<GLenum>(
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SplitCoreEsslBufferImageFormat(requested));
|
||||
if (base == 0) {
|
||||
return GL_UNKNOWN_MGL;
|
||||
}
|
||||
// EXACTLY the arming WidenImageFormatsForEssl uses, for the reason the widening's is:
|
||||
// the shader, the glTexBuffer view and the glBindImageTexture argument must all split
|
||||
// or none of them may, or the shader subscripts a view the buffer is not described as.
|
||||
if (g_GLESCapabilities.SupportsExtendedImageFormats &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(requested)) {
|
||||
return GL_UNKNOWN_MGL;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
} // namespace TextureImpl
|
||||
namespace PrgramImpl {
|
||||
String ProcessOutColorLocations(const String& glslCode) {
|
||||
@@ -955,11 +996,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
String arraySuffix; // "" or "[7]"
|
||||
SizeT declStart = 0;
|
||||
SizeT declLength = 0;
|
||||
SizeT nameStart = 0; // the name token alone, for a rename that edits nothing else
|
||||
SizeT nameLength = 0;
|
||||
SizeT referenceCount = 0; // uses this pass recognized and accounted for
|
||||
Bool loaded = false;
|
||||
Bool stored = false;
|
||||
Bool unknownUse = false;
|
||||
Bool split = false;
|
||||
// SPIRV-Cross already tagged this one readonly or writeonly, so it needs no
|
||||
// qualifier repair - only the rename that keeps two stages from merging it.
|
||||
Bool preTaggedReadonly = false;
|
||||
Bool preTaggedWriteonly = false;
|
||||
};
|
||||
|
||||
// A rebuilt declaration. Keeps SPIRV-Cross's own word order (`uniform readonly
|
||||
@@ -1296,10 +1343,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), imageDeclRegex), last; it != last; ++it) {
|
||||
const std::smatch& match = *it;
|
||||
const String qualifiers = match[2].str();
|
||||
// Already legal: SPIRV-Cross decided one way, leave it alone.
|
||||
if (ContainsIdentifier(qualifiers, "readonly") || ContainsIdentifier(qualifiers, "writeonly")) {
|
||||
continue;
|
||||
}
|
||||
const Bool hasReadonly = ContainsIdentifier(qualifiers, "readonly");
|
||||
const Bool hasWriteonly = ContainsIdentifier(qualifiers, "writeonly");
|
||||
// Carrying BOTH is a spelling no per-stage access analysis produces (SPIRV-Cross
|
||||
// clears one decoration or the other as soon as it sees a load or a store), so it
|
||||
// came from the application and is identical in every stage. Nothing to do.
|
||||
if (hasReadonly && hasWriteonly) continue;
|
||||
|
||||
Bool hasFormat = false;
|
||||
Bool exemptFormat = false;
|
||||
@@ -1308,10 +1357,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
hasFormat = true;
|
||||
exemptFormat = IsMemoryQualifierExemptImageFormat(token);
|
||||
}
|
||||
// No format qualifier at all is a different (and, in ES, unconditionally
|
||||
// illegal) shape that GL_EXT_shader_image_load_formatted would be needed for;
|
||||
// SPIRV-Cross refuses to emit it for an ES target, so nothing to do here.
|
||||
if (!hasFormat || exemptFormat) continue;
|
||||
// A declaration carrying neither qualifier is illegal ES unless its format is
|
||||
// r32f/r32i/r32ui, and no format qualifier at all is a shape SPIRV-Cross refuses
|
||||
// to emit for an ES target. Either way there is no repair to make - and no rename
|
||||
// to make either, because a declaration with no access qualifier is spelled the
|
||||
// same in every stage.
|
||||
if (!hasReadonly && !hasWriteonly && (!hasFormat || exemptFormat)) continue;
|
||||
|
||||
ImageUniformDecl decl;
|
||||
decl.layout = match[1].str();
|
||||
@@ -1321,6 +1372,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
decl.arraySuffix = NormalizeDeclarationSpacing(match[5].str());
|
||||
decl.declStart = static_cast<SizeT>(match.position(0));
|
||||
decl.declLength = match[0].str().size();
|
||||
decl.nameStart = static_cast<SizeT>(match.position(4));
|
||||
decl.nameLength = match[4].str().size();
|
||||
decl.preTaggedReadonly = hasReadonly;
|
||||
decl.preTaggedWriteonly = hasWriteonly;
|
||||
decls.push_back(Move(decl));
|
||||
}
|
||||
if (decls.empty()) {
|
||||
@@ -1452,6 +1507,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// already readonly/writeonly in the source, or r32f/r32i/r32ui, which need no
|
||||
// qualifier - keep their names, and they are exactly the ones that already match
|
||||
// across stages.
|
||||
if (decl.preTaggedReadonly || decl.preTaggedWriteonly) {
|
||||
// No repair: SPIRV-Cross already emitted a legal qualifier. But it derived
|
||||
// that qualifier from THIS STAGE's accesses, so a uniform stored in one stage
|
||||
// and loaded in another arrives here `writeonly` in one and `readonly` in the
|
||||
// other under ONE name - precisely the same-name/mismatched-qualifier pair
|
||||
// Adreno merges while silently discarding the writing stage's stores
|
||||
// (advanced-memory-dependentInvocation; a raw-ES probe reproduces it with no
|
||||
// MobileGL in the process, and renaming either half fixes it). Keyed on the
|
||||
// qualifier for the same reason the repair below is: two stages that agree
|
||||
// spell the same alias and stay merged, so no shader gains an image uniform.
|
||||
const char* preTagPrefix =
|
||||
decl.preTaggedReadonly ? IMAGE_READONLY_ALIAS_PREFIX : IMAGE_WRITEONLY_ALIAS_PREFIX;
|
||||
decl.aliasName = MakeImageAliasName(preTagPrefix, decl.name, glslCode, takenNames);
|
||||
takenNames.push_back(decl.aliasName);
|
||||
// The name token alone: the qualifiers are already right, and re-emitting the
|
||||
// whole declaration would only risk changing them.
|
||||
edits.push_back({decl.nameStart, decl.nameLength, decl.aliasName});
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* aliasPrefix = decl.loaded && decl.stored ? IMAGE_SPLIT_READ_ALIAS_PREFIX
|
||||
: decl.stored ? IMAGE_WRITEONLY_ALIAS_PREFIX
|
||||
: IMAGE_READONLY_ALIAS_PREFIX;
|
||||
|
||||
@@ -102,6 +102,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// would see them. Closing it needs the per-draw-buffer colour mask the three-channel
|
||||
// widening already carries (FramebufferImpl::g_alphaWidenedDrawBufferMask) generalized
|
||||
// from "alpha" to a channel count, which is its own change.
|
||||
// How the FRONTEND's CPU shadow for a widened format is laid out relative to the carrier's
|
||||
// transfer, i.e. what the upload has to do to it. Almost every entry is `Components`: the
|
||||
// shadow already holds SourceChannels components of exactly the carrier's own type, so
|
||||
// padding it out to four is the whole conversion. The packed entries do not - their shadow
|
||||
// is ONE 32-bit word per texel - and reading such a word as components of the carrier's
|
||||
// type takes twelve or sixteen bytes out of four and shears the level.
|
||||
enum class ImageWidenSourceEncoding : Uint8 {
|
||||
Components = 0,
|
||||
// r11f_g11f_b10f: GL_UNSIGNED_INT_10F_11F_11F_REV -> four GL_FLOATs of an rgba16f.
|
||||
PackedFloat11f11f10f,
|
||||
// rgb10_a2 and rgb10_a2ui: GL_UNSIGNED_INT_2_10_10_10_REV -> four GL_UNSIGNED_SHORT
|
||||
// channel CODES of an rgba16ui. The same split serves both: the two formats differ
|
||||
// only in what the codes MEAN, which is the shader's business and not the transfer's.
|
||||
PackedInt2101010Rev,
|
||||
};
|
||||
|
||||
struct ImageBindableStorageWidening {
|
||||
GLenum InternalFormat = GL_UNKNOWN_MGL;
|
||||
GLenum Format = GL_UNKNOWN_MGL;
|
||||
@@ -113,17 +129,46 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// transfer type cannot tell the two apart (GL_UNSIGNED_BYTE serves both RG8 and
|
||||
// RG8UI), so the carrier decides.
|
||||
Bool IntegerData = false;
|
||||
// The frontend shadow is a PACKED word rather than SourceChannels separate components
|
||||
// of the carrier's own type, so the upload has to DECODE it instead of padding it out
|
||||
// (PrepareImageWidenedUpload). True only for r11f_g11f_b10f, whose shadow is one
|
||||
// GL_UNSIGNED_INT_10F_11F_11F_REV per texel and whose carrier is GL_RGBA16F: the
|
||||
// channel repack every other entry uses would read three floats out of a four-byte
|
||||
// texel and shear the level.
|
||||
Bool PackedFloatSource = false;
|
||||
// What the upload has to do to the frontend shadow before it describes the level to
|
||||
// the driver (PrepareImageWidenedUpload).
|
||||
ImageWidenSourceEncoding SourceEncoding = ImageWidenSourceEncoding::Components;
|
||||
// Non-zero when the carrier holds this format's channels as the INTEGER CODES of a
|
||||
// NORMALIZED value - the seven 16-bit and 10-bit normalized formats, which core ESSL
|
||||
// has no image format of any width for and which a float carrier would requantise.
|
||||
// Each entry is the largest code that channel can hold, i.e. the denominator of GL 4.6
|
||||
// 2.3.5; SignedNormalized picks which of the two conversions it is the denominator of.
|
||||
//
|
||||
// Two things depend on it, both because the ES storage no longer shares the frontend
|
||||
// format's component class: the upload pads a missing alpha with ChannelMax[3] instead
|
||||
// of the transfer type's own "one" (through a uint carrier the saturated field IS the
|
||||
// one), and glGetTexImage divides the codes back out into the floats the application
|
||||
// is still owed.
|
||||
Uint ChannelMax[4] = {0u, 0u, 0u, 0u};
|
||||
Bool SignedNormalized = false;
|
||||
|
||||
Bool CarriesNormalizedCodes() const { return ChannelMax[0] != 0u; }
|
||||
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
|
||||
};
|
||||
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat);
|
||||
|
||||
// The single-channel core format an image-bindable BUFFER texture's view is SPLIT into, or
|
||||
// GL_UNKNOWN_MGL for a format that needs no split (or has no core base).
|
||||
//
|
||||
// A buffer texture cannot be widened: its texels are the application's buffer object, at
|
||||
// the size and layout the application gave it, and it is usually also a vertex, index or
|
||||
// storage buffer whose bytes are not ours to restride. But an rg32f view of N texels and
|
||||
// an r32f view of 2N texels describe exactly the SAME bytes, so the split changes only
|
||||
// how the shader subscripts them - component j of texel i is texel 2i + j of the base
|
||||
// view - which WidenImageFormatsPass rewrites every access to do. The same rule as the
|
||||
// widening decides WHETHER: a driver that can spell rg32f for an imageBuffer needs
|
||||
// nothing.
|
||||
//
|
||||
// KNOWN GAP, and the reason this is not applied to a texture that is merely sampled: a
|
||||
// buffer texture that is BOTH image-bound and read through a samplerBuffer would have its
|
||||
// sampled view split too, and the sampler side is not rewritten. Accepted for the same
|
||||
// reason the storage widening's gaps are - on a driver where the split applies at all
|
||||
// there is no legal ESSL for the image declaration, so such a program did not compile.
|
||||
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat);
|
||||
} // namespace TextureImpl
|
||||
|
||||
namespace FramebufferImpl {} // namespace FramebufferImpl
|
||||
@@ -399,8 +444,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// reading back zero. Mali and Mesa link the same text, so nothing but a device gate
|
||||
// catches this.
|
||||
//
|
||||
// The declarations this pass leaves untouched keep their names, and those are exactly the
|
||||
// ones that already agree across stages.
|
||||
// A declaration SPIRV-Cross already tagged `readonly` or `writeonly` needs no qualifier
|
||||
// repair, but it is NOT stage-independent: that tag is derived from the accesses of the
|
||||
// stage being emitted, so an image stored in the vertex stage and loaded in the fragment
|
||||
// stage arrives here as `coherent writeonly g_image` and `coherent readonly g_image` -
|
||||
// one name, two spellings, which is exactly the pair Adreno merges. Those declarations
|
||||
// are therefore renamed too, keyed on the qualifier they already carry (readonly ->
|
||||
// IMAGE_READONLY_ALIAS_PREFIX, writeonly -> IMAGE_WRITEONLY_ALIAS_PREFIX) and with
|
||||
// nothing but the identifier changed. Stages that agree still reach the same alias and
|
||||
// stay merged, so this costs no shader an extra image uniform.
|
||||
//
|
||||
// The declarations this pass still leaves untouched keep their names: one carrying BOTH
|
||||
// readonly and writeonly (a spelling no access analysis produces, so it came from the
|
||||
// application and is identical everywhere), and one carrying NEITHER, which is legal only
|
||||
// for the r32f/r32i/r32ui formats and is likewise spelled the same in every stage.
|
||||
//
|
||||
// The `coherent` on both halves of the pair is load-bearing, not decoration: GLSL only
|
||||
// guarantees a write through one image variable is visible to a read through a DIFFERENT
|
||||
|
||||
@@ -562,12 +562,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
||||
}
|
||||
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a
|
||||
// shader compiles and runs already - it is narrowed to 32 bits before the module
|
||||
// reaches this backend - so an application that simply uses doubles needs nothing
|
||||
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
|
||||
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
|
||||
// make an application that checks the string take a path MobileGL cannot honour.
|
||||
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64), and stays opt-in even on a
|
||||
// device that HAS shaderFloat64. Every `double` in a shader compiles and runs either way
|
||||
// - narrowed to 32 bits where the device has no 64-bit floats, kept whole where it does -
|
||||
// so an application that simply uses doubles needs nothing advertised. What the extension
|
||||
// additionally promises is the whole GL_ARB_gpu_shader_fp64 SURFACE (glUniform*d
|
||||
// conformance, the fp64 built-ins, the state queries), and turning the string on is a
|
||||
// decision about all of it rather than about the shader path alone.
|
||||
if (MG_Config::Features.AdvertiseFp64) {
|
||||
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
|
||||
}
|
||||
@@ -965,26 +966,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||
}
|
||||
}
|
||||
// Never, on any device, and no longer for the reason it used to be. It used to track
|
||||
// shaderFloat64 because a `dvec3` input needed the Float64 capability to exist in the
|
||||
// module at all; a 64-bit vertex FETCH was already impossible (VK_FORMAT_R64*_SFLOAT is
|
||||
// optional and lavapipe reports zero bufferFeatures for all four), so the attribute
|
||||
// arrived as its 32-bit word pair and PackDoubleVertexInputsPass bitcast it back.
|
||||
// The device feature the whole fp64 story hangs off. With it, a module keeps its
|
||||
// OpCapability Float64 and real doubles reach the driver; without it the transpile
|
||||
// narrows every 64-bit float to 32 (ShaderTranspiler::DemoteFloat64Pass), because
|
||||
// VUID-VkShaderModuleCreateInfo-pCode-08740 forbids the capability outright and no
|
||||
// pipeline could be built from such a module. lavapipe reports it; Adreno and Mali both
|
||||
// report VK_FALSE, so on every real mobile device this is false and the demotion runs
|
||||
// exactly as it always has.
|
||||
m_dynamicParameters.SupportsShaderFloat64 = m_vulkanCaps.SupportsShaderFloat64;
|
||||
// Never, on any device, and DELIBERATELY NOT COUPLED to the line above even though it
|
||||
// once tracked the same feature. It used to, because a `dvec` input needed Float64 to
|
||||
// exist in the module at all; a 64-bit vertex FETCH was already impossible
|
||||
// (VK_FORMAT_R64*_SFLOAT is optional and lavapipe reports zero bufferFeatures for all
|
||||
// four), so the attribute arrived as its 32-bit word pair and PackDoubleVertexInputsPass
|
||||
// bitcast it back.
|
||||
//
|
||||
// The shader half of that is gone: every 64-bit float is narrowed before any module
|
||||
// reaches a backend (ShaderTranspiler::DemoteFloat64Pass), so there is no `double` input
|
||||
// left to bitcast INTO, and feeding a UINT-formatted attribute to what is now a `float`
|
||||
// input would be silent garbage. Reconstructing the value would mean decoding the
|
||||
// IEEE-754 double bit pattern in the shader - software fp64, which is precisely what the
|
||||
// demotion exists to avoid - and on Espryt it would additionally need the ES driver to
|
||||
// fetch 2N uint components where the application declared N doubles, which a dvec3 or
|
||||
// dvec4 cannot even express within one attribute location.
|
||||
// Re-coupling it does not work, and the reason is worth recording because it is not
|
||||
// obvious: this flag decides the VkFormat from the VAO ATTRIBUTE alone, and the attribute
|
||||
// does not know what the shader declared. glVertexAttribFormat(GL_DOUBLE) against a plain
|
||||
// `in vec4` is not only legal but the common case
|
||||
// (KHR-GL43.vertex_attrib_binding.basic-input-case4 does exactly that, and case5 adds
|
||||
// normalized=GL_TRUE), and advanced-bindingUpdate feeds a dvec3 the same way - GL defines
|
||||
// all of them as "doubles in memory, converted to float". Turning the flag on turns the
|
||||
// narrowing OFF for every one of them and the attributes come back unfetched.
|
||||
//
|
||||
// So glVertexAttribLFormat / glVertexAttribLPointer are declined here exactly as they
|
||||
// already were on Espryt and on every real mobile device (Adreno and Mali both report
|
||||
// shaderFloat64 == VK_FALSE), and for the same visible reason. A `dvec3` INPUT still
|
||||
// compiles and draws - it is a `vec3` after demotion - as long as the application feeds
|
||||
// it with glVertexAttribPointer(GL_FLOAT) rather than 64-bit data.
|
||||
// What keeps the two halves honest instead is a per-MODULE decision: a vertex module that
|
||||
// declares a 64-bit float INPUT is demoted whole, even where the backend has native fp64,
|
||||
// so `dvec` inputs are `vec` inputs on this backend exactly as they always were. See
|
||||
// ShaderCompiler::SanitizeAndOptimizeBinary.
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||
|
||||
@@ -112,10 +112,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
||||
VertexStreamConversion conversion = VertexStreamConversion::None;
|
||||
// Gated on the SAME flag ToVkVertexFormat gates its 64-bit path on, and that is
|
||||
// load-bearing rather than belt-and-braces: the narrowing is only correct because
|
||||
// DemoteFloat64Pass already turned the shader's `dvec` input into a `vec`, and that
|
||||
// pass runs precisely when the backend declares no 64-bit vertex support. With the
|
||||
// flag set, a dvec3/dvec4 is declined by ToVkVertexFormat AND left 64-bit in the
|
||||
// load-bearing rather than belt-and-braces: the narrowing is only correct because the
|
||||
// shader's `dvec` input is a `vec` by the time the pipeline is built, and what
|
||||
// guarantees that is the flag being clear. It is clear on every backend today, and a
|
||||
// program with a 64-bit float vertex input is demoted WHOLE for the same reason even
|
||||
// where the device has native fp64 (ProgramSpirvTask::GenerateSpirv). With the flag
|
||||
// set, a dvec3/dvec4 would be declined by ToVkVertexFormat AND left 64-bit in the
|
||||
// module, so a float32 stream would be fed to a Float64 input.
|
||||
const Bool narrowFloat64Arrays =
|
||||
MG_Backend::pActiveBackendObject == nullptr ||
|
||||
|
||||
@@ -972,10 +972,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
// The fetch half of the fp64 demotion the shader side already does unconditionally
|
||||
// (DemoteFloat64Pass): the source bytes are ordinary IEEE-754 doubles, so a GL_DOUBLE array is
|
||||
// deinterleaved into a tightly packed float32 stream rather than dropped. `normalized` is not
|
||||
// consulted - GL ignores it for floating-point array types.
|
||||
// The fetch half of the 64-bit vertex narrowing, whose shader half is guaranteed by
|
||||
// SupportsFloat64VertexAttributes staying false on this backend: any program with a Float64
|
||||
// vertex INPUT is demoted whole, native fp64 or not, so the input is always a 32-bit one. The
|
||||
// source bytes are ordinary IEEE-754 doubles, so a GL_DOUBLE array is deinterleaved into a
|
||||
// tightly packed float32 stream rather than dropped. `normalized` is not consulted - GL
|
||||
// ignores it for floating-point array types.
|
||||
static Bool ConvertFloat64VertexStreamToFloat32(
|
||||
const MG_State::GLState::VertexAttribute& attribute,
|
||||
const Uint8* sourceData,
|
||||
|
||||
@@ -880,6 +880,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
// demotion makes a dmat4 a mat4 in the shader and a mat4-shaped slot here - but because it
|
||||
// is ROUTED differently: the caller's component-by-component EbtDouble branch has to widen
|
||||
// each float back to the queried type, and it undoes the same padding itself.
|
||||
// Float matrices only, in both senses: a DOUBLE matrix never comes through here, whether its
|
||||
// program was demoted (components are floats, the query is not) or kept its doubles (the
|
||||
// column stride is a dvec4's, and the caller's converting branch already walks it component
|
||||
// by component with the right one).
|
||||
Bool TryGatherFloatMatrixColumns(const TypeFactsRef ttype, const char* pBase, void* params) {
|
||||
if (!ttype.isMatrix || ttype.isDouble) return false;
|
||||
const Int columns = ttype.matrixCols;
|
||||
@@ -892,11 +896,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
// Bytes a uniform actually occupies in the global UBO. It is the tight GL type size for
|
||||
// everything except a float matrix, whose padded columns make it wider. The rule itself
|
||||
// lives on ProgramObject, because the pipeline composite's uniform refresh needs the same
|
||||
// one and two copies of a layout rule is one too many.
|
||||
SizeT UniformStorageSpanInBytes(const TypeFactsRef ttype, SizeT tightSize) {
|
||||
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize);
|
||||
// everything except a matrix, whose padded columns make it wider, and a `double` on a
|
||||
// program whose modules were demoted, where it is half. The rule itself lives on
|
||||
// ProgramObject, because the pipeline composite's uniform refresh needs the same one and
|
||||
// two copies of a layout rule is one too many.
|
||||
SizeT UniformStorageSpanInBytes(const TypeFactsRef ttype, SizeT tightSize, const Bool nativeFloat64) {
|
||||
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize, nativeFloat64);
|
||||
}
|
||||
|
||||
void GetUniform_State(GLuint program, GLint location, void* params) {
|
||||
@@ -929,7 +934,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto size = programObject->GetUniformSizesInBytes(location);
|
||||
char* pUBO = (char*)programObject->MapUBO();
|
||||
const auto& ttype = programObject->GetUniformTypeFacts(location);
|
||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||
const Bool nativeFloat64 = programObject->UsesNativeFloat64();
|
||||
const SizeT span = UniformStorageSpanInBytes(ttype, size, nativeFloat64);
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + span > programObject->GetUBOSize()) {
|
||||
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
@@ -939,9 +945,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) {
|
||||
// Never more than the uniform actually occupies. `size` is the GL type size,
|
||||
// which for a `double` uniform is twice its storage - every 64-bit float is
|
||||
// narrowed before the module reaches a backend, so the slot holds floats. The
|
||||
// typed entry points (glGetUniformdv and friends) go through
|
||||
// which on a DEMOTED program is twice a `double` uniform's storage - its 64-bit
|
||||
// floats were narrowed before the module reached a backend, so the slot holds
|
||||
// floats. The typed entry points (glGetUniformdv and friends) go through
|
||||
// GetUniformScalar_State, which converts component by component; this raw
|
||||
// copy has no type to convert with, so it is bounded rather than converted.
|
||||
Memcpy(params, pUBO + offset, std::min<SizeT>(size, span));
|
||||
@@ -983,7 +989,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto size = programObject->GetUniformSizesInBytes(location);
|
||||
char* pUBO = static_cast<char*>(programObject->MapUBO());
|
||||
const auto& ttype = programObject->GetUniformTypeFacts(location);
|
||||
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||
const Bool nativeFloat64 = programObject->UsesNativeFloat64();
|
||||
const SizeT span = UniformStorageSpanInBytes(ttype, size, nativeFloat64);
|
||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||
offset + span > programObject->GetUBOSize()) {
|
||||
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||
@@ -995,28 +1002,38 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
|
||||
}
|
||||
|
||||
// A double-precision uniform is the one case where the stored component type differs
|
||||
// from the DECLARED one for a non-opaque uniform: the shader's 64-bit floats are
|
||||
// narrowed to 32 bits before the module reaches a backend
|
||||
// A double-precision uniform is the one case where the stored component type can differ
|
||||
// from the DECLARED one for a non-opaque uniform: on a DEMOTED program the shader's
|
||||
// 64-bit floats were narrowed to 32 before the module reached the backend
|
||||
// (ShaderTranspiler::DemoteFloat64Pass), so what is in the global UBO is a float per
|
||||
// component, laid out exactly like the float-typed twin of this uniform - std140
|
||||
// 16-byte column stride for a matrix included. Reading it as a GLdouble would return
|
||||
// two components reinterpreted as one. Read component by component and let GL's
|
||||
// two components reinterpreted as one. A program that KEPT its doubles stores real ones
|
||||
// at the dvec4 column stride instead, so the width and the stride both move; everything
|
||||
// else about this walk is the same. Read component by component either way and let GL's
|
||||
// conversion rules (7.6: round to nearest for the integer queries) apply; the value
|
||||
// widens back to the queried type, having lost precision at the glUniform*d that
|
||||
// stored it and not here.
|
||||
// widens back to the queried type, having lost precision - where it lost any - at the
|
||||
// glUniform*d that stored it and not here.
|
||||
if (ttype.isDouble) {
|
||||
const Int columns = ttype.isMatrix ? ttype.matrixCols : 1;
|
||||
const Int rows = ttype.isMatrix ? ttype.matrixRows
|
||||
: (ttype.isVector ? ttype.vectorSize : 1);
|
||||
// std140 gives every matrix column its own 16-byte slot; a non-matrix is one
|
||||
// tightly packed run and never reaches the stride at all.
|
||||
const SizeT columnStride = 4 * sizeof(GLfloat);
|
||||
// A non-matrix is one tightly packed run and never reaches the stride at all.
|
||||
const SizeT columnStride =
|
||||
MG_State::GLState::ProgramObject::UniformMatrixColumnStride(ttype, nativeFloat64);
|
||||
const SizeT componentSize = nativeFloat64 ? sizeof(GLdouble) : sizeof(GLfloat);
|
||||
for (Int column = 0; column < columns; ++column) {
|
||||
for (Int row = 0; row < rows; ++row) {
|
||||
GLfloat component = 0.0f;
|
||||
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLfloat),
|
||||
sizeof(component));
|
||||
GLdouble component = 0.0;
|
||||
if (nativeFloat64) {
|
||||
Memcpy(&component, pUBO + offset + column * columnStride + row * componentSize,
|
||||
sizeof(GLdouble));
|
||||
} else {
|
||||
GLfloat narrow = 0.0f;
|
||||
Memcpy(&narrow, pUBO + offset + column * columnStride + row * componentSize,
|
||||
sizeof(narrow));
|
||||
component = static_cast<GLdouble>(narrow);
|
||||
}
|
||||
if constexpr (std::is_integral_v<T>) {
|
||||
// Rounded to the nearest integer and clamped into the queried type's
|
||||
// range, so a negative double read through glGetUniformuiv is 0
|
||||
@@ -1287,17 +1304,45 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// glUniform*d / glUniformMatrix*dv. Neither needs a layout of its own any more: the
|
||||
// transpile chain narrows every 64-bit float in the shader to 32 bits
|
||||
// Whether the program a uniform write is about to land in stores 64-bit floats at their
|
||||
// declared width. Answered off the PROGRAM, never off the live backend: it describes the
|
||||
// modules that were actually built for it, and a backend with native fp64 still demotes a
|
||||
// program whose vertex stage declares a Float64 input (see ProgramSpirvTask::GenerateSpirv).
|
||||
// Nullptr - no current program, or a name that is not a program - answers false and lets the
|
||||
// callee record the same error it always did.
|
||||
Bool CurrentProgramUsesNativeFloat64() {
|
||||
if (MG_State::pGLContext == nullptr) return false;
|
||||
const auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
||||
return programObject != nullptr && programObject->UsesNativeFloat64();
|
||||
}
|
||||
|
||||
Bool NamedProgramUsesNativeFloat64(GLuint program) {
|
||||
const auto& programObject = TryToGetProgramObject(program);
|
||||
return programObject != nullptr && programObject->GetLinkStatus() && programObject->UsesNativeFloat64();
|
||||
}
|
||||
|
||||
// glUniform*d / glUniformMatrix*dv. On a DEMOTED program neither needs a layout of its own:
|
||||
// the transpile chain narrowed every 64-bit float in the shader to 32
|
||||
// (ShaderTranspiler::DemoteFloat64Pass) and the global UBO is laid out by reflecting that
|
||||
// demoted module, so a double uniform's storage IS a float uniform's - same offset, same
|
||||
// 4-byte components, same std140 column padding for matrices. Narrowing here, at the one
|
||||
// place the 64-bit value enters, and then handing the bytes to the ordinary float upload
|
||||
// path is what keeps the two in step; a separate double-shaped layout here would write
|
||||
// path is what keeps the two in step; a separate double-shaped layout there would write
|
||||
// 8-byte components into 4-byte slots and silently address the wrong ones.
|
||||
//
|
||||
// The narrowing is the same static_cast the shader's own arithmetic now performs, so the
|
||||
// The narrowing is the same static_cast the demoted shader's own arithmetic performs, so the
|
||||
// value the shader reads is the value glUniform*d was given, at float precision.
|
||||
//
|
||||
// On a program that KEPT its doubles the reverse is true and for the same reason: its global
|
||||
// UBO really does hold 8-byte components, so narrowing would leave a float bit pattern in the
|
||||
// low half of a double slot - which is not a precision loss but a garbage value. The 64-bit
|
||||
// values go through unchanged then, and the upload path is width-agnostic (it is templated on
|
||||
// the component type and bounded by the uniform's own slot span).
|
||||
//
|
||||
// Note TryToGetProgramObject / GetProgramForUniform run TWICE on this path, once for the
|
||||
// width question and once inside the call below. That is a lookup and a join on an entry
|
||||
// point no shader pack uses; the alternative is duplicating both functions' whole validation
|
||||
// sequence here, which is the thing that must not drift.
|
||||
template <GLsizei ItemCount>
|
||||
void UniformvNarrowed_State(GLint location, GLsizei count, const GLdouble* value) {
|
||||
if (value == nullptr || count <= 0) {
|
||||
@@ -1306,6 +1351,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
Uniformv_State<ItemCount>(location, count, reinterpret_cast<const GLfloat*>(value));
|
||||
return;
|
||||
}
|
||||
if (location != -1 && CurrentProgramUsesNativeFloat64()) {
|
||||
Uniformv_State<ItemCount>(location, count, value);
|
||||
return;
|
||||
}
|
||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||
Uniformv_State<ItemCount>(location, count, narrowed.data());
|
||||
@@ -1317,6 +1366,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
ProgramUniformv_State<ItemCount>(program, location, count, reinterpret_cast<const GLfloat*>(value));
|
||||
return;
|
||||
}
|
||||
if (location != -1 && NamedProgramUsesNativeFloat64(program)) {
|
||||
ProgramUniformv_State<ItemCount>(program, location, count, value);
|
||||
return;
|
||||
}
|
||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||
ProgramUniformv_State<ItemCount>(program, location, count, narrowed.data());
|
||||
@@ -1368,15 +1421,63 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// glUniformMatrix*dv / glProgramUniformMatrix*dv. Narrowed to the float form and handed
|
||||
// straight to it: after DemoteFloat64Pass a `dmat4` uniform is a `mat4` in the shader and a
|
||||
// mat4-shaped slot in the global UBO, columns padded to a vec4 and all. Everything else
|
||||
// about the call - transpose handling, the array-element walk, the opaque-uniform refusal -
|
||||
// is then the one implementation both spellings share.
|
||||
// glUniformMatrix*dv / glProgramUniformMatrix*dv on a program that KEPT its doubles. Same
|
||||
// walk as UniformMatrixfv_Object down to the last branch, and deliberately a copy of it
|
||||
// rather than a template over the component type: the two differ in exactly one number that
|
||||
// is not derivable from the component type alone - std140 pads a double matrix's column out
|
||||
// to a dvec4 (32 bytes) unless the column is a dvec2, which is already 16 - and folding that
|
||||
// into the float version would put a per-call branch on the hot glUniformMatrix4fv path
|
||||
// Minecraft calls thousands of times a frame for a case no shader pack ever takes.
|
||||
template <typename Program>
|
||||
void UniformMatrixdvNative_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value, Int columns, Int rows,
|
||||
const String& ownerDescription) {
|
||||
const SizeT columnStride = rows <= 2 ? 2 * sizeof(GLdouble) : 4 * sizeof(GLdouble);
|
||||
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
||||
GLdouble column[4] = {};
|
||||
for (GLint matrix = 0; matrix < count; ++matrix) {
|
||||
if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) break;
|
||||
if (!programObject.IsValidUniformLocation(location + matrix)) {
|
||||
RecordInvalidUniformLocationError("glUniformMatrixdv", location + matrix, ownerDescription);
|
||||
return;
|
||||
}
|
||||
if (programObject.IsUniformOpaqueAtLocation(location + matrix)) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "glUniformMatrixdv",
|
||||
"Opaque uniforms cannot be set with matrix Uniform calls."));
|
||||
return;
|
||||
}
|
||||
const GLdouble* source = value + static_cast<SizeT>(matrix) * componentCount;
|
||||
for (Int c = 0; c < columns; ++c) {
|
||||
for (Int r = 0; r < rows; ++r) {
|
||||
column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r];
|
||||
}
|
||||
const SizeT byteOffset = static_cast<SizeT>(c) * columnStride;
|
||||
switch (rows) {
|
||||
case 2: Uniform_State<2>(programObject, location + matrix, column, byteOffset); break;
|
||||
case 3: Uniform_State<3>(programObject, location + matrix, column, byteOffset); break;
|
||||
default: Uniform_State<4>(programObject, location + matrix, column, byteOffset); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// glUniformMatrix*dv / glProgramUniformMatrix*dv. On a DEMOTED program this narrows to the
|
||||
// float form and hands it straight over: after DemoteFloat64Pass a `dmat4` uniform is a
|
||||
// `mat4` in the shader and a mat4-shaped slot in the global UBO, columns padded to a vec4
|
||||
// and all. Everything else about the call - transpose handling, the array-element walk, the
|
||||
// opaque-uniform refusal - is then the one implementation both spellings share. A program
|
||||
// that kept its doubles gets the same walk at double width and the wider column stride.
|
||||
template <typename Program>
|
||||
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
||||
const GLdouble* value, Int columns, Int rows) {
|
||||
if (value == nullptr || count <= 0) return;
|
||||
if (programObject.UsesNativeFloat64()) {
|
||||
UniformMatrixdvNative_Object(programObject, location, count, transpose, value, columns, rows,
|
||||
"the current program object");
|
||||
return;
|
||||
}
|
||||
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * componentCount);
|
||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||
|
||||
@@ -6,25 +6,29 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - GLSL DOUBLES, RUN AT SINGLE PRECISION.
|
||||
// Scenario - GLSL DOUBLES, AT WHATEVER PRECISION THE BACKEND CAN GIVE.
|
||||
//
|
||||
// No mobile GPU has 64-bit floats. Adreno and Mali both report shaderFloat64 == VK_FALSE, so
|
||||
// Magma cannot build a module that declares the Float64 capability, and ESSL has no fp64 type
|
||||
// at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES
|
||||
// profile") and the program never reaches the driver. MobileGL therefore narrows every 64-bit
|
||||
// float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than declining the
|
||||
// shader: `double` compiles and runs everywhere, at float precision.
|
||||
// Magma cannot build a module that declares the Float64 capability there, and ESSL has no fp64
|
||||
// type at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES
|
||||
// profile") and the program never reaches the driver. On every such backend MobileGL narrows
|
||||
// every 64-bit float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than
|
||||
// declining the shader: `double` compiles and runs everywhere, at float precision. Where the
|
||||
// backend DOES consume 64-bit floats - lavapipe is the one that does - the narrowing is skipped
|
||||
// and the doubles reach the driver whole.
|
||||
//
|
||||
// The narrowing is only half a contract. The other half is the API side: the global UBO is
|
||||
// laid out by reflecting the DEMOTED module, so glUniform*d has to store a float where the
|
||||
// shader reads a float, glGetUniform*v has to read one back, and a dmat4's columns are now
|
||||
// std140-padded like any other matrix's. Every one of those is a byte offset that fails
|
||||
// silently - the uniform simply reads as something else - so the cases below set values
|
||||
// through the API and have the SHADER report what it saw.
|
||||
// Either way it is only half a contract. The other half is the API side: the global UBO is laid
|
||||
// out by reflecting whichever module was produced, so glUniform*d has to store the width the
|
||||
// shader reads, glGetUniform*v has to read that width back, and a matrix's columns are
|
||||
// std140-padded to a vec4 or a dvec4 to match. Every one of those is a byte offset that fails
|
||||
// silently - the uniform simply reads as something else - so the cases below set values through
|
||||
// the API and have the SHADER report what it saw.
|
||||
//
|
||||
// What is deliberately NOT asserted: that the values are exact to double precision. They are
|
||||
// not, and cannot be. Every expectation here is the float value of the double that was set,
|
||||
// which is the whole point.
|
||||
// WHY ALMOST EVERY EXPECTATION HERE IS A FLOAT VALUE, and why that is not an accident of the
|
||||
// demotion: the shader reports through a `float` SSBO, and every value chosen is exact in
|
||||
// float32, so the same number is correct in both regimes and the assertions test the LAYOUT
|
||||
// rather than the precision. Exactly one case (GetUniformdvReadsBackWhatWasStored) uses a value
|
||||
// that is not - 0.1 - and it names both answers explicitly.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
@@ -574,12 +578,24 @@ void main() {
|
||||
glUseProgram(0);
|
||||
|
||||
// The readback has to undo exactly what the write did - the same std140 column
|
||||
// padding, the same 4-byte components - or a dmat4 comes back with its columns
|
||||
// shifted and nothing else in the API would say so.
|
||||
// padding, the same component width - or a dmat4 comes back with its columns
|
||||
// shifted and nothing else in the API would say so. Every value below except the
|
||||
// scalar is exact in float32, so those expectations pin the LAYOUT and hold in
|
||||
// either regime; the scalar is the one that also pins the PRECISION.
|
||||
GLdouble readScalar = 0.0;
|
||||
glGetUniformdv(m_program, scalar, &readScalar);
|
||||
EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1)))
|
||||
<< "the value is what a float can hold, not the double that was passed in";
|
||||
// 0.1 is not representable in float32, so what comes back names the regime: a
|
||||
// backend without native fp64 narrowed it at the glUniform1d above (the module's own
|
||||
// doubles were demoted, so its storage is 4 bytes per component), and one with it
|
||||
// stored the double whole. Both are correct; asserting only the narrow answer would
|
||||
// fail the moment fp64 stops being emulated, and asserting only the wide one would
|
||||
// fail on every mobile device there is.
|
||||
if (readScalar == 0.1) {
|
||||
SUCCEED() << "this backend consumes 64-bit floats natively; the double survived whole";
|
||||
} else {
|
||||
EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1)))
|
||||
<< "the value is what a float can hold, not the double that was passed in";
|
||||
}
|
||||
|
||||
GLdouble readVector[3] = {};
|
||||
glGetUniformdv(m_program, vector, readVector);
|
||||
@@ -593,7 +609,8 @@ void main() {
|
||||
EXPECT_DOUBLE_EQ(readMatrix[i], 100.0 + i) << "dmat4 component " << i;
|
||||
}
|
||||
|
||||
// The float query sees the same storage through the type it is actually stored as.
|
||||
// The float query sees the same storage through a narrower type, and answers the
|
||||
// same float either way: GL 4.6 core 7.6 converts on the way out.
|
||||
GLfloat readFloat = 0.0f;
|
||||
glGetUniformfv(m_program, scalar, &readFloat);
|
||||
EXPECT_FLOAT_EQ(readFloat, static_cast<float>(0.1));
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
// store, or masked it with the wrong constants, or widened the storage without widening the bind,
|
||||
// fails these on the device while the software lanes stay green.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -176,6 +178,62 @@ namespace MGITest {
|
||||
return texels;
|
||||
}
|
||||
|
||||
// A GL_TEXTURE_CUBE_MAP_ARRAY of `cubeCount` cubes, i.e. 6 * cubeCount layer-faces
|
||||
// addressed as array layers. The target the allTargets walkers reach last and the one
|
||||
// that has caught the most emulation bugs, because it is the only one whose ES
|
||||
// equivalent is a 2D array with a different addressing rule from the GL name.
|
||||
GLuint MakeCubeArrayTexture(GLenum internalFormat, GLenum uploadFormat, GLenum uploadType,
|
||||
const void* seed, int cubeCount) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
|
||||
glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 1, internalFormat, kExtent, kExtent,
|
||||
6 * cubeCount);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "allocating cube-array storage errored with " << GLErrorName(error);
|
||||
return 0;
|
||||
}
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
if (seed != nullptr) {
|
||||
glTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, 0, kExtent, kExtent,
|
||||
6 * cubeCount, uploadFormat, uploadType, seed);
|
||||
}
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
void BindLayeredImage(GLuint unit, GLuint texture, GLenum internalFormat, GLenum access) {
|
||||
glBindImageTexture(unit, texture, 0, GL_TRUE, 0, access, internalFormat);
|
||||
ASSERT_EQ(FirstGLError(), 0u)
|
||||
<< "glBindImageTexture refused layered format " << std::hex << internalFormat;
|
||||
}
|
||||
|
||||
// Sets `name` from `values`, which must hold 4 * count floats.
|
||||
void SetVec4Array(GLuint program, const char* name, const std::vector<float>& values,
|
||||
int count) {
|
||||
glUseProgram(program);
|
||||
const GLint location = glGetUniformLocation(program, name);
|
||||
ASSERT_GE(location, 0) << "the uniform array '" << name << "' was not reflected";
|
||||
glUniform4fv(location, count, values.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "setting '" << name << "' errored";
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
std::vector<float> ReadFloatsFrom(GLenum target, GLuint texture, GLenum format,
|
||||
int componentsPerTexel, int texelCount) {
|
||||
std::vector<float> texels(static_cast<std::size_t>(texelCount) * componentsPerTexel,
|
||||
-12345.0f);
|
||||
glBindTexture(target, texture);
|
||||
glGetTexImage(target, 0, format, GL_FLOAT, texels.data());
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << "reading the image back errored with " << GLErrorName(error);
|
||||
}
|
||||
return texels;
|
||||
}
|
||||
|
||||
std::vector<GLuint> ReadUints(GLuint texture, GLenum format, int componentsPerTexel) {
|
||||
std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * componentsPerTexel,
|
||||
0xFFFFFFFFu);
|
||||
@@ -351,6 +409,93 @@ void main()
|
||||
}
|
||||
}
|
||||
|
||||
// GL_RGB10_A2UI, the format all four allFormats walkers stop at once r11f_g11f_b10f is
|
||||
// carried - and the only widening whose carrier has as MANY channels as the original, so
|
||||
// GL leaves nothing to pin and neither access is rewritten. What it does need is the other
|
||||
// packed transfer: its shadow is one GL_UNSIGNED_INT_2_10_10_10_REV word per texel, which
|
||||
// the GL_RGBA16UI carrier is uploaded as four shorts.
|
||||
//
|
||||
// The seed is checked through an imageLoad BEFORE anything is stored, for the reason the
|
||||
// r11f case is: a sheared split still produces plausible integers, and a store would
|
||||
// overwrite every texel the upload got wrong. Every channel of every texel is distinct,
|
||||
// and the alpha values walk the whole 0..3 a two-bit channel has - a widening that pinned
|
||||
// alpha to GL's "1" the way a three-channel one must would pass for texel 1 alone.
|
||||
TEST_F(NonCoreImageFormatScenario, PackedIntegerImageSplitsItsUploadAndKeepsAllFourChannels) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
constexpr int kTexels = kExtent * kExtent;
|
||||
std::vector<GLuint> seed(static_cast<std::size_t>(kTexels), 0u);
|
||||
std::vector<GLuint> expected(static_cast<std::size_t>(kTexels) * 4u, 0u);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
const GLuint r = static_cast<GLuint>(texel) * 7u; // 0 .. 105
|
||||
const GLuint g = 1023u - static_cast<GLuint>(texel) * 11u; // 1023 .. 858
|
||||
const GLuint b = 512u + static_cast<GLuint>(texel); // 512 .. 527
|
||||
const GLuint a = static_cast<GLuint>(texel) % 4u; // the whole 0..3
|
||||
seed[texel] = r | (g << 10) | (b << 20) | (a << 30);
|
||||
expected[texel * 4 + 0] = r;
|
||||
expected[texel * 4 + 1] = g;
|
||||
expected[texel * 4 + 2] = b;
|
||||
expected[texel * 4 + 3] = a;
|
||||
}
|
||||
const std::vector<GLuint> wideSeed(static_cast<std::size_t>(kTexels) * 4u, 999u);
|
||||
const GLuint narrow =
|
||||
MakeTexture(GL_RGB10_A2UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV, seed.data());
|
||||
const GLuint wide = MakeTexture(GL_RGBA32UI, GL_RGBA_INTEGER, GL_UNSIGNED_INT, wideSeed.data());
|
||||
if (narrow == 0 || wide == 0) return;
|
||||
|
||||
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rgb10_a2ui, binding = 0) readonly uniform uimage2D narrow;
|
||||
layout (rgba32ui, binding = 1) writeonly uniform uimage2D wide;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(wide, coord, imageLoad(narrow, coord));
|
||||
}
|
||||
)");
|
||||
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rgb10_a2ui, binding = 0) writeonly uniform uimage2D narrow;
|
||||
|
||||
void main()
|
||||
{
|
||||
imageStore(narrow, ivec2(gl_GlobalInvocationID.xy), uvec4(11u, 22u, 33u, 2u));
|
||||
}
|
||||
)");
|
||||
if (loadProgram == 0 || storeProgram == 0) return;
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGB10_A2UI, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32UI, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
const std::vector<GLuint> loaded = ReadUints(wide, GL_RGBA_INTEGER, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(loaded[texel * 4 + 0], expected[texel * 4 + 0]) << "texel " << texel << " red";
|
||||
EXPECT_EQ(loaded[texel * 4 + 1], expected[texel * 4 + 1]) << "texel " << texel << " green";
|
||||
EXPECT_EQ(loaded[texel * 4 + 2], expected[texel * 4 + 2]) << "texel " << texel << " blue";
|
||||
EXPECT_EQ(loaded[texel * 4 + 3], expected[texel * 4 + 3]) << "texel " << texel << " alpha";
|
||||
}
|
||||
|
||||
// THE STORE. All four channels survive - this is the one widened format where GL drops
|
||||
// nothing, so a mask here would be a bug rather than the emulation.
|
||||
BindImage(kNarrowUnit, narrow, GL_RGB10_A2UI, GL_WRITE_ONLY);
|
||||
Dispatch(storeProgram);
|
||||
|
||||
const std::vector<GLuint> stored = ReadUints(narrow, GL_RGBA_INTEGER, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(stored[texel * 4 + 0], 11u) << "texel " << texel << " red";
|
||||
EXPECT_EQ(stored[texel * 4 + 1], 22u) << "texel " << texel << " green";
|
||||
EXPECT_EQ(stored[texel * 4 + 2], 33u) << "texel " << texel << " blue";
|
||||
EXPECT_EQ(stored[texel * 4 + 3], 2u) << "texel " << texel << " alpha";
|
||||
}
|
||||
}
|
||||
|
||||
// GL_R8UI: the only format KHR-GL43.shader_image_load_store.single-byte_data_alignment
|
||||
// declares, and one SPIRV-Cross refuses to print for ESSL at all, so before the emulation
|
||||
// no text was produced for the stage and the dispatch could not run.
|
||||
@@ -419,6 +564,652 @@ void main()
|
||||
}
|
||||
}
|
||||
|
||||
// GL_RG16, the first of the seven NORMALIZED formats and the first carrier that changes the
|
||||
// shader-visible TYPE: core ESSL has no 16-bit normalized image format of any width, and
|
||||
// no float carrier is honest either (a half has eleven mantissa bits against sixteen), so
|
||||
// the rgba16ui behind it holds the format's own CODES and every access converts.
|
||||
//
|
||||
// Both directions of GL 4.6 2.3.5 are checked, and the STORE direction is checked as exact
|
||||
// INTEGER CODES rather than as floats within a tolerance - which is the point of a code
|
||||
// carrier over a float one, and the only thing that would catch a rounding rule that was
|
||||
// merely close. The values are chosen so that the products are exact in float32: 0.25 and
|
||||
// 0.75 land off a tie, 0.5 lands exactly ON one (0.5 * 65535 = 32767.5), and the two
|
||||
// out-of-range values must be clamped before they are rounded rather than after.
|
||||
TEST_F(NonCoreImageFormatScenario, UnsignedNormalizedImageCarriesItsCodesBothWays) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
constexpr int kTexels = kExtent * kExtent;
|
||||
constexpr double kUnorm16Max = 65535.0;
|
||||
|
||||
// THE UPLOAD. Distinct per texel, and the codes are the shadow's own 16-bit words: a
|
||||
// widening that padded or sheared them still produces plausible normalized floats.
|
||||
std::vector<GLushort> seed(static_cast<std::size_t>(kTexels) * 2u, 0);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
seed[texel * 2 + 0] = static_cast<GLushort>(texel * 4001);
|
||||
seed[texel * 2 + 1] = static_cast<GLushort>(65535 - texel * 3001);
|
||||
}
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -1.0f);
|
||||
const GLuint narrow = MakeTexture(GL_RG16, GL_RG, GL_UNSIGNED_SHORT, seed.data());
|
||||
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
|
||||
if (narrow == 0 || wide == 0) return;
|
||||
|
||||
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rg16, binding = 0) readonly uniform image2D narrow;
|
||||
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(wide, coord, imageLoad(narrow, coord));
|
||||
}
|
||||
)");
|
||||
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rg16, binding = 0) writeonly uniform image2D narrow;
|
||||
uniform vec4 g_values[16];
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(narrow, coord, g_values[coord.y * 4 + coord.x]);
|
||||
}
|
||||
)");
|
||||
if (loadProgram == 0 || storeProgram == 0) return;
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RG16, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * kUnorm16Max), seed[texel * 2 + 0])
|
||||
<< "texel " << texel << " red";
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 1] * kUnorm16Max), seed[texel * 2 + 1])
|
||||
<< "texel " << texel << " green";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 2], 0.0f)
|
||||
<< "texel " << texel << ": imageLoad on a two-channel format must report 0 for blue";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f)
|
||||
<< "texel " << texel << ": imageLoad on a format without alpha must report 1";
|
||||
}
|
||||
|
||||
// THE STORE, per GL 4.6 2.3.5: c = round(clamp(f, 0, 1) * (2^b - 1)), with a tie
|
||||
// rounded away from zero.
|
||||
struct Boundary {
|
||||
float value;
|
||||
long code;
|
||||
};
|
||||
const Boundary boundaries[kTexels] = {
|
||||
{0.0f, 0}, {1.0f, 65535}, {0.5f, 32768}, {0.25f, 16384},
|
||||
{0.75f, 49151}, {-0.5f, 0}, {2.0f, 65535}, {-1.0f, 0},
|
||||
{1.0f / 131072.0f, 0}, // 0.4999923 of a code: rounds DOWN
|
||||
{3.0f / 131072.0f, 1}, // 1.4999771 of a code: rounds DOWN to 1
|
||||
{1.0f / 65535.0f, 1}, // exactly one code
|
||||
{32767.0f / 65535.0f, 32767}, {32768.0f / 65535.0f, 32768},
|
||||
// 0.125 * 65535 = 8191.875 and 0.875 * 65535 = 57343.125 - neither is a tie, and
|
||||
// both round DOWN, which is the pair that catches a conversion that scaled by 2^b
|
||||
// instead of 2^b - 1.
|
||||
{65534.0f / 65535.0f, 65534}, {0.125f, 8192}, {0.875f, 57343},
|
||||
};
|
||||
std::vector<float> values(static_cast<std::size_t>(kTexels) * 4u, 0.0f);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
values[texel * 4 + 0] = boundaries[texel].value;
|
||||
values[texel * 4 + 1] = boundaries[texel].value;
|
||||
values[texel * 4 + 2] = 0.5f; // dropped: a two-channel format has no blue
|
||||
values[texel * 4 + 3] = 0.5f; // dropped: nor an alpha
|
||||
}
|
||||
SetVec4Array(storeProgram, "g_values", values, kTexels);
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RG16, GL_WRITE_ONLY);
|
||||
Dispatch(storeProgram);
|
||||
|
||||
// Read back through the IMAGE, so what is compared is the code the store actually
|
||||
// wrote rather than anything the readback path might renormalize on its own.
|
||||
BindImage(kNarrowUnit, narrow, GL_RG16, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * kUnorm16Max), boundaries[texel].code)
|
||||
<< "texel " << texel << " stored " << boundaries[texel].value;
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 1] * kUnorm16Max), boundaries[texel].code)
|
||||
<< "texel " << texel << " green";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 2], 0.0f) << "texel " << texel << " blue";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f) << "texel " << texel << " alpha";
|
||||
}
|
||||
|
||||
// ...and glGetTexImage owes the application the NORMALIZED value, whatever the ES
|
||||
// storage holds. The whole texture is an integer one now, so this is the only place
|
||||
// the readback conversion is exercised at all.
|
||||
const std::vector<float> viaGetTexImage = ReadFloats(narrow, GL_RG, 2);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(viaGetTexImage[texel * 2 + 0] * kUnorm16Max), boundaries[texel].code)
|
||||
<< "texel " << texel << " red through glGetTexImage";
|
||||
EXPECT_EQ(std::lround(viaGetTexImage[texel * 2 + 1] * kUnorm16Max), boundaries[texel].code)
|
||||
<< "texel " << texel << " green through glGetTexImage";
|
||||
}
|
||||
}
|
||||
|
||||
// GL_RGBA16_SNORM, the signed twin. Two things differ and both are one-line mistakes: the
|
||||
// code is a two's-complement 16-bit integer stored in an UNSIGNED carrier channel, so it
|
||||
// has to be sign-extended on the way out (a zero extension reads every negative value as
|
||||
// something near +1), and the decode is max(c / 32767, -1) rather than the bare division,
|
||||
// because the code -32768 exists and GL says it means exactly -1.
|
||||
TEST_F(NonCoreImageFormatScenario, SignedNormalizedImageSignExtendsItsCodesAndClampsAtMinusOne) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
constexpr int kTexels = kExtent * kExtent;
|
||||
constexpr double kSnorm16Max = 32767.0;
|
||||
|
||||
// The seed walks the whole signed range, INCLUDING -32768, whose decode is the one
|
||||
// value the division alone gets wrong.
|
||||
const GLshort seedCodes[kTexels] = {0, 32767, -32767, -32768, 1, -1, 16384, -16384,
|
||||
12345, -12345, 32766, -32766, 255, -256, 4095, -4096};
|
||||
std::vector<GLshort> seed(static_cast<std::size_t>(kTexels) * 4u, 0);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
seed[texel * 4 + 0] = seedCodes[texel];
|
||||
seed[texel * 4 + 1] = static_cast<GLshort>(-seedCodes[texel] == -32768 ? 32767
|
||||
: -seedCodes[texel]);
|
||||
seed[texel * 4 + 2] = seedCodes[(texel + 1) % kTexels];
|
||||
seed[texel * 4 + 3] = seedCodes[(texel + 2) % kTexels];
|
||||
}
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -12.0f);
|
||||
const GLuint narrow = MakeTexture(GL_RGBA16_SNORM, GL_RGBA, GL_SHORT, seed.data());
|
||||
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
|
||||
if (narrow == 0 || wide == 0) return;
|
||||
|
||||
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rgba16_snorm, binding = 0) readonly uniform image2D narrow;
|
||||
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(wide, coord, imageLoad(narrow, coord));
|
||||
}
|
||||
)");
|
||||
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rgba16_snorm, binding = 0) writeonly uniform image2D narrow;
|
||||
uniform vec4 g_values[16];
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(narrow, coord, g_values[coord.y * 4 + coord.x]);
|
||||
}
|
||||
)");
|
||||
if (loadProgram == 0 || storeProgram == 0) return;
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGBA16_SNORM, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
const GLshort code = seed[texel * 4 + channel];
|
||||
const float expected =
|
||||
std::max(static_cast<float>(code) / static_cast<float>(kSnorm16Max), -1.0f);
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + channel], expected)
|
||||
<< "texel " << texel << " channel " << channel << " code " << code;
|
||||
}
|
||||
}
|
||||
|
||||
// THE STORE: c = round(clamp(f, -1, 1) * (2^(b-1) - 1)), ties away from zero on BOTH
|
||||
// sides - which is what makes -0.5 land on -16384 rather than on -16383.
|
||||
struct Boundary {
|
||||
float value;
|
||||
long code;
|
||||
};
|
||||
const Boundary boundaries[kTexels] = {
|
||||
{0.0f, 0}, {1.0f, 32767}, {-1.0f, -32767}, {0.5f, 16384},
|
||||
{-0.5f, -16384}, {2.0f, 32767}, {-2.0f, -32767}, {0.25f, 8192},
|
||||
{-0.25f, -8192}, {1.0f / 32767.0f, 1}, {-1.0f / 32767.0f, -1},
|
||||
// Three quarters of a code, not half: GL leaves the direction of a TIE to the
|
||||
// implementation ("if two values are equally near, the implementation may choose
|
||||
// either"), and Magma hands these formats to Vulkan unemulated, so a value exactly
|
||||
// on 0.5 of a code is the one thing the two backends are allowed to disagree
|
||||
// about. Every entry here is off a tie except the ones at 0.5 and 0.25 of the
|
||||
// RANGE, whose products (16383.5 and 8192) round the same way under either rule.
|
||||
{0.75f / 32767.0f, 1}, {-0.75f / 32767.0f, -1},
|
||||
{16383.0f / 32767.0f, 16383}, {-16383.0f / 32767.0f, -16383},
|
||||
{0.125f, 4096},
|
||||
};
|
||||
std::vector<float> values(static_cast<std::size_t>(kTexels) * 4u, 0.0f);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
values[texel * 4 + channel] = boundaries[texel].value;
|
||||
}
|
||||
}
|
||||
SetVec4Array(storeProgram, "g_values", values, kTexels);
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGBA16_SNORM, GL_WRITE_ONLY);
|
||||
Dispatch(storeProgram);
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGBA16_SNORM, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + channel] * kSnorm16Max),
|
||||
boundaries[texel].code)
|
||||
<< "texel " << texel << " channel " << channel << " stored "
|
||||
<< boundaries[texel].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GL_RGB10_A2, the one normalized format whose channels are not all the same width: three
|
||||
// of ten bits and one of two. A single denominator would be right for three quarters of
|
||||
// every texel and wildly wrong for the fourth - alpha 1.0 would come back as 3/1023.
|
||||
TEST_F(NonCoreImageFormatScenario, TenTenTenTwoImageUsesItsOwnPerChannelDenominators) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
|
||||
constexpr int kTexels = kExtent * kExtent;
|
||||
|
||||
std::vector<GLuint> seed(static_cast<std::size_t>(kTexels), 0u);
|
||||
std::vector<GLuint> seedCodes(static_cast<std::size_t>(kTexels) * 4u, 0u);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
const GLuint r = static_cast<GLuint>(texel) * 67u;
|
||||
const GLuint g = 1023u - static_cast<GLuint>(texel) * 13u;
|
||||
const GLuint b = 341u + static_cast<GLuint>(texel);
|
||||
const GLuint a = static_cast<GLuint>(texel) % 4u;
|
||||
seed[texel] = r | (g << 10) | (b << 20) | (a << 30);
|
||||
seedCodes[texel * 4 + 0] = r;
|
||||
seedCodes[texel * 4 + 1] = g;
|
||||
seedCodes[texel * 4 + 2] = b;
|
||||
seedCodes[texel * 4 + 3] = a;
|
||||
}
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -1.0f);
|
||||
const GLuint narrow =
|
||||
MakeTexture(GL_RGB10_A2, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV, seed.data());
|
||||
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
|
||||
if (narrow == 0 || wide == 0) return;
|
||||
|
||||
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rgb10_a2, binding = 0) readonly uniform image2D narrow;
|
||||
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(wide, coord, imageLoad(narrow, coord));
|
||||
}
|
||||
)");
|
||||
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rgb10_a2, binding = 0) writeonly uniform image2D narrow;
|
||||
|
||||
void main()
|
||||
{
|
||||
imageStore(narrow, ivec2(gl_GlobalInvocationID.xy), vec4(0.0, 0.5, 1.0, 1.0));
|
||||
}
|
||||
)");
|
||||
if (loadProgram == 0 || storeProgram == 0) return;
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGB10_A2, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
const auto denominator = channel == 3 ? 3.0f : 1023.0f;
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + channel],
|
||||
static_cast<float>(seedCodes[texel * 4 + channel]) / denominator)
|
||||
<< "texel " << texel << " channel " << channel;
|
||||
}
|
||||
}
|
||||
|
||||
// 0.5 through a TWO-bit channel is 1.5 of a code and rounds away from zero to 2, which
|
||||
// is 2/3 back - a value only the two-bit denominator can produce.
|
||||
BindImage(kNarrowUnit, narrow, GL_RGB10_A2, GL_WRITE_ONLY);
|
||||
Dispatch(storeProgram);
|
||||
|
||||
BindImage(kNarrowUnit, narrow, GL_RGB10_A2, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * 1023.0), 0) << "texel " << texel << " red";
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 1] * 1023.0), 512) << "texel " << texel << " green";
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 2] * 1023.0), 1023) << "texel " << texel << " blue";
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 3] * 3.0), 3) << "texel " << texel << " alpha";
|
||||
}
|
||||
}
|
||||
|
||||
// The same carrier on a GL_TEXTURE_CUBE_MAP_ARRAY, the target the allTargets walkers reach
|
||||
// last and the one whose ES equivalent is addressed differently from its GL name (six
|
||||
// layer-faces per cube, as array layers). Nothing about the format conversion changes with
|
||||
// the target - which is exactly the claim, since the storage widening, the layered bind and
|
||||
// the per-layer readback all have their own code paths for this target alone.
|
||||
TEST_F(NonCoreImageFormatScenario, NormalizedImageCarriesEveryLayerFaceOfACubeMapArray) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
GLint maxComputeImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
|
||||
constexpr int kCubes = 2;
|
||||
constexpr int kLayerFaces = 6 * kCubes;
|
||||
constexpr int kTexelsPerFace = kExtent * kExtent;
|
||||
constexpr int kTexels = kTexelsPerFace * kLayerFaces;
|
||||
constexpr double kUnorm16Max = 65535.0;
|
||||
|
||||
std::vector<GLushort> seed(static_cast<std::size_t>(kTexels), 0);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
seed[texel] = static_cast<GLushort>((texel * 5477u) & 0xFFFFu);
|
||||
}
|
||||
const GLuint narrow =
|
||||
MakeCubeArrayTexture(GL_R16, GL_RED, GL_UNSIGNED_SHORT, seed.data(), kCubes);
|
||||
if (narrow == 0) return;
|
||||
|
||||
// THE UPLOAD, read back through glGetTexImage across every layer-face. A carrier that
|
||||
// widened the storage but seeded only the first face leaves the rest at zero, which is
|
||||
// what the per-layer readback path is there to catch, and this target is the only one
|
||||
// whose readback goes layer by layer.
|
||||
const std::vector<float> uploaded =
|
||||
ReadFloatsFrom(GL_TEXTURE_CUBE_MAP_ARRAY, narrow, GL_RED, 1, kTexels);
|
||||
for (int texel = 0; texel < kTexels; ++texel) {
|
||||
EXPECT_EQ(std::lround(uploaded[texel] * kUnorm16Max), seed[texel])
|
||||
<< "texel " << texel << " of " << kTexels;
|
||||
}
|
||||
|
||||
// ...and through an imageCubeArray, which is the declaration the shader half has to
|
||||
// carry for this target: a layered bind, an ivec3 coordinate whose z is the
|
||||
// layer-face, and the same unpack as every other target.
|
||||
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexelsPerFace) * 4u, -1.0f);
|
||||
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
|
||||
if (wide == 0) return;
|
||||
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (r16, binding = 0) readonly uniform imageCubeArray narrow;
|
||||
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
|
||||
uniform int g_layerFace;
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
|
||||
imageStore(wide, coord, imageLoad(narrow, ivec3(coord, g_layerFace)));
|
||||
}
|
||||
)");
|
||||
if (loadProgram == 0) return;
|
||||
|
||||
// Two faces, one of them past the first cube, so a carrier that addressed only the
|
||||
// first six layer-faces cannot pass.
|
||||
for (const int layerFace : {1, 9}) {
|
||||
glUseProgram(loadProgram);
|
||||
const GLint location = glGetUniformLocation(loadProgram, "g_layerFace");
|
||||
ASSERT_GE(location, 0) << "g_layerFace was not reflected";
|
||||
glUniform1i(location, layerFace);
|
||||
glUseProgram(0);
|
||||
|
||||
BindLayeredImage(kNarrowUnit, narrow, GL_R16, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
Dispatch(loadProgram);
|
||||
|
||||
const std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
|
||||
for (int texel = 0; texel < kTexelsPerFace; ++texel) {
|
||||
const int sourceTexel = layerFace * kTexelsPerFace + texel;
|
||||
EXPECT_EQ(std::lround(loaded[texel * 4 + 0] * kUnorm16Max), seed[sourceTexel])
|
||||
<< "layer-face " << layerFace << " texel " << texel;
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 1], 0.0f)
|
||||
<< "layer-face " << layerFace << " texel " << texel
|
||||
<< ": imageLoad on a one-channel format must report 0 for green";
|
||||
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f)
|
||||
<< "layer-face " << layerFace << " texel " << texel
|
||||
<< ": imageLoad on a format without alpha must report 1";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A BUFFER image, which takes neither of the emulations above. Its texels are the
|
||||
// application's buffer object - at the size and layout the application gave it, and
|
||||
// usually also a vertex, index or storage buffer - so there is nothing to reallocate a
|
||||
// carrier in. What CAN be done is a SPLIT: rg32f over N texels and r32f over 2N texels
|
||||
// describe exactly the same bytes, so the view is re-declared and every subscript is
|
||||
// doubled (WidenImageFormatsPass, and the matching glTexBuffer/glBindImageTexture format
|
||||
// in TextureImpl).
|
||||
//
|
||||
// THE NUMBERS HERE ARE THE ONES THAT PINNED THE OLD BUG. Widening a buffer image instead
|
||||
// leaves the shader striding 16 bytes through 8-byte texels: measured on an Adreno 830
|
||||
// with this exact 32-byte GL_RG32F buffer and this exact shader, the readback came back
|
||||
// [1,100] [0,1] [2,100] [0,1] - texels 0 and 1 landed on top of all four, and texels 2 and
|
||||
// 3 were written past the end of the application's buffer.
|
||||
//
|
||||
// This runs on every backend, and on a driver that CAN spell rg32f for an imageBuffer
|
||||
// (Mesa's, which the software lanes use) nothing is split at all - which is the other half
|
||||
// of the claim: the arming has to agree with the shader, so a split that fired where the
|
||||
// driver needed none would double every subscript and fail here just as loudly.
|
||||
TEST_F(NonCoreImageFormatScenario, BufferImageAddressesTheApplicationsOwnTexels) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
GLint maxTextureBufferSize = 0;
|
||||
glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTextureBufferSize);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
if (maxTextureBufferSize <= 0) GTEST_SKIP() << "no buffer textures on this driver";
|
||||
|
||||
constexpr int kBufferTexels = 4;
|
||||
const std::vector<float> seed(static_cast<std::size_t>(kBufferTexels) * 2u, -1.0f);
|
||||
|
||||
GLuint buffer = 0;
|
||||
glGenBuffers(1, &buffer);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(seed.size() * sizeof(float)), seed.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, buffer);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
glDeleteBuffers(1, &buffer);
|
||||
GTEST_SKIP() << "glTexBuffer(GL_RG32F) errored with " << GLErrorName(error);
|
||||
}
|
||||
|
||||
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rg32f, binding = 0) writeonly uniform imageBuffer narrow;
|
||||
|
||||
void main()
|
||||
{
|
||||
int texel = int(gl_GlobalInvocationID.x);
|
||||
imageStore(narrow, texel, vec4(float(texel + 1), 100.0, 3.0, 4.0));
|
||||
}
|
||||
)");
|
||||
if (storeProgram == 0) {
|
||||
glDeleteBuffers(1, &buffer);
|
||||
return;
|
||||
}
|
||||
|
||||
BindImage(kNarrowUnit, texture, GL_RG32F, GL_WRITE_ONLY);
|
||||
glUseProgram(storeProgram);
|
||||
glDispatchCompute(kBufferTexels, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
|
||||
glUseProgram(0);
|
||||
|
||||
std::vector<float> readback(seed.size(), -12345.0f);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||
glGetBufferSubData(GL_TEXTURE_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(readback.size() * sizeof(float)), readback.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "reading the buffer back errored";
|
||||
|
||||
for (int texel = 0; texel < kBufferTexels; ++texel) {
|
||||
EXPECT_FLOAT_EQ(readback[texel * 2 + 0], static_cast<float>(texel + 1))
|
||||
<< "texel " << texel << " red";
|
||||
EXPECT_FLOAT_EQ(readback[texel * 2 + 1], 100.0f) << "texel " << texel << " green";
|
||||
}
|
||||
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
||||
glDeleteBuffers(1, &buffer);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
// The SAME buffer texture read through BOTH doors at once, which is the shape the split
|
||||
// originally broke. A buffer texture that is image-bound is split - the view is re-declared
|
||||
// one component at a time and every image subscript is doubled to match - but the sampler
|
||||
// side is NOT subscript-rewritten, so re-describing the APPLICATION's own texture made
|
||||
// texelFetch(s, i) return component 2i of the base view instead of texel i's whole pair.
|
||||
// The split therefore goes on a private second name over the same buffer
|
||||
// (BackendTextureObject::m_bufferImageSplitViewId) and the application's name keeps the
|
||||
// format it asked for: rg32f is a legal SAMPLED buffer-texture format in ES 3.2, it is only
|
||||
// the IMAGE binding ES cannot spell.
|
||||
//
|
||||
// This is KHR-GL42/43.shader_image_load_store.advanced-sync-imageAccess reduced to one
|
||||
// dispatch. That case image-stores into a GL_RG32F buffer texture and then, in one shader,
|
||||
// reads the same texture through an imageBuffer AND a samplerBuffer and compares the two -
|
||||
// so it went red on every pixel while its sibling -vertexArray, which never samples the
|
||||
// buffer texture, passed.
|
||||
//
|
||||
// Like the case above this runs on every backend, and on a driver that can spell rg32f for
|
||||
// an imageBuffer nothing is split at all - both doors then trivially agree, which is the
|
||||
// other half of the claim: a split that fired where the driver needed none would show up
|
||||
// here as the two disagreeing.
|
||||
TEST_F(NonCoreImageFormatScenario, ASplitBufferImageStillSamplesWholeTexels) {
|
||||
if (!Ready()) GTEST_SKIP() << "no GL context";
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
||||
GLint maxTextureBufferSize = 0;
|
||||
glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTextureBufferSize);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
if (maxTextureBufferSize <= 0) GTEST_SKIP() << "no buffer textures on this driver";
|
||||
|
||||
constexpr int kBufferTexels = 4;
|
||||
// Both components of every texel distinct and non-zero, so a sampler that reads the
|
||||
// SPLIT view cannot accidentally agree: texel i would come back as (2i-th component,
|
||||
// 0, 0, 1) rather than (x, y, 0, 1), and every one of those is a value no texel holds.
|
||||
std::vector<float> seed(static_cast<std::size_t>(kBufferTexels) * 2u, 0.0f);
|
||||
for (int texel = 0; texel < kBufferTexels; ++texel) {
|
||||
seed[static_cast<std::size_t>(texel) * 2u + 0u] = static_cast<float>(texel * 10 + 1);
|
||||
seed[static_cast<std::size_t>(texel) * 2u + 1u] = static_cast<float>(texel * 10 + 2);
|
||||
}
|
||||
|
||||
GLuint buffer = 0;
|
||||
glGenBuffers(1, &buffer);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(seed.size() * sizeof(float)), seed.data(),
|
||||
GL_DYNAMIC_DRAW);
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, buffer);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
glDeleteBuffers(1, &buffer);
|
||||
GTEST_SKIP() << "glTexBuffer(GL_RG32F) errored with " << GLErrorName(error);
|
||||
}
|
||||
|
||||
// The answer buffer is rgba32f, which IS core ESSL, so it is never split and cannot
|
||||
// hide a mistake in the thing under test.
|
||||
constexpr int kAnswers = kBufferTexels * 2;
|
||||
const std::vector<float> answerSeed(static_cast<std::size_t>(kAnswers) * 4u, -12345.0f);
|
||||
GLuint answerBuffer = 0;
|
||||
glGenBuffers(1, &answerBuffer);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, answerBuffer);
|
||||
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(answerSeed.size() * sizeof(float)),
|
||||
answerSeed.data(), GL_DYNAMIC_DRAW);
|
||||
GLuint answerTexture = 0;
|
||||
glGenTextures(1, &answerTexture);
|
||||
m_textures.push_back(answerTexture);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, answerTexture);
|
||||
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, answerBuffer);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
glDeleteBuffers(1, &buffer);
|
||||
glDeleteBuffers(1, &answerBuffer);
|
||||
GTEST_SKIP() << "glTexBuffer(GL_RGBA32F) errored with " << GLErrorName(error);
|
||||
}
|
||||
|
||||
const GLuint program = MakeComputeProgram(R"(#version 430 core
|
||||
|
||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
||||
|
||||
layout (rg32f, binding = 0) readonly uniform imageBuffer narrow;
|
||||
layout (rgba32f, binding = 1) writeonly uniform imageBuffer answers;
|
||||
uniform samplerBuffer sampled;
|
||||
|
||||
void main()
|
||||
{
|
||||
int texel = int(gl_GlobalInvocationID.x);
|
||||
imageStore(answers, texel * 2 + 0, imageLoad(narrow, texel));
|
||||
imageStore(answers, texel * 2 + 1, texelFetch(sampled, texel));
|
||||
}
|
||||
)");
|
||||
if (program == 0) {
|
||||
glDeleteBuffers(1, &buffer);
|
||||
glDeleteBuffers(1, &answerBuffer);
|
||||
return;
|
||||
}
|
||||
|
||||
BindImage(kNarrowUnit, texture, GL_RG32F, GL_READ_ONLY);
|
||||
BindImage(kWideUnit, answerTexture, GL_RGBA32F, GL_WRITE_ONLY);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
glUseProgram(program);
|
||||
glUniform1i(glGetUniformLocation(program, "sampled"), 0);
|
||||
glDispatchCompute(kBufferTexels, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
|
||||
glUseProgram(0);
|
||||
|
||||
std::vector<float> readback(answerSeed.size(), -54321.0f);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, answerBuffer);
|
||||
glGetBufferSubData(GL_TEXTURE_BUFFER, 0,
|
||||
static_cast<GLsizeiptr>(readback.size() * sizeof(float)), readback.data());
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "reading the answers back errored";
|
||||
|
||||
for (int texel = 0; texel < kBufferTexels; ++texel) {
|
||||
const float red = static_cast<float>(texel * 10 + 1);
|
||||
const float green = static_cast<float>(texel * 10 + 2);
|
||||
const std::size_t viaImage = static_cast<std::size_t>(texel) * 8u;
|
||||
const std::size_t viaSampler = viaImage + 4u;
|
||||
EXPECT_FLOAT_EQ(readback[viaImage + 0u], red) << "texel " << texel << " imageLoad red";
|
||||
EXPECT_FLOAT_EQ(readback[viaImage + 1u], green) << "texel " << texel << " imageLoad green";
|
||||
EXPECT_FLOAT_EQ(readback[viaSampler + 0u], red) << "texel " << texel << " texelFetch red";
|
||||
EXPECT_FLOAT_EQ(readback[viaSampler + 1u], green)
|
||||
<< "texel " << texel
|
||||
<< " texelFetch green: a samplerBuffer must see whole texels even where the "
|
||||
"image side of the same texture was split";
|
||||
EXPECT_FLOAT_EQ(readback[viaSampler + 2u], 0.0f) << "texel " << texel << " texelFetch blue";
|
||||
EXPECT_FLOAT_EQ(readback[viaSampler + 3u], 1.0f) << "texel " << texel << " texelFetch alpha";
|
||||
}
|
||||
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
||||
glDeleteBuffers(1, &buffer);
|
||||
glDeleteBuffers(1, &answerBuffer);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
// The other consumer of the same texture. A widened texture's ES storage really does have
|
||||
// four channels, so a sampler reading it raw would see whatever the carrier holds; the
|
||||
// logical format's missing channels have to keep reading 0 and 1 (which Espryt arranges
|
||||
|
||||
@@ -787,9 +787,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// The L1 key. Every input below is one that can change the SPIR-V this program
|
||||
// generates; see the key inventory on SpirvTranslationKeyInputs.
|
||||
//
|
||||
// Deliberately NOT keyed on: nothing that only steers a BACKEND transpile - see the
|
||||
// Deliberately NOT keyed on: anything that only steers a BACKEND transpile - see the
|
||||
// classification on CompileEnv::frontendFingerprint, and L2's own key in
|
||||
// MG_Util/ShaderTranspiler/TranslationCache.h.
|
||||
// MG_Util/ShaderTranspiler/TranslationCache.h. The single capability bit that IS here
|
||||
// (nativeFloat64) earns its place by changing SanitizeAndOptimizeBinary's own output,
|
||||
// which is what the payload stores.
|
||||
MG_Util::ShaderTranspiler::TranslationCacheKey ProgramLinkTask::BuildSpirvCacheKey(
|
||||
const MG_Util::ShaderTranspiler::CompileEnv& env) const {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
@@ -805,6 +807,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// value cannot alias a module parsed without it.
|
||||
keyInputs.shaderCompileFlags = 0;
|
||||
keyInputs.enableSpirvValidation = in.enableSpirvValidation;
|
||||
// The one BACKEND capability bit in this key, and it has to be here: it reaches inside
|
||||
// SanitizeAndOptimizeBinary, whose output is what the payload holds. Read from the same
|
||||
// env snapshot ProgramSpirvTask hands the chain, so the key and the bytes can never
|
||||
// disagree.
|
||||
keyInputs.nativeFloat64 = env.ConsumesFloat64Natively();
|
||||
keyInputs.stages.reserve(in.shaders.size());
|
||||
for (const LinkShaderInput& shader : in.shaders) {
|
||||
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
|
||||
|
||||
@@ -155,6 +155,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
Uint8* const scratch = m_spirv.globalUboScratch.data();
|
||||
const SizeT uboSize = m_spirv.globalUboScratch.size();
|
||||
// Read straight off m_spirv, not through UsesNativeFloat64(): this runs INSIDE the
|
||||
// phase-B publish, where the join gate is not re-entrant. Same reason the scratch above
|
||||
// is taken directly.
|
||||
const Bool nativeFloat64 = m_spirv.nativeFloat64;
|
||||
|
||||
for (const auto& init : initializers) {
|
||||
// Scalars per array ELEMENT. A matrix element carries cols * rows of them, laid
|
||||
@@ -165,12 +169,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
const Int elements = init.arraySize;
|
||||
if (componentsPerElement <= 0 || elements <= 0) continue;
|
||||
|
||||
// EbtDouble belongs with the floats now, not with the skipped types: every 64-bit
|
||||
// float in a shader is narrowed to 32 bits before the module reaches a backend
|
||||
// EbtDouble belongs with the floats, not with the skipped types. On a DEMOTED
|
||||
// program its 64-bit floats were narrowed to 32 before the module reached a backend
|
||||
// (ShaderTranspiler::DemoteFloat64Pass), so a `uniform double d = 1.5;` has exactly
|
||||
// the 32-bit shadow encoding a `uniform float` does - and glslang already folded its
|
||||
// value into floatValues, which is a vector<double> either way. Leaving it out meant
|
||||
// the initializer was silently dropped and the uniform came up zero.
|
||||
// the 32-bit shadow encoding a `uniform float` does; on a program that kept them it
|
||||
// has an 8-byte one, which the store width below picks up. glslang folded the value
|
||||
// into floatValues, a vector<double>, in both cases. Leaving it out meant the
|
||||
// initializer was silently dropped and the uniform came up zero.
|
||||
const Bool isFloat = init.basicType == glslang::EbtFloat ||
|
||||
init.basicType == glslang::EbtFloat16 ||
|
||||
init.basicType == glslang::EbtDouble;
|
||||
@@ -195,22 +200,36 @@ namespace MobileGL::MG_State::GLState {
|
||||
// std140 pads every column of a float matrix out to a vec4, so the columns of
|
||||
// a mat3 are 16 bytes apart even though each carries 12. The slot's own span
|
||||
// states the stride the rest of the pipeline agreed on rather than guessing it.
|
||||
const SizeT slotSpan = GetUniformStorageSpanInBytes(static_cast<Uint>(location));
|
||||
// The static form, with the width taken from m_spirv directly: the member
|
||||
// overload asks UsesNativeFloat64(), which joins phase B - and phase B is what
|
||||
// is publishing right now.
|
||||
const SizeT slotSpan =
|
||||
UniformStorageSpanInBytes(GetUniformTypeFacts(static_cast<Uint>(location)),
|
||||
GetUniformSizesInBytes(static_cast<Uint>(location)), nativeFloat64);
|
||||
const SizeT columnStride =
|
||||
columns > 0 ? slotSpan / static_cast<SizeT>(columns) : slotSpan;
|
||||
const Int componentsPerColumn = columns > 0 ? rows : componentsPerElement;
|
||||
const Int columnCount = columns > 0 ? columns : 1;
|
||||
|
||||
// A `double` initializer on a program that KEPT its doubles lands in an 8-byte
|
||||
// component, not a 4-byte one; every other basic type - and every double on a
|
||||
// demoted program - stays one 32-bit word. glslang folded the value into
|
||||
// floatValues (a vector<double>) either way, so only the store width moves.
|
||||
const Bool isWideDouble = init.basicType == glslang::EbtDouble && nativeFloat64;
|
||||
const SizeT componentSize = isWideDouble ? sizeof(Double) : sizeof(Uint32);
|
||||
for (Int column = 0; column < columnCount; ++column) {
|
||||
const SizeT byteOffset = static_cast<SizeT>(offset) + static_cast<SizeT>(column) * columnStride;
|
||||
const SizeT writeSize = static_cast<SizeT>(componentsPerColumn) * sizeof(Uint32);
|
||||
const SizeT writeSize = static_cast<SizeT>(componentsPerColumn) * componentSize;
|
||||
if (byteOffset + writeSize > uboSize) break;
|
||||
const SizeT firstComponent = static_cast<SizeT>(element) * componentsPerElement +
|
||||
static_cast<SizeT>(column) * componentsPerColumn;
|
||||
for (Int component = 0; component < componentsPerColumn; ++component) {
|
||||
const SizeT source = firstComponent + static_cast<SizeT>(component);
|
||||
Uint8* const destination = scratch + byteOffset + component * sizeof(Uint32);
|
||||
if (isFloat) {
|
||||
Uint8* const destination = scratch + byteOffset + component * componentSize;
|
||||
if (isWideDouble) {
|
||||
const Double value = init.floatValues[source];
|
||||
std::memcpy(destination, &value, sizeof(value));
|
||||
} else if (isFloat) {
|
||||
const Float value = static_cast<Float>(init.floatValues[source]);
|
||||
std::memcpy(destination, &value, sizeof(value));
|
||||
} else {
|
||||
|
||||
@@ -565,26 +565,47 @@ namespace MobileGL::MG_State::GLState {
|
||||
: kInvalidUniformOffset;
|
||||
}
|
||||
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
|
||||
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size,
|
||||
// for two reasons. std140 pads each column of a matrix out to a vec4, so a mat3 spans
|
||||
// 48 bytes even though only 36 of them carry components. And every 64-bit float in a
|
||||
// shader is narrowed to 32 bits before the module reaches a backend
|
||||
// (ShaderTranspiler::DemoteFloat64Pass) - the global UBO is laid out by reflecting that
|
||||
// demoted module - so a `double` uniform occupies exactly what its float-typed twin
|
||||
// would, half its GL type size, and a `dmat4` is padded like any other matrix. Anything
|
||||
// reading or writing a whole uniform's storage - a bounds check, a copy between two
|
||||
// programs' shadows - wants this rather than GetUniformSizesInBytes.
|
||||
static SizeT UniformStorageSpanInBytes(const TypeFacts& type, SizeT tightSize) {
|
||||
if (type.isMatrix) {
|
||||
return static_cast<SizeT>(type.matrixCols) * 4 * sizeof(Float);
|
||||
// std140 column stride of a matrix uniform in the global UBO: every column is padded out
|
||||
// to the base alignment of a vec4 for 32-bit components, and of a dvec4 for 64-bit ones -
|
||||
// except that a 2-ROW double column is a dvec2, whose base alignment is already 16.
|
||||
// (GL 4.6 core 7.6.2.2 rules 2-4; SPIRV-Cross derives the same numbers, which is what
|
||||
// makes this agree with the reflected module.)
|
||||
static SizeT UniformMatrixColumnStride(const TypeFacts& type, const Bool nativeFloat64) {
|
||||
if (type.isDouble && nativeFloat64) {
|
||||
return type.matrixRows <= 2 ? 2 * sizeof(GLdouble) : 4 * sizeof(GLdouble);
|
||||
}
|
||||
if (type.isDouble) {
|
||||
return 4 * sizeof(Float);
|
||||
}
|
||||
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size,
|
||||
// for two reasons. std140 pads each column of a matrix out to a vec4 (or a dvec4), so a
|
||||
// mat3 spans 48 bytes even though only 36 of them carry components. And a 64-bit float
|
||||
// may have been narrowed to 32 before the module reached the backend
|
||||
// (ShaderTranspiler::DemoteFloat64Pass) - the global UBO is laid out by reflecting
|
||||
// whichever module was produced - so on a DEMOTED program a `double` uniform occupies
|
||||
// exactly what its float-typed twin would, half its GL type size, and a `dmat4` is padded
|
||||
// like any other 32-bit matrix. On a program that kept its doubles it occupies the full
|
||||
// GL type size and its matrix columns are twice as far apart. `nativeFloat64` is the
|
||||
// program's own SpirvArtifacts flag, never a live backend read: it describes the modules
|
||||
// that were actually built. Anything reading or writing a whole uniform's storage - a
|
||||
// bounds check, a copy between two programs' shadows - wants this rather than
|
||||
// GetUniformSizesInBytes.
|
||||
static SizeT UniformStorageSpanInBytes(const TypeFacts& type, SizeT tightSize,
|
||||
const Bool nativeFloat64 = false) {
|
||||
if (type.isMatrix) {
|
||||
return static_cast<SizeT>(type.matrixCols) * UniformMatrixColumnStride(type, nativeFloat64);
|
||||
}
|
||||
if (type.isDouble && !nativeFloat64) {
|
||||
return tightSize / 2;
|
||||
}
|
||||
return tightSize;
|
||||
}
|
||||
// Whether this program's modules KEPT their 64-bit floats. Joins phase B, like every
|
||||
// other question about the global UBO's layout - and it is one: it decides how wide a
|
||||
// `double` uniform's slot is.
|
||||
Bool UsesNativeFloat64() const { return Spirv().nativeFloat64; }
|
||||
SizeT GetUniformStorageSpanInBytes(Uint location) const {
|
||||
return UniformStorageSpanInBytes(GetUniformTypeFacts(location), GetUniformSizesInBytes(location));
|
||||
return UniformStorageSpanInBytes(GetUniformTypeFacts(location), GetUniformSizesInBytes(location),
|
||||
UsesNativeFloat64());
|
||||
}
|
||||
|
||||
// ---- "written since link": the per-location dirty set the pipeline composite mirrors from ----
|
||||
@@ -1323,6 +1344,15 @@ namespace MobileGL::MG_State::GLState {
|
||||
// not drawable, which the backends already express through their link-status
|
||||
// gates.
|
||||
Bool spirvStatus = false;
|
||||
// Whether these modules KEPT their 64-bit floats instead of being narrowed to 32
|
||||
// (ShaderTranspiler::DemoteFloat64Pass). Decided per PROGRAM, never per module - the
|
||||
// global UBO is one buffer all stages read, so two stages disagreeing about whether a
|
||||
// `uniform double` occupies 4 or 8 bytes would put every uniform after it at a
|
||||
// different offset in each. Recorded here rather than re-derived from the backend
|
||||
// because it is the layout THESE modules were built with: it is what the routing
|
||||
// table's offsets mean, and glUniform*d / glGetUniform*v have to write and read the
|
||||
// width the shader actually declares.
|
||||
Bool nativeFloat64 = false;
|
||||
};
|
||||
|
||||
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
|
||||
|
||||
@@ -122,7 +122,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_phaseA->in.env != nullptr && m_phaseA->in.env->backend == BackendType::DirectVulkan;
|
||||
const Bool enableSpirvValidation = m_phaseA->in.enableSpirvValidation;
|
||||
artifacts.enableSpirvValidation = enableSpirvValidation;
|
||||
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation);
|
||||
// Whether this backend consumes 64-bit floats itself. Read off the SNAPSHOT, like every
|
||||
// other environment question this node asks: a worker may not touch
|
||||
// MG_Backend::pActiveBackendObject, and the answer has to be the one the L1 key was built
|
||||
// with (ProgramLinkTask::BuildSpirvCacheKey reads the same env) or a memo written under
|
||||
// one answer could be handed back under the other.
|
||||
const Bool nativeFloat64 = m_phaseA->in.env != nullptr && m_phaseA->in.env->ConsumesFloat64Natively();
|
||||
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation,
|
||||
nativeFloat64);
|
||||
// GlslangToSpv was the only consumer of the parsed ASTs; everything after this point
|
||||
// works on the SPIR-V and on the TProgram's own self-contained reflection pool. Drop
|
||||
// them here rather than at the end of the body, which is ~87% of this node's runtime
|
||||
@@ -181,7 +188,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
|
||||
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex,
|
||||
const Bool deferOutputValidationForDirectVulkan,
|
||||
const Bool enableSpirvValidation) {
|
||||
const Bool enableSpirvValidation, const Bool nativeFloat64) {
|
||||
/* As we passed first stage compilation/linking,
|
||||
* we'll assume all the operations here should
|
||||
* pass. We may be able to employ some optimizations
|
||||
@@ -209,12 +216,39 @@ namespace MobileGL::MG_State::GLState {
|
||||
MGLOG_D("ProgramObject %u: GenerateSpirv - generated %zu SPIR-V modules", externalIndex,
|
||||
artifacts.generatedSpirv.size());
|
||||
|
||||
// The fp64 verdict, taken ONCE for the whole program and before any module is touched.
|
||||
//
|
||||
// Per program rather than per module, and that is forced by the global UBO: all stages
|
||||
// read one buffer whose layout is derived by reflecting the modules, so a vertex stage
|
||||
// that stored a `uniform double` as 4 bytes next to a fragment stage that stored it as 8
|
||||
// would put every uniform after it somewhere different in each, and the routing table
|
||||
// (one offset per location) could only describe one of them.
|
||||
//
|
||||
// The exception itself is the vertex INPUT: no backend here can fetch a 64-bit attribute,
|
||||
// and VertexInputStateFactory picks the format from the VAO attribute without ever seeing
|
||||
// what the shader declared, so a Float64 input would meet a narrowed float32 stream. One
|
||||
// such stage demotes the whole program, which is exactly what every backend without
|
||||
// native fp64 does to it anyway.
|
||||
Bool keepFloat64 = nativeFloat64;
|
||||
if (keepFloat64) {
|
||||
for (const auto& spv : artifacts.generatedSpirv) {
|
||||
if (ShaderCompiler::ModuleDeclaresFloat64VertexInput(spv)) {
|
||||
keepFloat64 = false;
|
||||
MGLOG_D("ProgramObject %u: a vertex stage declares a 64-bit float input; demoting the "
|
||||
"whole program despite native fp64",
|
||||
externalIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
artifacts.nativeFloat64 = keepFloat64;
|
||||
|
||||
// Linked SPIR-V generated, sanitize and optimize it
|
||||
Bool allOptimized = true;
|
||||
{
|
||||
for (auto& spv : artifacts.generatedSpirv) {
|
||||
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(
|
||||
spv, spv, !deferOutputValidationForDirectVulkan, enableSpirvValidation);
|
||||
spv, spv, !deferOutputValidationForDirectVulkan, enableSpirvValidation, keepFloat64);
|
||||
if (!success) {
|
||||
// The one genuine phase-B failure mode: one of the seven optimizer passes
|
||||
// reported failure, so `spv` is whatever the run left behind. A fordebug
|
||||
|
||||
@@ -66,7 +66,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
void RunBody() override;
|
||||
|
||||
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex,
|
||||
Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation);
|
||||
Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation,
|
||||
Bool nativeFloat64);
|
||||
void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
||||
|
||||
// Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as
|
||||
|
||||
@@ -160,8 +160,12 @@ TEST(SplitReadWriteImageUniformsTest, ExemptFormatsAreLeftCompletelyAlone) {
|
||||
}
|
||||
}
|
||||
|
||||
// A declaration SPIRV-Cross already qualified is none of this pass's business.
|
||||
TEST(SplitReadWriteImageUniformsTest, AlreadyQualifiedDeclarationsAreUntouched) {
|
||||
// A declaration SPIRV-Cross already qualified needs no REPAIR - but it still needs the rename.
|
||||
// The input to this pass is SPIRV-Cross output, not application source, and SPIRV-Cross picks
|
||||
// `readonly` or `writeonly` from the accesses of the stage it is emitting, so "already qualified"
|
||||
// says nothing about whether the other stages spell it the same way. The qualifiers must survive
|
||||
// untouched; only the identifier changes.
|
||||
TEST(SplitReadWriteImageUniformsTest, AlreadyQualifiedDeclarationsAreRenamedButNotRequalified) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(binding = 0, rgba8) uniform readonly highp image2D reader;
|
||||
layout(binding = 1, rgba8) uniform writeonly highp image2D writer;
|
||||
@@ -170,9 +174,33 @@ void main()
|
||||
imageStore(writer, ivec2(0), imageLoad(reader, ivec2(0)));
|
||||
}
|
||||
)";
|
||||
// Untouched means UNRENAMED too: a declaration that already carries its qualifier in the
|
||||
// source carries the SAME one in every stage, so there is no cross-stage mismatch to break up
|
||||
// and renaming it would only churn the text.
|
||||
const String out = SplitReadWriteImageUniforms(source);
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 0, rgba8) uniform readonly highp image2D " +
|
||||
RoAlias("reader") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "layout(binding = 1, rgba8) uniform writeonly highp image2D " +
|
||||
WoAlias("writer") + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + WoAlias("writer") + ",")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageLoad(" + RoAlias("reader") + ",")) << out;
|
||||
// Neither declaration is doubled and neither gains a qualifier it did not have: this is a
|
||||
// rename, not a repair.
|
||||
EXPECT_FALSE(Contains(out, IMAGE_WRITE_ALIAS_PREFIX)) << out;
|
||||
EXPECT_EQ(CountOf(out, "coherent"), 0u) << out;
|
||||
EXPECT_FALSE(Contains(out, "memoryBarrierImage")) << out;
|
||||
}
|
||||
|
||||
// A declaration carrying BOTH qualifiers is a spelling no per-stage access analysis produces, so
|
||||
// it came from the application and reads the same in every stage. Nothing to rename.
|
||||
TEST(SplitReadWriteImageUniformsTest, ADeclarationQualifiedBothWaysIsLeftCompletelyAlone) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(binding = 0, rgba8) uniform readonly writeonly highp image2D inert;
|
||||
void main()
|
||||
{
|
||||
highp ivec2 size = imageSize(inert);
|
||||
if (size.x < 0) discard;
|
||||
}
|
||||
)";
|
||||
EXPECT_EQ(SplitReadWriteImageUniforms(source), source);
|
||||
}
|
||||
|
||||
@@ -494,6 +522,67 @@ void main()
|
||||
EXPECT_TRUE(Contains(fsOut, "binding = 0"));
|
||||
}
|
||||
|
||||
// The same defect, in the shape it actually reaches the driver in. SPIRV-Cross emits the access
|
||||
// qualifier ITSELF whenever the stage only loads or only stores, so the declaration arrives here
|
||||
// already legal - and this pass used to skip it on exactly that ground, leaving the vertex stage's
|
||||
// `coherent writeonly g_image` and the fragment stage's `coherent readonly g_image` sharing one
|
||||
// name. That is the pair a raw-ES probe on the Adreno 830 reproduces with no MobileGL in the
|
||||
// process: the fragment stage reads back the untouched zeros
|
||||
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation's [1,0,0,0.2]), and
|
||||
// renaming either half fixes it. This is the emitted text of that test, verbatim.
|
||||
TEST(SplitReadWriteImageUniformsTest, StagesSpirvCrossQualifiedDifferentlyGetDifferentNames) {
|
||||
const String vertexSource = R"(#version 320 es
|
||||
layout(binding = 1, rgba32f) uniform coherent writeonly highp image2D g_image;
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image, ivec2(0), vec4(2.0));
|
||||
gl_Position = vec4(0.0);
|
||||
}
|
||||
)";
|
||||
const String fragmentSource = R"(#version 320 es
|
||||
layout(binding = 1, rgba32f) uniform coherent readonly highp image2D g_image;
|
||||
layout(location = 0) out highp vec4 mg_FragColor;
|
||||
void main()
|
||||
{
|
||||
mg_FragColor = imageLoad(g_image, ivec2(0));
|
||||
}
|
||||
)";
|
||||
const String vsOut = SplitReadWriteImageUniforms(vertexSource);
|
||||
const String fsOut = SplitReadWriteImageUniforms(fragmentSource);
|
||||
|
||||
const String vsName = WoAlias("g_image");
|
||||
const String fsName = RoAlias("g_image");
|
||||
EXPECT_NE(vsName, fsName);
|
||||
EXPECT_TRUE(Contains(vsOut, "uniform coherent writeonly highp image2D " + vsName + ";")) << vsOut;
|
||||
EXPECT_TRUE(Contains(fsOut, "uniform coherent readonly highp image2D " + fsName + ";")) << fsOut;
|
||||
EXPECT_TRUE(Contains(vsOut, "imageStore(" + vsName + ",")) << vsOut;
|
||||
EXPECT_TRUE(Contains(fsOut, "imageLoad(" + fsName + ",")) << fsOut;
|
||||
// Nothing left for a linker to merge and mis-qualify...
|
||||
EXPECT_FALSE(Contains(vsOut, fsName)) << vsOut;
|
||||
EXPECT_FALSE(Contains(fsOut, vsName)) << fsOut;
|
||||
// ...and the image unit is still the one the application asked for.
|
||||
EXPECT_TRUE(Contains(vsOut, "binding = 1")) << vsOut;
|
||||
EXPECT_TRUE(Contains(fsOut, "binding = 1")) << fsOut;
|
||||
}
|
||||
|
||||
// ...and the budget half of it: two stages SPIRV-Cross qualified the SAME way must still land on
|
||||
// one shared name, or every stage that names the image spends an image location of its own.
|
||||
TEST(SplitReadWriteImageUniformsTest, StagesSpirvCrossQualifiedAlikeShareOneName) {
|
||||
const String stage = R"(#version 320 es
|
||||
layout(binding = 1, rgba32f) uniform coherent readonly highp image2D g_image;
|
||||
layout(location = 0) out highp vec4 mg_FragColor;
|
||||
void main()
|
||||
{
|
||||
mg_FragColor = imageLoad(g_image, ivec2(0));
|
||||
}
|
||||
)";
|
||||
const String first = SplitReadWriteImageUniforms(stage);
|
||||
const String second = SplitReadWriteImageUniforms(stage);
|
||||
EXPECT_EQ(first, second);
|
||||
EXPECT_TRUE(Contains(first, "uniform coherent readonly highp image2D " + RoAlias("g_image") + ";"))
|
||||
<< first;
|
||||
}
|
||||
|
||||
// The other side of that coin, and the one a per-STAGE tag got wrong. Two stages that use the
|
||||
// image the same way emit byte-identical declarations, so they must arrive at ONE shared name:
|
||||
// Adreno allocates an image LOCATION per distinct uniform, and giving each stage its own name
|
||||
|
||||
@@ -3999,6 +3999,8 @@ namespace {
|
||||
constexpr Uint kGlR8ui = 0x8232;
|
||||
constexpr Uint kGlR32f = 0x822E;
|
||||
constexpr Uint kGlRgb10A2ui = 0x906F;
|
||||
constexpr Uint kGlRgb10A2 = 0x8059;
|
||||
constexpr Uint kGlRgb8 = 0x8051; // not one of the forty image formats at all
|
||||
} // namespace
|
||||
|
||||
// The KHR-GL4x.packed_depth_stencil.stencil_texturing compute shader, reduced: one format-less
|
||||
@@ -4049,15 +4051,26 @@ void main() { imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(15u,
|
||||
// pass on the ESSL chain and re-declares them in a core carrier SPIRV-Cross does print, so for
|
||||
// those the module is the right place and the text completion would put back the narrow token no
|
||||
// ES driver accepts. r8ui - which the stencil half of the packed_depth_stencil case binds - is
|
||||
// one of the rescued ones; rgb10_a2ui, whose 10/10/10/2 channel widths no core format has, is not.
|
||||
// one of the rescued ones, and so, now that the carriers cover all twenty-six non-core formats,
|
||||
// is every other IMAGE format. What is left for the guard is a format that is not an image format
|
||||
// at all: it has no carrier and no ESSL image spelling either, so baking it would put a token in
|
||||
// the module that means nothing.
|
||||
TEST_F(ProgramUtilTest, BakeImageFormatsLeavesOnlyTheFormatsNoCoreCarrierRescues) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR8ui))
|
||||
<< "if SPIRV-Cross ever learns to print r8ui for ES, this route can go";
|
||||
ASSERT_NE(ShaderCompiler::WidenedCoreEsslImageFormat(kGlR8ui), 0u);
|
||||
// Unprintable and rescued anyway: rgb10_a2ui's channels are unsigned INTEGER, so an rgba16ui
|
||||
// holds all four outright, and rgb10_a2's are the same channels read as NORMALIZED, which the
|
||||
// same carrier holds as their codes.
|
||||
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlRgb10A2ui));
|
||||
ASSERT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2ui), 0u);
|
||||
ASSERT_NE(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2ui), 0u);
|
||||
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlRgb10A2));
|
||||
ASSERT_NE(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb10A2), 0u);
|
||||
// ...and the one the guard still turns away.
|
||||
ASSERT_FALSE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlRgb8));
|
||||
ASSERT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(kGlRgb8), 0u);
|
||||
ASSERT_TRUE(ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(kGlR32ui));
|
||||
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(kGlR8ui), "r8ui");
|
||||
EXPECT_EQ(ShaderCompiler::EsslImageFormatSpelling(0x8051 /*GL_RGB8*/), "");
|
||||
@@ -4072,7 +4085,7 @@ void main() { imageStore(uni_image, ivec2(0), uvec4(15u)); }
|
||||
|
||||
{ // Unprintable AND uncarriable: declined, module untouched, and the stage still transpiles.
|
||||
Vector<Uint32> baked;
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlRgb10A2ui}}, baked));
|
||||
ASSERT_TRUE(ShaderCompiler::BakeImageFormatsForEssl(spirv, {{"uni_image", kGlRgb8}}, baked));
|
||||
EXPECT_EQ(baked, spirv) << "a format nothing can carry must leave the module untouched";
|
||||
EXPECT_FALSE(DecompileToEssl(baked).empty());
|
||||
}
|
||||
|
||||
@@ -379,6 +379,90 @@ TEST_F(DemoteFloat64Test, TheSharedChainDemotesToo) {
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(output)) << Disassemble(output);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The capability gate. A backend that consumes 64-bit floats itself gets none of this.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
// Everything kWideVertexSource has except the 64-bit vertex INPUT, which is what the
|
||||
// whole-program demotion falls back for. A fragment stage, so there is no input to have.
|
||||
constexpr const char* kWideFragmentSource = R"(#version 460 core
|
||||
layout(std140, binding = 0) uniform Blk {
|
||||
float a;
|
||||
double d;
|
||||
dvec2 v2;
|
||||
dvec4 v4;
|
||||
dmat4 m4;
|
||||
double arr[3];
|
||||
};
|
||||
layout(location = 0) uniform double uScale;
|
||||
layout(location = 0) in vec3 inNormal;
|
||||
layout(location = 0) out float fOut;
|
||||
void main() {
|
||||
double s = d * uScale + a;
|
||||
s += v2.x + v4.y + m4[0].z + arr[0] + arr[1] + arr[2] + 0.5lf;
|
||||
fOut = float(s) + inNormal.x;
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
// THE NEGATIVE CONTROL for the whole change: the identical module through the identical entry
|
||||
// point answers both ways, and the only thing that moved is the capability argument.
|
||||
TEST_F(DemoteFloat64Test, TheSharedChainKeepsFloat64WhenTheBackendConsumesIt) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_FRAGMENT_SHADER, kWideFragmentSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
ASSERT_TRUE(DeclaresFloat64Capability(input));
|
||||
ASSERT_GT(CountFloatTypesOfWidth(input, 64), 0u);
|
||||
|
||||
Vector<Uint32> native;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, native, true, true, true));
|
||||
EXPECT_TRUE(DeclaresFloat64Capability(native)) << Disassemble(native);
|
||||
EXPECT_GT(CountFloatTypesOfWidth(native, 64), 0u) << Disassemble(native);
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(native));
|
||||
|
||||
Vector<Uint32> demoted;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, demoted, true, true, false));
|
||||
EXPECT_FALSE(DeclaresFloat64Capability(demoted)) << Disassemble(demoted);
|
||||
EXPECT_EQ(CountFloatTypesOfWidth(demoted, 64), 0u) << Disassemble(demoted);
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64(demoted));
|
||||
|
||||
EXPECT_NE(native, demoted);
|
||||
}
|
||||
|
||||
// The exception the vertex path needs, at the level ProgramSpirvTask asks it: no backend here can
|
||||
// FETCH 64 bits, so a stage that declares a Float64 input is demoted whole even where the rest of
|
||||
// its doubles could have survived.
|
||||
TEST_F(DemoteFloat64Test, AFloat64VertexInputIsRecognisedAndOnlyOnAVertexStage) {
|
||||
const Vector<Uint32> vertexWithDoubleInput = CompileToSpirv(GL_VERTEX_SHADER, kWideVertexSource);
|
||||
ASSERT_FALSE(vertexWithDoubleInput.empty());
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64VertexInput(vertexWithDoubleInput));
|
||||
|
||||
// Doubles everywhere but the inputs: the same verdict must be false, or nothing would ever
|
||||
// take the native path.
|
||||
const Vector<Uint32> fragmentWithDoubles = CompileToSpirv(GL_FRAGMENT_SHADER, kWideFragmentSource);
|
||||
ASSERT_FALSE(fragmentWithDoubles.empty());
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64VertexInput(fragmentWithDoubles));
|
||||
|
||||
// A vertex stage whose doubles are all internal is fine too - it is the INPUT that cannot be
|
||||
// fed, not the stage.
|
||||
const String vertexWithoutDoubleInput = R"(#version 460 core
|
||||
layout(location = 0) uniform double uScale;
|
||||
layout(location = 0) in vec3 inPos;
|
||||
layout(location = 0) out float vOut;
|
||||
void main() {
|
||||
double s = double(inPos.x) * uScale + 0.5lf;
|
||||
vOut = float(s);
|
||||
gl_Position = vec4(float(s));
|
||||
}
|
||||
)";
|
||||
const Vector<Uint32> internalOnly = CompileToSpirv(GL_VERTEX_SHADER, vertexWithoutDoubleInput);
|
||||
ASSERT_FALSE(internalOnly.empty());
|
||||
EXPECT_TRUE(ShaderCompiler::ModuleDeclaresFloat64(internalOnly));
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64VertexInput(internalOnly));
|
||||
|
||||
EXPECT_FALSE(ShaderCompiler::ModuleDeclaresFloat64VertexInput({}));
|
||||
}
|
||||
|
||||
// The payoff on the Espryt path: SPIRV-Cross throws "FP64 not supported in ES profile" for every
|
||||
// one of these before demotion, so the program simply could not be transpiled at all.
|
||||
class DemoteFloat64EsslTest : public DemoteFloat64Test, public ::testing::WithParamInterface<const char*> {};
|
||||
|
||||
@@ -303,3 +303,45 @@ void main() {
|
||||
// reflects and what glUniform*d then writes into.
|
||||
EXPECT_EQ(MemberOffsetsOf(output, structId), (Vector<Uint32>{0, 4, 8})) << Disassemble(output);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The capability-gated half: a backend that consumes 64-bit floats natively gets neither pass.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// The flatten exists to preserve a byte layout ACROSS a narrowing. Where nothing narrows there is
|
||||
// nothing to preserve and the driver lays the block out itself - so the block keeps its seven
|
||||
// members at the offsets glslang computed, and the doubles in it are still doubles.
|
||||
TEST_F(FlattenFloat64StorageBlockTest, TheNativePathLeavesTheBlockAndItsDoublesAlone) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, kStd140BlockSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
const Uint32 inputStructId = StructIdNamed(input, "Wide");
|
||||
ASSERT_NE(inputStructId, 0u);
|
||||
const Vector<Uint32> inputOffsets = MemberOffsetsOf(input, inputStructId);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const Uint32 structId = StructIdNamed(output, "Wide");
|
||||
ASSERT_NE(structId, 0u) << Disassemble(output);
|
||||
EXPECT_EQ(MemberTypesOf(output, structId).size(), 7u)
|
||||
<< "the block must not be flattened when nothing is narrowing it\n"
|
||||
<< Disassemble(output);
|
||||
EXPECT_EQ(MemberOffsetsOf(output, structId), inputOffsets) << Disassemble(output);
|
||||
EXPECT_GT(CountFloatTypesOfWidth(output, 64), 0u) << Disassemble(output);
|
||||
}
|
||||
|
||||
// And the control: the SAME module through the SAME entry point with the bit clear is flattened
|
||||
// exactly as it always was. This is the pair that pins "capability-false is byte-for-byte the old
|
||||
// behaviour" at the level the device A/B checks.
|
||||
TEST_F(FlattenFloat64StorageBlockTest, TheDemotedPathIsUnchangedByTheCapabilityArgument) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, kStd140BlockSource);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> explicitlyDemoted;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, explicitlyDemoted, true, true, false));
|
||||
// The four-argument spelling every existing caller uses, which must keep meaning "demote".
|
||||
const Vector<Uint32> defaulted = Sanitize(input);
|
||||
EXPECT_EQ(explicitlyDemoted, defaulted);
|
||||
EXPECT_EQ(CountFloatTypesOfWidth(defaulted, 64), 0u) << Disassemble(defaulted);
|
||||
}
|
||||
|
||||
@@ -491,6 +491,14 @@ TEST_F(TranslationCacheTest, L1KeyMovesWithEveryInputThatMovesTheSpirv) {
|
||||
v.enableSpirvValidation = true;
|
||||
variants.emplace_back("enableSpirvValidation", BuildSpirvTranslationKey(v));
|
||||
}
|
||||
{ // CompileEnv::ConsumesFloat64Natively(): the fp64 tail of SanitizeAndOptimizeBinary is
|
||||
// skipped under it, so the SAME GLSL yields modules with real doubles under one answer
|
||||
// and demoted, storage-block-flattened ones under the other. The one backend capability
|
||||
// bit in this key, and the only one allowed in without changing what glslang produces.
|
||||
SpirvTranslationKeyInputs v = base;
|
||||
v.nativeFloat64 = true;
|
||||
variants.emplace_back("nativeFloat64", BuildSpirvTranslationKey(v));
|
||||
}
|
||||
// ---- inputs the WIDENED payload pulled into the key ----
|
||||
// They cannot move a word of the generated SPIR-V, but they do shape the reflection the
|
||||
// payload now carries, so they have to split the key. This is the group that would go
|
||||
@@ -599,6 +607,45 @@ TEST_F(TranslationCacheTest, TwoBackendsCompilingTheSameGlslShareOneL1Entry) {
|
||||
EXPECT_TRUE(BuildSpirvTranslationKey(onA) == BuildSpirvTranslationKey(onB));
|
||||
}
|
||||
|
||||
// The ONE capability bit that breaks that sharing, and the two halves of why it is placed where
|
||||
// it is. It must NOT move the front-end fingerprint - glslang parses, reflects and generates a
|
||||
// `double` identically under it, and L1c (the parse-verdict memo) keys on that same fingerprint
|
||||
// and would take a false miss per backend for nothing. It MUST move the L1 key, because L1's
|
||||
// payload is the module AFTER SanitizeAndOptimizeBinary and the fp64 tail of that chain is
|
||||
// exactly what this bit gates.
|
||||
TEST_F(TranslationCacheTest, NativeFloat64IsOutOfTheFrontendFingerprintAndInsideTheL1Key) {
|
||||
CompileEnv none; // no backend at all
|
||||
CompileEnv emulated; // a backend without the feature
|
||||
CompileEnv nativeEnv; // a backend with it
|
||||
emulated.backend = BackendType::DirectVulkan;
|
||||
nativeEnv.backend = BackendType::DirectVulkan;
|
||||
nativeEnv.params.SupportsShaderFloat64 = true;
|
||||
|
||||
// No backend answers FALSE: the demoted module is the one that works everywhere, so a
|
||||
// standalone compile gets it.
|
||||
EXPECT_FALSE(none.ConsumesFloat64Natively());
|
||||
EXPECT_FALSE(emulated.ConsumesFloat64Natively());
|
||||
EXPECT_TRUE(nativeEnv.ConsumesFloat64Natively());
|
||||
|
||||
EXPECT_EQ(ComputeFrontendCompileEnvFingerprint(emulated), ComputeFrontendCompileEnvFingerprint(nativeEnv))
|
||||
<< "the fp64 capability leaked into the front-end fingerprint";
|
||||
EXPECT_NE(ComputeCompileEnvFingerprint(emulated), ComputeCompileEnvFingerprint(nativeEnv))
|
||||
<< "the whole-environment fingerprint has to notice it - it is a DynamicBackendParameters "
|
||||
"field, hashed by object representation";
|
||||
|
||||
const Vector<SpirvTranslationKeyInputs::Stage> stages{{GL_VERTEX_SHADER, kVertexSource},
|
||||
{GL_FRAGMENT_SHADER, kFragmentSource}};
|
||||
SpirvTranslationKeyInputs demoted = BaselineSpirvInputs(stages);
|
||||
demoted.frontendFingerprint = ComputeFrontendCompileEnvFingerprint(emulated);
|
||||
demoted.nativeFloat64 = emulated.ConsumesFloat64Natively();
|
||||
SpirvTranslationKeyInputs kept = BaselineSpirvInputs(stages);
|
||||
kept.frontendFingerprint = ComputeFrontendCompileEnvFingerprint(nativeEnv);
|
||||
kept.nativeFloat64 = nativeEnv.ConsumesFloat64Natively();
|
||||
|
||||
EXPECT_FALSE(BuildSpirvTranslationKey(demoted) == BuildSpirvTranslationKey(kept))
|
||||
<< "one L1 entry would then describe two different module sets";
|
||||
}
|
||||
|
||||
// The other direction, one case per input that was KEPT. Each is a limit the front end
|
||||
// really consumes - everything BuildTBuiltInResource copies into TBuiltInResource, plus the
|
||||
// two inputs to the reflection vertex-attrib limit - so each must still split the key.
|
||||
|
||||
@@ -89,6 +89,7 @@ namespace {
|
||||
struct StorageImageType {
|
||||
Uint32 resultId = 0u;
|
||||
Uint32 format = 0u;
|
||||
Uint32 sampledTypeId = 0u;
|
||||
};
|
||||
|
||||
Vector<StorageImageType> CollectStorageImageTypes(const Vector<Uint32>& spirv) {
|
||||
@@ -96,11 +97,27 @@ namespace {
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (opcode != spv::Op::OpTypeImage || wordCount < 9u) return;
|
||||
if (words[7] != 2u) return;
|
||||
types.push_back(StorageImageType{words[1], words[8]});
|
||||
types.push_back(StorageImageType{words[1], words[8], words[2]});
|
||||
});
|
||||
return types;
|
||||
}
|
||||
|
||||
// "float" / "uint" / "int" / "" for a scalar numeric type id, which is the one thing that says
|
||||
// whether a declaration is still an image2D or has become a uimage2D.
|
||||
String ScalarTypeSpellingOf(const Vector<Uint32>& spirv, Uint32 typeId) {
|
||||
String spelling;
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
if (words[1] != typeId) return;
|
||||
// OpTypeFloat words: 1 result id, 2 width. OpTypeInt adds 3 signedness.
|
||||
if (opcode == spv::Op::OpTypeFloat && wordCount >= 3u) {
|
||||
spelling = "float";
|
||||
} else if (opcode == spv::Op::OpTypeInt && wordCount >= 4u) {
|
||||
spelling = words[3] != 0u ? "int" : "uint";
|
||||
}
|
||||
});
|
||||
return spelling;
|
||||
}
|
||||
|
||||
// OpVectorShuffle words: 0 opcode/count, 1 result type, 2 result id, 3 vector 1, 4 vector 2,
|
||||
// 5.. the component selectors.
|
||||
struct VectorShuffle {
|
||||
@@ -243,8 +260,9 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
// rg32f again, but as a BUFFER image. Same format, same carrier on paper - and it must be
|
||||
// left alone anyway, because a buffer image's texels are the application's buffer object.
|
||||
// rg32f again, but as a BUFFER image. Same format, and NOT the same emulation: a buffer
|
||||
// image's texels are the application's buffer object, so there is nothing to reallocate a
|
||||
// carrier in - but the same bytes can be VIEWED as twice as many r32f texels, which is exact.
|
||||
const char* const kRg32fBufferLoadStore = R"(#version 430 core
|
||||
layout(rg32f, binding = 0) uniform imageBuffer img;
|
||||
out vec4 fragColor;
|
||||
@@ -255,15 +273,76 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
// rg16 is one of the EIGHT with no core carrier at all - core ESSL has no 16-bit normalized
|
||||
// format, so every candidate loses range or changes the component type the texture presents.
|
||||
// It must be left alone and keep the honest "no GLSL ES spelling" diagnostic instead.
|
||||
// ...and one that asks the image how big it is, which the split has to halve: the ES view has
|
||||
// twice the texels the application's format describes.
|
||||
const char* const kRg32fBufferSize = R"(#version 430 core
|
||||
layout(rg32f, binding = 0) uniform imageBuffer img;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = vec4(float(imageSize(img)));
|
||||
}
|
||||
)";
|
||||
|
||||
// rg16f as a buffer image: two channels of 16-bit float, whose single-channel base r16f core
|
||||
// ESSL does not have. Nothing to split it into, so it keeps the honest failure.
|
||||
const char* const kRg16fBufferLoadStore = R"(#version 430 core
|
||||
layout(rg16f, binding = 0) uniform imageBuffer img;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 texel = imageLoad(img, int(gl_FragCoord.x));
|
||||
imageStore(img, int(gl_FragCoord.x), vec4(1.0, 2.0, 3.0, 4.0));
|
||||
fragColor = texel;
|
||||
}
|
||||
)";
|
||||
|
||||
// rgb10_a2ui: FOUR unsigned-integer channels of 10, 10, 10 and 2 bits, carried in an rgba16ui
|
||||
// that gives each of them sixteen. The only widening whose carrier has as many channels as the
|
||||
// original, so it is the only one where GL leaves NOTHING to pin and both accesses must come
|
||||
// out exactly as glslang emitted them.
|
||||
const char* const kRgb10A2uiLoadStore = R"(#version 430 core
|
||||
layout(rgb10_a2ui, binding = 0) uniform uimage2D img;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
uvec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
|
||||
imageStore(img, ivec2(gl_FragCoord.xy), uvec4(7u, 8u, 9u, 3u));
|
||||
fragColor = vec4(texel);
|
||||
}
|
||||
)";
|
||||
|
||||
// rg16: TWO unsigned-normalized 16-bit channels, which core ESSL has no image format of any
|
||||
// width for. Carried as its own CODES in an rgba16ui, so the declaration comes out a
|
||||
// uimage2D and every access is wrapped in GL 4.6 2.3.5 as well as masked.
|
||||
const char* const kRg16LoadStore = R"(#version 430 core
|
||||
layout(rg16, binding = 0) uniform image2D img;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
|
||||
imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, 2.0, 3.0, 4.0));
|
||||
imageStore(img, ivec2(gl_FragCoord.xy), vec4(0.25, 0.5, 0.75, 1.0));
|
||||
fragColor = texel;
|
||||
}
|
||||
)";
|
||||
|
||||
// rgba16_snorm: the signed twin, whose code is a two's-complement 16-bit integer sitting in an
|
||||
// UNSIGNED carrier channel - so the load has to sign-extend it back and the store has to mask
|
||||
// it down, neither of which the unsigned conversion does.
|
||||
const char* const kRgba16SnormLoadStore = R"(#version 430 core
|
||||
layout(rgba16_snorm, binding = 0) uniform image2D img;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
|
||||
imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, -1.0, 0.5, -0.5));
|
||||
fragColor = texel;
|
||||
}
|
||||
)";
|
||||
|
||||
// rgb10_a2: FOUR normalized channels that are not all the same width, so its denominator is
|
||||
// (1023, 1023, 1023, 3) and one number would be wrong for a quarter of every texel.
|
||||
const char* const kRgb10A2LoadStore = R"(#version 430 core
|
||||
layout(rgb10_a2, binding = 0) uniform image2D img;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy));
|
||||
imageStore(img, ivec2(gl_FragCoord.xy), vec4(0.25, 0.5, 0.75, 1.0));
|
||||
fragColor = texel;
|
||||
}
|
||||
)";
|
||||
@@ -273,7 +352,7 @@ void main() {
|
||||
// the shader rewrite, the ES texture storage and the glBindImageTexture argument. If it drifts
|
||||
// the three stop agreeing, and a narrow texture read through a wide image goes out of bounds
|
||||
// silently on every driver tested.
|
||||
TEST(WidenImageFormats, EighteenNonCoreFormatsHaveALosslessCoreCarrier) {
|
||||
TEST(WidenImageFormats, TwentySixNonCoreFormatsHaveALosslessCoreCarrier) {
|
||||
struct Case {
|
||||
Uint requested;
|
||||
Uint carrier;
|
||||
@@ -302,6 +381,19 @@ TEST(WidenImageFormats, EighteenNonCoreFormatsHaveALosslessCoreCarrier) {
|
||||
// is e5m5 against a half's s1e5m10, so the carrier is still lossless - and three channels,
|
||||
// so the mask has to pin only alpha.
|
||||
{0x8C3A, 0x881A, 3, "GL_R11F_G11F_B10F -> GL_RGBA16F"},
|
||||
// FOUR channels: 10, 10, 10 and 2 bits of unsigned integer all fit in sixteen, so nothing
|
||||
// is masked at all and only the packed TRANSFER is re-encoded.
|
||||
{0x906F, 0x8D76, 4, "GL_RGB10_A2UI -> GL_RGBA16UI"},
|
||||
// The seven NORMALIZED formats, carried as their own channel CODES in the same rgba16ui.
|
||||
// These are the entries whose carrier changes the shader-visible TYPE as well, which is
|
||||
// why every access through them is wrapped in GL 4.6 2.3.5 rather than only masked.
|
||||
{0x805B, 0x8D76, 4, "GL_RGBA16 -> GL_RGBA16UI"},
|
||||
{0x822C, 0x8D76, 2, "GL_RG16 -> GL_RGBA16UI"},
|
||||
{0x822A, 0x8D76, 1, "GL_R16 -> GL_RGBA16UI"},
|
||||
{0x8059, 0x8D76, 4, "GL_RGB10_A2 -> GL_RGBA16UI"},
|
||||
{0x8F9B, 0x8D76, 4, "GL_RGBA16_SNORM -> GL_RGBA16UI"},
|
||||
{0x8F99, 0x8D76, 2, "GL_RG16_SNORM -> GL_RGBA16UI"},
|
||||
{0x8F98, 0x8D76, 1, "GL_R16_SNORM -> GL_RGBA16UI"},
|
||||
};
|
||||
for (const Case& testCase : cases) {
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(testCase.requested), testCase.carrier)
|
||||
@@ -317,7 +409,7 @@ TEST(WidenImageFormats, EighteenNonCoreFormatsHaveALosslessCoreCarrier) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(WidenImageFormats, CoreFormatsAndTheEightWithoutALosslessCarrierAreRefused) {
|
||||
TEST(WidenImageFormats, CoreFormatsAreRefused) {
|
||||
// The thirteen GLSL ES already has: nothing to carry.
|
||||
for (const Uint coreFormat : {0x8814u /*RGBA32F*/, 0x881Au /*RGBA16F*/, 0x822Eu /*R32F*/,
|
||||
0x8058u /*RGBA8*/, 0x8F97u /*RGBA8_SNORM*/, 0x8D82u /*RGBA32I*/,
|
||||
@@ -327,24 +419,63 @@ TEST(WidenImageFormats, CoreFormatsAndTheEightWithoutALosslessCarrierAreRefused)
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(coreFormat), 0u)
|
||||
<< "core format 0x" << std::hex << coreFormat;
|
||||
}
|
||||
// The eight with no LOSSLESS core carrier: core ESSL has no 16-bit normalized format and no
|
||||
// 10-bit one, so every candidate for these either loses range or changes the component type
|
||||
// the texture presents to anything that samples it. Deliberately left to the honest
|
||||
// diagnostic. r11f_g11f_b10f is NOT among them - rgba16f holds every value it can, so it is
|
||||
// carried above.
|
||||
for (const Uint hardFormat : {0x8059u /*RGB10_A2*/,
|
||||
0x906Fu /*RGB10_A2UI*/, 0x805Bu /*RGBA16*/, 0x822Cu /*RG16*/,
|
||||
0x822Au /*R16*/, 0x8F9Bu /*RGBA16_SNORM*/, 0x8F99u /*RG16_SNORM*/,
|
||||
0x8F98u /*R16_SNORM*/}) {
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(hardFormat), 0u)
|
||||
<< "format without an exact carrier 0x" << std::hex << hardFormat;
|
||||
}
|
||||
// Not an image format at all.
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(0x8051 /*GL_RGB8*/), 0u);
|
||||
EXPECT_EQ(ShaderCompiler::ImageFormatChannelCount(0x8051 /*GL_RGB8*/), 0u);
|
||||
EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(0), 0u);
|
||||
}
|
||||
|
||||
// The denominators of GL 4.6 2.3.5, which is the whole difference between a carrier that holds a
|
||||
// format's VALUES and one that holds its CODES. Both halves of DirectGLES's transfer read them
|
||||
// (the upload's synthetic alpha and glGetTexImage's divide), and so does the shader rewrite, so a
|
||||
// wrong entry here is wrong in three places at once and consistently - which is exactly the kind
|
||||
// of error a round-trip test cannot see.
|
||||
TEST(WidenImageFormats, OnlyTheNormalizedFormatsCarryCodesAndTheirDenominatorsAreTheFormatsOwn) {
|
||||
struct Case {
|
||||
Uint format;
|
||||
Uint32 channelMax[4];
|
||||
bool isSigned;
|
||||
const char* name;
|
||||
};
|
||||
const Case cases[] = {
|
||||
{0x805B, {65535u, 65535u, 65535u, 65535u}, false, "GL_RGBA16"},
|
||||
{0x822C, {65535u, 65535u, 65535u, 65535u}, false, "GL_RG16"},
|
||||
{0x822A, {65535u, 65535u, 65535u, 65535u}, false, "GL_R16"},
|
||||
// The one format whose channels are not all the same width, and the reason the answer is
|
||||
// four numbers rather than one: a two-bit alpha saturates at 3, not at 1023.
|
||||
{0x8059, {1023u, 1023u, 1023u, 3u}, false, "GL_RGB10_A2"},
|
||||
{0x8F9B, {32767u, 32767u, 32767u, 32767u}, true, "GL_RGBA16_SNORM"},
|
||||
{0x8F99, {32767u, 32767u, 32767u, 32767u}, true, "GL_RG16_SNORM"},
|
||||
{0x8F98, {32767u, 32767u, 32767u, 32767u}, true, "GL_R16_SNORM"},
|
||||
};
|
||||
for (const Case& testCase : cases) {
|
||||
Uint32 channelMax[4] = {0u, 0u, 0u, 0u};
|
||||
bool isSigned = !testCase.isSigned;
|
||||
EXPECT_TRUE(ShaderCompiler::NormalizedImageCarrierCodes(testCase.format, channelMax, isSigned))
|
||||
<< testCase.name;
|
||||
for (Uint channel = 0; channel < 4; ++channel) {
|
||||
EXPECT_EQ(channelMax[channel], testCase.channelMax[channel])
|
||||
<< testCase.name << " channel " << channel;
|
||||
}
|
||||
EXPECT_EQ(isSigned, testCase.isSigned) << testCase.name;
|
||||
}
|
||||
|
||||
// Everything else keeps its own component type in the carrier, so nothing is converted: an
|
||||
// rg8's carrier channel really is an 8-bit unsigned normalized one, and an rgb10_a2ui's
|
||||
// channel really does hold the integer the shader stored.
|
||||
for (const Uint direct : {0x8230u /*RG32F*/, 0x8229u /*R8*/, 0x8F94u /*R8_SNORM*/,
|
||||
0x8232u /*R8UI*/, 0x8C3Au /*R11F_G11F_B10F*/, 0x906Fu /*RGB10_A2UI*/,
|
||||
0x8814u /*RGBA32F*/, 0x8051u /*RGB8, not an image format*/}) {
|
||||
Uint32 channelMax[4] = {7u, 7u, 7u, 7u};
|
||||
bool isSigned = true;
|
||||
EXPECT_FALSE(ShaderCompiler::NormalizedImageCarrierCodes(direct, channelMax, isSigned))
|
||||
<< "format 0x" << std::hex << direct;
|
||||
for (Uint channel = 0; channel < 4; ++channel) {
|
||||
EXPECT_EQ(channelMax[channel], 7u) << "a refused format must leave the output alone";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(WidenImageFormats, TwoChannelFloatImageBecomesRgba32fWithBothAccessesMasked) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRg32fLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
@@ -429,6 +560,38 @@ TEST(WidenImageFormats, ThreeChannelPackedFloatImageBecomesRgba16fWithOnlyAlphaP
|
||||
EXPECT_TRUE(HasComponents(*loadMask, {0u, 1u, 2u, 7u}));
|
||||
}
|
||||
|
||||
// The four-channel case, which is the whole of rgb10_a2ui's shader-side emulation: the carrier has
|
||||
// as many channels as the original, every value of every channel fits, and GL therefore defines
|
||||
// NOTHING about a surplus channel because there is none. So both accesses have to come out
|
||||
// untouched - a pass that masked here would replace the alpha the application stored (0..3 of a
|
||||
// two-bit channel, which the CTS walker writes as 3) with the constant 1 and drop blue outright.
|
||||
TEST(WidenImageFormats, FourChannelIntegerImageBecomesRgba16uiWithNeitherAccessMasked) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRgb10A2uiLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
|
||||
const auto beforeTypes = CollectStorageImageTypes(spirv);
|
||||
ASSERT_EQ(beforeTypes.size(), 1u);
|
||||
EXPECT_EQ(beforeTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgb10a2ui));
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
||||
/*enableSpirvValidation=*/true));
|
||||
ASSERT_FALSE(widened.empty());
|
||||
EXPECT_TRUE(Validates(widened));
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(widened));
|
||||
|
||||
const auto afterTypes = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba16ui));
|
||||
|
||||
// The declaration moved and nothing else did.
|
||||
EXPECT_EQ(CollectVectorShuffles(widened).size(), CollectVectorShuffles(spirv).size())
|
||||
<< "a carrier with as many channels as the original must add no mask";
|
||||
EXPECT_EQ(CollectImageReadResultIds(widened).size(), CollectImageReadResultIds(spirv).size())
|
||||
<< "the imageLoad was duplicated for a rewrite that has nothing to rewrite";
|
||||
}
|
||||
|
||||
// ...and the same module through the emitter, which is where the failure actually showed: ESSL has
|
||||
// no `r11f_g11f_b10f` token, SPIRV-Cross throws for it, and the throw took every image uniform
|
||||
// declared in the same stage with it.
|
||||
@@ -451,37 +614,157 @@ TEST(WidenImageFormats, PackedFloatImageOnlyReachesEsslThroughTheCarrier) {
|
||||
EXPECT_EQ(after.text.find("r11f_g11f_b10f"), String::npos) << after.text;
|
||||
}
|
||||
|
||||
// A BUFFER image is declined whatever its format, and the format alone cannot say so - rg32f is
|
||||
// carried exactly when it is an image2D. What makes the difference is that widening REALLOCATES
|
||||
// the texture behind the image in the carrier, and a buffer image has no texture storage to
|
||||
// reallocate: its texels are the application's buffer object, usually also a vertex, index or
|
||||
// storage buffer. Widening one leaves the shader striding 16 bytes through 8-byte texels - the
|
||||
// measured symptom on an Adreno 830 was a 32-byte GL_RG32F buffer reading back
|
||||
// A BUFFER image is never WIDENED, whatever its format, and the format alone cannot say so -
|
||||
// rg32f is carried in an rgba32f when it is an image2D. What makes the difference is that widening
|
||||
// REALLOCATES the texture behind the image in the carrier, and a buffer image has no texture
|
||||
// storage to reallocate: its texels are the application's buffer object, usually also a vertex,
|
||||
// index or storage buffer. Widening one leaves the shader striding 16 bytes through 8-byte texels
|
||||
// - the measured symptom on an Adreno 830 was a 32-byte GL_RG32F buffer reading back
|
||||
// [1,100] [0,1] [2,100] [0,1] instead of [1,100] [2,100] [3,100] [4,100], with the last two texels
|
||||
// written past the end of the application's buffer.
|
||||
TEST(WidenImageFormats, BufferImagesAreDeclinedEvenWhenTheirFormatHasACarrier) {
|
||||
//
|
||||
// It is SPLIT instead, which is the opposite move: the bytes stay exactly where they are and the
|
||||
// SUBSCRIPT changes. rg32f over N texels and r32f over 2N texels describe the same memory, so
|
||||
// component j of texel i is texel 2i + j, and the base format is one of the thirteen ES has.
|
||||
TEST(WidenImageFormats, BufferImagesAreSplitByTheSubscriptRatherThanWidened) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRg32fBufferLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
const auto beforeTypes = CollectStorageImageTypes(spirv);
|
||||
ASSERT_EQ(beforeTypes.size(), 1u);
|
||||
EXPECT_EQ(beforeTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg32f))
|
||||
<< "the fixture stopped declaring the format this test is about";
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
|
||||
Vector<Uint32> split;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, split, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
||||
/*enableSpirvValidation=*/true));
|
||||
ASSERT_FALSE(split.empty());
|
||||
EXPECT_TRUE(Validates(split));
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(split));
|
||||
|
||||
const auto afterTypes = CollectStorageImageTypes(split);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::R32f))
|
||||
<< "the base format is the SINGLE-channel one, not the four-channel carrier a 2D image "
|
||||
"would take - a buffer image that gained texel width would run off the end of the "
|
||||
"application's buffer";
|
||||
|
||||
// ONE imageLoad became TWO, and ONE imageStore became two as well: each component of the
|
||||
// original texel is its own texel of the base view.
|
||||
EXPECT_EQ(CollectImageReadResultIds(split).size(), 2u * CollectImageReadResultIds(spirv).size());
|
||||
EXPECT_EQ(CollectImageWriteTexelIds(split).size(), 2u * CollectImageWriteTexelIds(spirv).size());
|
||||
|
||||
// ...and the store's two texels are the two components, not the same one twice.
|
||||
const auto shuffles = CollectVectorShuffles(split);
|
||||
const auto texelIds = CollectImageWriteTexelIds(split);
|
||||
ASSERT_EQ(texelIds.size(), 2u);
|
||||
const VectorShuffle* firstTexel = FindShuffleWithResult(shuffles, texelIds[0]);
|
||||
const VectorShuffle* secondTexel = FindShuffleWithResult(shuffles, texelIds[1]);
|
||||
ASSERT_NE(firstTexel, nullptr);
|
||||
ASSERT_NE(secondTexel, nullptr);
|
||||
EXPECT_TRUE(HasComponents(*firstTexel, {0u, 4u, 4u, 7u}))
|
||||
<< "expected (r, 0, 0, 1) - component 0 of the texel into a one-channel base format";
|
||||
EXPECT_TRUE(HasComponents(*secondTexel, {1u, 4u, 4u, 7u}))
|
||||
<< "expected (g, 0, 0, 1) - component 1 into the NEXT base texel";
|
||||
|
||||
// The subscript arithmetic itself: one multiply and one add per access.
|
||||
Uint32 multiplies = 0;
|
||||
Uint32 adds = 0;
|
||||
ForEachInstruction(split, [&](spv::Op opcode, const Uint32*, Uint32) {
|
||||
if (opcode == spv::Op::OpIMul) ++multiplies;
|
||||
if (opcode == spv::Op::OpIAdd) ++adds;
|
||||
});
|
||||
EXPECT_GE(multiplies, 2u) << "2i, once for the load and once for the store";
|
||||
EXPECT_GE(adds, 2u) << "2i + 1, once for the load and once for the store";
|
||||
|
||||
// And what reaches the driver names a format ES has.
|
||||
const EsslAttempt after = EmitEssl(split);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
EXPECT_NE(after.text.find("r32f"), String::npos) << after.text;
|
||||
EXPECT_EQ(after.text.find("rg32f"), String::npos)
|
||||
<< "the token no ES driver accepts is still in the emitted source:\n"
|
||||
<< after.text;
|
||||
}
|
||||
|
||||
// imageSize() has to be halved with everything else: the ES view really does have twice the texels
|
||||
// the application's format describes, so a shader that walks the buffer by its own size would run
|
||||
// off the end of it - or, on a well-behaved driver, spend half its invocations past the data.
|
||||
TEST(WidenImageFormats, ASplitBufferImageReportsTheSizeItsOwnFormatDescribes) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRg32fBufferSize);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
|
||||
Vector<Uint32> split;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, split, false, true));
|
||||
ASSERT_FALSE(split.empty());
|
||||
EXPECT_TRUE(Validates(split));
|
||||
|
||||
Uint32 sizeQueries = 0;
|
||||
Uint32 divisions = 0;
|
||||
ForEachInstruction(split, [&](spv::Op opcode, const Uint32*, Uint32) {
|
||||
if (opcode == spv::Op::OpImageQuerySize) ++sizeQueries;
|
||||
if (opcode == spv::Op::OpSDiv || opcode == spv::Op::OpUDiv) ++divisions;
|
||||
});
|
||||
EXPECT_EQ(sizeQueries, 1u) << "the query itself is not duplicated, only divided";
|
||||
EXPECT_EQ(divisions, 1u);
|
||||
|
||||
const EsslAttempt after = EmitEssl(split);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
EXPECT_NE(after.text.find("imageSize"), String::npos) << after.text;
|
||||
EXPECT_NE(after.text.find("/ 2"), String::npos)
|
||||
<< "the reported size must be the application's, not the base view's:\n"
|
||||
<< after.text;
|
||||
}
|
||||
|
||||
// A buffer image whose base format is NOT core ESSL has nothing to split into, and must keep the
|
||||
// honest "no GLSL ES spelling" failure rather than take a wider one: rg16f's components are 16-bit
|
||||
// floats and core ESSL has no r16f, so a split would have to change the component type.
|
||||
TEST(WidenImageFormats, ABufferImageWithNoCoreBaseFormatIsLeftAlone) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRg16fBufferLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
const auto types = CollectStorageImageTypes(spirv);
|
||||
ASSERT_EQ(types.size(), 1u);
|
||||
EXPECT_EQ(types.front().format, static_cast<Uint32>(spv::ImageFormat::Rg32f))
|
||||
<< "the fixture stopped declaring the format this test is about";
|
||||
EXPECT_EQ(types.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16f));
|
||||
|
||||
// The gate says no, so the optimizer is never even run for it...
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
// ...and running it anyway changes nothing, which is what keeps the gate and the pass from
|
||||
// disagreeing about a module.
|
||||
Vector<Uint32> widened;
|
||||
ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
||||
/*enableSpirvValidation=*/true);
|
||||
EXPECT_TRUE(widened.empty() || widened == spirv) << "a buffer image was rewritten";
|
||||
Vector<Uint32> split;
|
||||
ShaderCompiler::WidenImageFormatsForEssl(spirv, split, false, true);
|
||||
if (!split.empty()) {
|
||||
const auto afterTypes = CollectStorageImageTypes(split);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16f));
|
||||
}
|
||||
}
|
||||
|
||||
// The same format in a NON-buffer image still widens, or this test would pass for the wrong
|
||||
// reason - a widening that had simply stopped working.
|
||||
const Vector<Uint32> planar = CompileFragment(kRg32fLoadStore);
|
||||
ASSERT_FALSE(planar.empty());
|
||||
EXPECT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(planar));
|
||||
// The table the three layers share, from the other side: only the 32-bit component family has a
|
||||
// core single-channel base, and a two-dimensional image never takes this route.
|
||||
TEST(WidenImageFormats, OnlyTheThirtyTwoBitTwoChannelFormatsSplitAsBufferImages) {
|
||||
struct Case {
|
||||
Uint format;
|
||||
Uint base;
|
||||
const char* name;
|
||||
};
|
||||
const Case cases[] = {
|
||||
{0x8230, 0x822E, "GL_RG32F -> GL_R32F"},
|
||||
{0x823B, 0x8235, "GL_RG32I -> GL_R32I"},
|
||||
{0x823C, 0x8236, "GL_RG32UI -> GL_R32UI"},
|
||||
};
|
||||
for (const Case& testCase : cases) {
|
||||
EXPECT_EQ(ShaderCompiler::SplitCoreEsslBufferImageFormat(testCase.format), testCase.base)
|
||||
<< testCase.name;
|
||||
EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(testCase.base)) << testCase.name;
|
||||
EXPECT_EQ(ShaderCompiler::ImageFormatChannelCount(testCase.base), 1u) << testCase.name;
|
||||
}
|
||||
// No core single-channel base of the right component type, so no split.
|
||||
for (const Uint refused : {0x822Fu /*RG16F*/, 0x8239u /*RG16I*/, 0x823Au /*RG16UI*/, 0x822Bu /*RG8*/,
|
||||
0x8F95u /*RG8_SNORM*/, 0x822Cu /*RG16*/, 0x8237u /*RG8I*/, 0x8238u /*RG8UI*/,
|
||||
// Already core, or four-channel, or not an image format at all.
|
||||
0x8814u /*RGBA32F*/, 0x822Eu /*R32F*/, 0x8051u /*RGB8*/, 0u}) {
|
||||
EXPECT_EQ(ShaderCompiler::SplitCoreEsslBufferImageFormat(refused), 0u)
|
||||
<< "format 0x" << std::hex << refused;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(WidenImageFormats, SingleChannelUnsignedImageBecomesRgba8uiWithBothAccessesMasked) {
|
||||
@@ -631,25 +914,132 @@ TEST(WidenImageFormats, CoreFormatModuleIsHandedBackUntouched) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(WidenImageFormats, FormatWithoutAnExactCarrierIsLeftAlone) {
|
||||
// The normalized carrier, which is the one that does not merely re-DECLARE the image: a 16-bit
|
||||
// normalized channel has no core ESSL format of any width behind it, and no FLOAT carrier is
|
||||
// honest either (a half has eleven mantissa bits against its sixteen), so what the rgba16ui holds
|
||||
// is the format's own CODE. That changes the shader-visible TYPE, which is the thing to check -
|
||||
// an image2D whose format moved to rgba16ui but whose sampled type stayed float is not merely
|
||||
// wrong, it is invalid SPIR-V, and a module that kept the float type while the STORAGE became an
|
||||
// integer texture would read whole texels as garbage.
|
||||
TEST(WidenImageFormats, NormalizedImageBecomesAUimageWhoseAccessesConvertItsCodes) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRg16LoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
|
||||
const auto beforeTypes = CollectStorageImageTypes(spirv);
|
||||
ASSERT_EQ(beforeTypes.size(), 1u);
|
||||
EXPECT_EQ(beforeTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16));
|
||||
|
||||
// rg16 has no core format with 16-bit unsigned-normalized channels behind it. Anything wider
|
||||
// would requantize differently from what the application asked for, so the pass declines and
|
||||
// CollectImageFormatBakeInputs reports the format as unspellable instead.
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
EXPECT_EQ(ScalarTypeSpellingOf(spirv, beforeTypes.front().sampledTypeId), "float");
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
||||
/*enableSpirvValidation=*/true);
|
||||
if (!widened.empty()) {
|
||||
const auto afterTypes = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rg16));
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*onlyFormatsSpirvCrossRefusesToPrint=*/false,
|
||||
/*enableSpirvValidation=*/true));
|
||||
ASSERT_FALSE(widened.empty());
|
||||
EXPECT_TRUE(Validates(widened));
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(widened));
|
||||
|
||||
const auto afterTypes = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba16ui));
|
||||
EXPECT_EQ(ScalarTypeSpellingOf(widened, afterTypes.front().sampledTypeId), "uint")
|
||||
<< "the carrier's component type is unsigned integer, and spirv-val requires the image's "
|
||||
"Sampled Type to say so";
|
||||
|
||||
// The masks are still there and still say what a two-channel format's surplus channels are -
|
||||
// the conversion wraps them, it does not replace them.
|
||||
const auto shuffles = CollectVectorShuffles(widened);
|
||||
const auto texelIds = CollectImageWriteTexelIds(widened);
|
||||
ASSERT_EQ(texelIds.size(), 1u);
|
||||
// The texel is now the PACKED value, so the mask is one step further back: find the shuffle
|
||||
// by its component selectors instead.
|
||||
Bool sawTwoChannelMask = false;
|
||||
for (const VectorShuffle& shuffle : shuffles) {
|
||||
sawTwoChannelMask = sawTwoChannelMask || HasComponents(shuffle, {0u, 1u, 6u, 7u});
|
||||
}
|
||||
EXPECT_TRUE(sawTwoChannelMask) << "expected the (r, g, 0, 1) mask a two-channel format needs";
|
||||
|
||||
// ...and the ESSL says the whole story: a uimage2D holding rgba16ui, divided and multiplied
|
||||
// by the format's own 65535.
|
||||
const EsslAttempt after = EmitEssl(widened);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
EXPECT_NE(after.text.find("uimage2D"), String::npos) << after.text;
|
||||
EXPECT_NE(after.text.find("rgba16ui"), String::npos) << after.text;
|
||||
EXPECT_EQ(after.text.find("rg16"), String::npos)
|
||||
<< "the token no ES driver accepts is still in the emitted source:\n"
|
||||
<< after.text;
|
||||
EXPECT_NE(after.text.find("65535.0"), String::npos)
|
||||
<< "the unsigned-normalized denominator is 2^16 - 1:\n"
|
||||
<< after.text;
|
||||
EXPECT_EQ(after.text.find("32767.0"), String::npos)
|
||||
<< "an unsigned format must not take the SIGNED denominator:\n"
|
||||
<< after.text;
|
||||
}
|
||||
|
||||
// The signed half, which needs two things the unsigned one does not: the code is sign-extended
|
||||
// out of the unsigned carrier channel on the way in, and the decode is max(c / 32767, -1) rather
|
||||
// than the bare division - GL clamps -2^15/32767 up to exactly -1.
|
||||
TEST(WidenImageFormats, SignedNormalizedImageSignExtendsItsCodeAndClampsAtMinusOne) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRgba16SnormLoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, false, true));
|
||||
ASSERT_FALSE(widened.empty());
|
||||
EXPECT_TRUE(Validates(widened));
|
||||
|
||||
const auto afterTypes = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba16ui));
|
||||
EXPECT_EQ(ScalarTypeSpellingOf(widened, afterTypes.front().sampledTypeId), "uint");
|
||||
|
||||
// The sign extension is a shift PAIR, and the arithmetic one is what makes it a sign
|
||||
// extension rather than a zero extension.
|
||||
Bool sawShiftLeft = false;
|
||||
Bool sawArithmeticShiftRight = false;
|
||||
ForEachInstruction(widened, [&](spv::Op opcode, const Uint32*, Uint32) {
|
||||
sawShiftLeft = sawShiftLeft || opcode == spv::Op::OpShiftLeftLogical;
|
||||
sawArithmeticShiftRight = sawArithmeticShiftRight || opcode == spv::Op::OpShiftRightArithmetic;
|
||||
});
|
||||
EXPECT_TRUE(sawShiftLeft);
|
||||
EXPECT_TRUE(sawArithmeticShiftRight)
|
||||
<< "a logical shift right would read every negative code as a large positive one";
|
||||
|
||||
const EsslAttempt after = EmitEssl(widened);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
EXPECT_NE(after.text.find("uimage2D"), String::npos) << after.text;
|
||||
EXPECT_NE(after.text.find("rgba16ui"), String::npos) << after.text;
|
||||
EXPECT_EQ(after.text.find("rgba16_snorm"), String::npos) << after.text;
|
||||
EXPECT_NE(after.text.find("32767.0"), String::npos)
|
||||
<< "the signed-normalized denominator is 2^15 - 1:\n"
|
||||
<< after.text;
|
||||
EXPECT_NE(after.text.find("-1.0"), String::npos)
|
||||
<< "GL clamps the signed decode at -1:\n"
|
||||
<< after.text;
|
||||
}
|
||||
|
||||
// rgb10_a2, whose four channels are 10, 10, 10 and 2 bits: the only entry where one denominator
|
||||
// would be wrong for a channel that IS present, rather than for one the mask discards anyway.
|
||||
TEST(WidenImageFormats, TenTenTenTwoImageTakesAPerChannelDenominator) {
|
||||
const Vector<Uint32> spirv = CompileFragment(kRgb10A2LoadStore);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv));
|
||||
|
||||
Vector<Uint32> widened;
|
||||
ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, false, true));
|
||||
ASSERT_FALSE(widened.empty());
|
||||
EXPECT_TRUE(Validates(widened));
|
||||
|
||||
const auto afterTypes = CollectStorageImageTypes(widened);
|
||||
ASSERT_EQ(afterTypes.size(), 1u);
|
||||
EXPECT_EQ(afterTypes.front().format, static_cast<Uint32>(spv::ImageFormat::Rgba16ui));
|
||||
EXPECT_EQ(ScalarTypeSpellingOf(widened, afterTypes.front().sampledTypeId), "uint");
|
||||
|
||||
const EsslAttempt after = EmitEssl(widened);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
EXPECT_NE(after.text.find("1023.0"), String::npos) << after.text;
|
||||
EXPECT_NE(after.text.find("3.0"), String::npos)
|
||||
<< "the two-bit alpha saturates at 3, not at 1023:\n"
|
||||
<< after.text;
|
||||
EXPECT_EQ(after.text.find("rgb10_a2)"), String::npos) << after.text;
|
||||
}
|
||||
|
||||
@@ -5286,3 +5286,53 @@ TEST_F(TextureTest, ImageWidenedUploadExpandsOneAndTwoChannelDataWithGLsMissingC
|
||||
EXPECT_TRUE(widened.empty());
|
||||
}
|
||||
}
|
||||
|
||||
// The OTHER transfer shape the image widening needs, and the one a channel repack cannot serve:
|
||||
// GL_RGB10_A2UI's shadow is ONE 32-bit word per texel, not four components of the GL_RGBA16UI
|
||||
// carrier's own type. Repacking it as components would take sixteen bytes out of a four-byte texel
|
||||
// and shear the level - which only a LOAD notices, because a store overwrites whatever the upload
|
||||
// got wrong.
|
||||
//
|
||||
// GL_UNSIGNED_INT_2_10_10_10_REV puts the FIRST component in the LOW bits, which is the whole
|
||||
// content of the word "REV" and the single thing this can get backwards, so every field here is a
|
||||
// different value and the boundary codes (0, the 10-bit maximum, the 2-bit maximum) are pinned
|
||||
// exactly rather than compared with a tolerance.
|
||||
TEST_F(TextureTest, ImageWidenedUploadSplitsAPacked2101010RevShadowIntoFourChannelCodes) {
|
||||
using MobileGL::MG_Backend::DirectGLES::TextureImpl::PreparePackedIntWidenedUpload;
|
||||
|
||||
const IntVec3 texelSize(3, 1, 1);
|
||||
// r=1, g=2, b=3, a=1 | r=1023, g=0, b=1023, a=3 | r=0, g=1023, b=0, a=0
|
||||
const Uint32 source[] = {
|
||||
1u | (2u << 10) | (3u << 20) | (1u << 30),
|
||||
1023u | (0u << 10) | (1023u << 20) | (3u << 30),
|
||||
0u | (1023u << 10) | (0u << 20) | (0u << 30),
|
||||
};
|
||||
Vector<Uint8> widened;
|
||||
const auto* result = static_cast<const Uint16*>(
|
||||
PreparePackedIntWidenedUpload(texelSize, source, sizeof(source), widened));
|
||||
ASSERT_NE(result, static_cast<const void*>(source));
|
||||
ASSERT_EQ(widened.size(), 12 * sizeof(Uint16));
|
||||
const Uint16 expected[] = {1, 2, 3, 1, 1023, 0, 1023, 3, 0, 1023, 0, 0};
|
||||
for (SizeT i = 0; i < 12; ++i) {
|
||||
EXPECT_EQ(result[i], expected[i]) << "component " << i;
|
||||
}
|
||||
|
||||
// Sized from the LEVEL, never from the source: the driver reads a full width*height*4 shorts
|
||||
// for the transfer it was handed, so a short source still has to leave a full destination.
|
||||
{
|
||||
Vector<Uint8> shortWidened;
|
||||
const auto* shortResult = static_cast<const Uint16*>(
|
||||
PreparePackedIntWidenedUpload(texelSize, source, sizeof(Uint32), shortWidened));
|
||||
ASSERT_EQ(shortWidened.size(), 12 * sizeof(Uint16));
|
||||
for (SizeT i = 4; i < 12; ++i) {
|
||||
EXPECT_EQ(shortResult[i], 0u) << "component " << i << " past the source must be zero";
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to split.
|
||||
{
|
||||
Vector<Uint8> empty;
|
||||
EXPECT_EQ(PreparePackedIntWidenedUpload(texelSize, nullptr, 0, empty), nullptr);
|
||||
EXPECT_TRUE(empty.empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,19 +51,21 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
};
|
||||
|
||||
// Both backends' fp64 rows end the same way, and the sentence they end with depends on
|
||||
// a config flag rather than on anything either backend probes: the demotion is what
|
||||
// makes doubles work, but GL_ARB_gpu_shader_fp64 promises the PRECISION the demotion
|
||||
// cannot deliver, so the string is opt-in and the row has to say which way it went.
|
||||
// a config flag rather than on anything either backend probes: doubles WORK on every
|
||||
// backend, but GL_ARB_gpu_shader_fp64 additionally promises 64-bit PRECISION, which only
|
||||
// a backend that consumes fp64 natively actually has. The string is opt-in either way -
|
||||
// advertising it is a decision about the whole extension's surface, not just about
|
||||
// precision - so the row has to say which way it went.
|
||||
String AppendFp64AdvertisementNote(String detail) {
|
||||
if (MG_Config::Features.AdvertiseFp64) {
|
||||
return Move(detail) +
|
||||
". GL_ARB_gpu_shader_fp64 IS advertised (MOBILEGL_ADVERTISE_FP64): an application "
|
||||
"that checks the string will believe it has 64-bit precision, and it does not";
|
||||
"that checks the string will believe it has 64-bit precision, which is true only "
|
||||
"where the row above says native";
|
||||
}
|
||||
return Move(detail) +
|
||||
". GL_ARB_gpu_shader_fp64 is not advertised, because the precision it promises is the "
|
||||
"one thing the demotion cannot provide; set MOBILEGL_ADVERTISE_FP64=1 to advertise it "
|
||||
"anyway";
|
||||
". GL_ARB_gpu_shader_fp64 is not advertised by default; set MOBILEGL_ADVERTISE_FP64=1 "
|
||||
"to advertise it anyway";
|
||||
}
|
||||
|
||||
struct ReportBuilder {
|
||||
@@ -2321,27 +2323,35 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"unsupported; a GL_TEXTURE_CUBE_MAP_ARRAY texture gets no image at all, so sampling "
|
||||
"one reads nothing and glFramebufferTextureLayer on one is declined");
|
||||
}
|
||||
// Reported whichever way the device answers, because MobileGL no longer follows the
|
||||
// device here: every 64-bit float is narrowed to 32 bits before any module reaches this
|
||||
// backend (DemoteFloat64Pass), so the Float64 capability is never declared and a device
|
||||
// that HAS the feature gains nothing from it. The device's own answer is still worth
|
||||
// printing - it is the reason the demotion is unconditional.
|
||||
builder.Pass("fp64", AppendFp64AdvertisementNote(
|
||||
format("demoted to fp32 (device shaderFloat64 = {}) - every double / dvec / "
|
||||
"dmat in a shader is narrowed to 32 bits before pipeline creation, so "
|
||||
"such shaders BUILD AND RUN at single precision on every device "
|
||||
"instead of failing to create a shader module on the ones without the "
|
||||
"feature. A block containing a double is re-laid-out for the narrowed "
|
||||
"members, so an application that hard-codes std140 offsets computed "
|
||||
"for doubles must query them instead",
|
||||
features.shaderFloat64 == VK_TRUE ? "supported" : "unsupported")));
|
||||
// MobileGL follows the device here: shaderFloat64 decides whether a module keeps its
|
||||
// 64-bit floats or has them narrowed before pipeline creation (DemoteFloat64Pass). Adreno
|
||||
// and Mali both report VK_FALSE, so the demoted row is what a real phone prints; lavapipe
|
||||
// reports VK_TRUE and gets real doubles.
|
||||
if (features.shaderFloat64 == VK_TRUE) {
|
||||
builder.Pass("fp64", AppendFp64AdvertisementNote(
|
||||
"native (device shaderFloat64 = supported) - every double / dvec / dmat in "
|
||||
"a shader keeps its declared width, blocks keep the layout glslang computed "
|
||||
"for them, and glUniform*d stores 8-byte components. The one exception is a "
|
||||
"VERTEX stage that declares a 64-bit float INPUT: there is no 64-bit vertex "
|
||||
"FETCH here, so such a program is narrowed whole exactly as it would be on a "
|
||||
"device without the feature"));
|
||||
} else {
|
||||
builder.Pass("fp64", AppendFp64AdvertisementNote(
|
||||
"demoted to fp32 (device shaderFloat64 = unsupported) - every double / dvec "
|
||||
"/ dmat in a shader is narrowed to 32 bits before pipeline creation, so such "
|
||||
"shaders BUILD AND RUN at single precision instead of failing to create a "
|
||||
"shader module. A block containing a double is re-laid-out for the narrowed "
|
||||
"members, so an application that hard-codes std140 offsets computed for "
|
||||
"doubles must query them instead"));
|
||||
}
|
||||
builder.Warn("64-bit vertex attributes",
|
||||
"narrowed to float32; there is no 64-bit shader input left to feed after the fp64 "
|
||||
"demotion above, and no VK_FORMAT_R64*_SFLOAT vertex fetch to feed it with on most "
|
||||
"devices anyway. glVertexAttribLFormat succeeds, its state is queryable, and an "
|
||||
"ENABLED 64-bit array IS fetched - the source doubles are deinterleaved into a "
|
||||
"float32 stream at draw, so values outside float32's range or precision are "
|
||||
"rounded rather than exact");
|
||||
"narrowed to float32 on every device, whatever the row above says: there is no "
|
||||
"VK_FORMAT_R64*_SFLOAT vertex fetch here, and the format is chosen from the VAO "
|
||||
"attribute, which does not know what type the shader declared - which is why a "
|
||||
"vertex stage with a 64-bit float INPUT is narrowed whole even where fp64 is native. "
|
||||
"glVertexAttribLFormat succeeds, its state is queryable, and an ENABLED 64-bit array "
|
||||
"IS fetched - the source doubles are deinterleaved into a float32 stream at draw, so "
|
||||
"values outside float32's range or precision are rounded rather than exact");
|
||||
|
||||
Bool shaderDrawParameters = false;
|
||||
if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
|
||||
|
||||
@@ -117,8 +117,14 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// preprocessed text is in the L1 key verbatim, a strictly finer discriminator
|
||||
// than the extension list. (E_GL_ARB_gpu_shader_fp64 is never read by the front
|
||||
// end at all: MOBILEGL_ADVERTISE_FP64 only adds it to the extension STRING the
|
||||
// application queries, and DemoteFloat64Pass runs unconditionally either way, so
|
||||
// fp64 GLSL translates identically with the flag on or off.)
|
||||
// application queries, and glslang parses `double` the same way either way.)
|
||||
// * params.SupportsShaderFloat64, i.e. ConsumesFloat64Natively(). glslang produces
|
||||
// the SAME SPIR-V under it - a `double` parses, reflects and generates as a
|
||||
// 64-bit float regardless - so it is not a front-end input and putting it here
|
||||
// would also cost L1c (the parse-verdict memo, which keys on this fingerprint and
|
||||
// is genuinely independent of it) a false miss per backend. It DOES change what
|
||||
// SanitizeAndOptimizeBinary produces, and L1's payload is post-Sanitize, so it
|
||||
// rides in L1's key as a field of its own; see SpirvTranslationKeyInputs.
|
||||
// * the other ~50 DynamicBackendParameters fields: read by the GL getters and by
|
||||
// the backends, never by the parse, the link or reflection.
|
||||
// * maxComputeWorkGroupInvocations - and ONLY this one; its two former companions
|
||||
@@ -135,6 +141,16 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
Uint64 frontendFingerprint = 0; // set by CaptureCompileEnv()
|
||||
|
||||
Bool HasBackend() const { return backend != BackendType::Unknown; }
|
||||
// Whether the backend this env was captured against can CONSUME a module that still
|
||||
// declares 64-bit floats - the one thing that decides whether the transpile keeps
|
||||
// `double` or narrows it (FlattenFloat64StorageBlockPass + DemoteFloat64Pass).
|
||||
//
|
||||
// The no-backend case answers FALSE, deliberately opposite to IsExtensionAdvertised's
|
||||
// permissive fallback: an extension the frontend cannot gate against is best assumed
|
||||
// present, but a hardware capability nothing has declared must be assumed absent. The
|
||||
// demoted module is the one that works everywhere, so it is what a standalone compile
|
||||
// (an internal shader object, a unit test) gets.
|
||||
Bool ConsumesFloat64Natively() const { return HasBackend() && params.SupportsShaderFloat64; }
|
||||
// Matches the historical rule exactly: with no active backend every extension counts
|
||||
// as advertised, because the frontend then has nothing to gate against.
|
||||
Bool IsExtensionAdvertised(GLExtension extension) const {
|
||||
|
||||
@@ -701,6 +701,57 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The leaf-width test behind ModuleDeclaresFloat64VertexInput, and it is a LEAF
|
||||
// test rather than a shape test on purpose: a `dmat4` input is an OpTypeMatrix of
|
||||
// OpTypeVector of OpTypeFloat 64, and it is as unfetchable as a bare `double`.
|
||||
Bool TypeHoldsFloat64(const spvtools::opt::analysis::Type* type) {
|
||||
if (type == nullptr) return false;
|
||||
if (const auto* scalar = type->AsFloat()) return scalar->width() == 64;
|
||||
if (const auto* vector = type->AsVector()) return TypeHoldsFloat64(vector->element_type());
|
||||
if (const auto* matrix = type->AsMatrix()) return TypeHoldsFloat64(matrix->element_type());
|
||||
if (const auto* array = type->AsArray()) return TypeHoldsFloat64(array->element_type());
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ShaderCompiler::ModuleDeclaresFloat64VertexInput(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) {
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleDeclaresFloat64VertexInput"),
|
||||
spirv.data(), spirv.size());
|
||||
if (!context) {
|
||||
return false;
|
||||
}
|
||||
// Vertex only. Every other stage's inputs come from another stage's outputs, which
|
||||
// MobileGL never re-formats, so a 64-bit varying between two stages is the driver's
|
||||
// business and not this question's.
|
||||
auto entryPoints = context->module()->entry_points();
|
||||
if (entryPoints.begin() == entryPoints.end()) return false;
|
||||
const spvtools::opt::Instruction& entryPoint = *entryPoints.begin();
|
||||
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
|
||||
spv::ExecutionModel::Vertex) {
|
||||
return false;
|
||||
}
|
||||
auto* typeManager = context->get_type_mgr();
|
||||
auto* defUseManager = context->get_def_use_mgr();
|
||||
for (const spvtools::opt::Instruction& variable : context->module()->types_values()) {
|
||||
if (variable.opcode() != spv::Op::OpVariable || variable.NumInOperands() < 1) continue;
|
||||
if (static_cast<spv::StorageClass>(variable.GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Input) {
|
||||
continue;
|
||||
}
|
||||
const spvtools::opt::Instruction* pointerType = defUseManager->GetDef(variable.type_id());
|
||||
if (pointerType == nullptr || pointerType->NumInOperands() < 2) continue;
|
||||
if (TypeHoldsFloat64(typeManager->GetType(pointerType->GetSingleWordInOperand(1)))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ShaderCompiler::ModuleReadsLocatedInput(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) {
|
||||
return false;
|
||||
@@ -751,7 +802,8 @@ namespace MobileGL {
|
||||
bool ShaderCompiler::SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool validateOutput,
|
||||
const bool enableSpirvValidation) {
|
||||
const bool enableSpirvValidation,
|
||||
const bool nativeFloat64) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
|
||||
@@ -788,16 +840,25 @@ namespace MobileGL {
|
||||
RenameBuiltinShadowingFunctionsPass::CreateRenameBuiltinShadowingFunctionsPass());
|
||||
optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass());
|
||||
optimizer.RegisterPass(DecomposeWorkgroupVec3Pass::CreateDecomposeWorkgroupVec3Pass());
|
||||
// No mobile GPU has 64-bit floats: Adreno and Mali both report shaderFloat64 ==
|
||||
// VK_FALSE, and ESSL has no fp64 type for SPIRV-Cross to emit. Demoting here - in
|
||||
// the one chain every module goes through, on both backends, at link - is what
|
||||
// makes `double` compile at all, and makes it behave the SAME everywhere, which
|
||||
// matters because the GL frontend's uniform storage cannot be per-backend: the
|
||||
// glUniform*d shadow narrows to float unconditionally to match this. Runs last so
|
||||
// no earlier pass ever has to reason about a width it will not see in the output;
|
||||
// in particular it runs before the backends' PackDoubleVertexInputsPass, whose
|
||||
// OpBitcast this one would otherwise decline on. Costs one types_values() walk on
|
||||
// the overwhelming majority of modules, which declare no 64-bit float at all.
|
||||
// The fp64 tail, and the ONE part of this chain that is not the same on every
|
||||
// backend. Both passes are skipped when the backend can consume Float64 itself
|
||||
// (`nativeFloat64`, i.e. VkPhysicalDeviceFeatures::shaderFloat64 on DirectVulkan):
|
||||
// there is nothing to emulate then, and narrowing would only throw away precision
|
||||
// the driver was willing to give. That is DirectVulkan-on-lavapipe today and
|
||||
// nothing else - Adreno and Mali both report shaderFloat64 == VK_FALSE, and
|
||||
// DirectGLES can never qualify because GLSL ES has no fp64 type for SPIRV-Cross to
|
||||
// emit at all, so on every real mobile device this branch is not taken and the two
|
||||
// passes run exactly as they always have.
|
||||
//
|
||||
// Demoting here - in the one chain every module goes through, at link - is what
|
||||
// makes `double` compile at all where the hardware has none, and makes it behave
|
||||
// the SAME across both backends of such a device, which matters because the GL
|
||||
// frontend's uniform storage is per PROGRAM rather than per call: the glUniform*d
|
||||
// shadow narrows to float to match this. Runs last so no earlier pass ever has to
|
||||
// reason about a width it will not see in the output; in particular it runs before
|
||||
// the backends' PackDoubleVertexInputsPass, whose OpBitcast this one would
|
||||
// otherwise decline on. Costs one types_values() walk on the overwhelming majority
|
||||
// of modules, which declare no 64-bit float at all.
|
||||
// ...but demoting a double that lives in a SHADER STORAGE BLOCK also repacks that
|
||||
// block, and the bytes an application put in the buffer do not move with it. This
|
||||
// runs first and takes those blocks out of the demotion's hands: each becomes a
|
||||
@@ -806,10 +867,16 @@ namespace MobileGL {
|
||||
// and only the VALUES narrow. Gated on a block actually holding a 64-bit float, so
|
||||
// every other module pays one types_values() walk and nothing else, and it declines
|
||||
// (leaving the block for the demotion to handle the old way) on any shape it cannot
|
||||
// re-address exactly. See FlattenFloat64StorageBlockPass.h.
|
||||
optimizer.RegisterPass(
|
||||
FlattenFloat64StorageBlockPass::CreateFlattenFloat64StorageBlockPass());
|
||||
optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass());
|
||||
// re-address exactly. See FlattenFloat64StorageBlockPass.h. It is skipped with the
|
||||
// demotion rather than kept: its whole purpose is to preserve the byte layout ACROSS
|
||||
// a narrowing that is no longer happening, and flattening a block a native driver
|
||||
// would have laid out correctly by itself only costs the shader its index
|
||||
// arithmetic.
|
||||
if (!nativeFloat64) {
|
||||
optimizer.RegisterPass(
|
||||
FlattenFloat64StorageBlockPass::CreateFlattenFloat64StorageBlockPass());
|
||||
optimizer.RegisterPass(DemoteFloat64Pass::CreateDemoteFloat64Pass());
|
||||
}
|
||||
|
||||
return RunOptimizerChecked("SanitizeAndOptimizeBinary", optimizer, inputBinary,
|
||||
outputBinary, validateOutput, enableSpirvValidation);
|
||||
@@ -957,6 +1024,16 @@ namespace MobileGL {
|
||||
return WidenImageFormatsPass::ImageFormatChannelCount(glInternalFormat);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
|
||||
bool& outSignedNormalized) {
|
||||
return WidenImageFormatsPass::NormalizedImageCarrierCodes(glInternalFormat, outChannelMax,
|
||||
outSignedNormalized);
|
||||
}
|
||||
|
||||
Uint ShaderCompiler::SplitCoreEsslBufferImageFormat(Uint glInternalFormat) {
|
||||
return WidenImageFormatsPass::SplitCoreEsslBufferImageFormat(glInternalFormat);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector<Uint32>& inputBinary,
|
||||
const std::set<String>& blockNames,
|
||||
std::set<String>& flattenedBlockNames,
|
||||
|
||||
@@ -23,10 +23,22 @@ namespace MobileGL {
|
||||
static Result<SharedPtr<glslang::TShader>> CompileShader(const ShaderAttrib& attrib);
|
||||
static Result<SharedPtr<glslang::TProgram>> LinkProgram(const ProgramAttrib& attrib);
|
||||
static Result<Vector<Vector<unsigned>>> GetSpirvBinaryFromProgram(const ProgramBinaryAttrib& attrib);
|
||||
// `nativeFloat64` is the caller's FINAL verdict, not a capability read: true means
|
||||
// the two fp64 passes at the tail of the chain are skipped and real doubles reach
|
||||
// the driver. False - which is DirectGLES always, every mobile device, and the
|
||||
// no-backend default - runs the chain exactly as it always has. It is the ONE
|
||||
// argument of this function that changes the output bytes, which is why it is
|
||||
// also a field of the L1 memo's key.
|
||||
//
|
||||
// Production sets it in ProgramSpirvTask::GenerateSpirv, which takes the verdict
|
||||
// for the WHOLE program (CompileEnv::ConsumesFloat64Natively() minus the
|
||||
// 64-bit-vertex-input exception) before touching any module. Do not re-derive it
|
||||
// per module: the global UBO is one buffer every stage reads.
|
||||
static bool SanitizeAndOptimizeBinary(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool validateOutput = true,
|
||||
bool enableSpirvValidation = false);
|
||||
bool enableSpirvValidation = false,
|
||||
bool nativeFloat64 = false);
|
||||
// Demotes DrawIndex/BaseInstance/BaseVertex builtins to plain Private globals
|
||||
// (mg_DrawID/mg_BaseInstance/mg_BaseVertex) so SPIRV-Cross can emit ESSL.
|
||||
// Only for backends without native draw-parameter support (DirectGLES).
|
||||
@@ -286,6 +298,17 @@ namespace MobileGL {
|
||||
// Channels a GL image internal format really has (1-4), 0 when it is not one of
|
||||
// the forty image formats.
|
||||
static Uint ImageFormatChannelCount(Uint glInternalFormat);
|
||||
// Whether the carrier holds the format's channels as the INTEGER CODES of a
|
||||
// normalized value, and the largest code each channel can hold. See
|
||||
// WidenImageFormatsPass::NormalizedImageCarrierCodes - DirectGLES needs it for
|
||||
// both halves of the transfer, which no longer share the frontend format's
|
||||
// component class with the ES storage.
|
||||
static bool NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
|
||||
bool& outSignedNormalized);
|
||||
// The single-channel core format a non-core BUFFER image is SPLIT into, or 0. See
|
||||
// WidenImageFormatsPass::SplitCoreEsslBufferImageFormat - DirectGLES asks it for
|
||||
// glTexBuffer's internal format and for glBindImageTexture's.
|
||||
static Uint SplitCoreEsslBufferImageFormat(Uint glInternalFormat);
|
||||
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
@@ -432,6 +455,18 @@ namespace MobileGL {
|
||||
// what the backends report: no mobile driver can build such a module.
|
||||
static Bool ModuleDeclaresFloat64(const Vector<Uint32>& spirv);
|
||||
|
||||
// True when the module is a VERTEX stage that declares a 64-bit float INPUT
|
||||
// variable - `in double`, `in dvec2`, `in dmat3` and so on.
|
||||
//
|
||||
// Asked only on a backend with native fp64, and it is what keeps that backend's
|
||||
// vertex path consistent. No backend here can FETCH 64 bits (VK_FORMAT_R64*_SFLOAT
|
||||
// is optional and lavapipe advertises none of them), and the format is chosen from
|
||||
// the VAO attribute, which does not know what the shader declared - so a module
|
||||
// that keeps a Float64 input would be fed a narrowed float32 stream, or a packed
|
||||
// uint pair with no matching format. Such a module is demoted WHOLE instead, which
|
||||
// is exactly what every other backend does to it.
|
||||
static Bool ModuleDeclaresFloat64VertexInput(const Vector<Uint32>& spirv);
|
||||
|
||||
// True when the module declares an Input variable carrying a Location - i.e. a
|
||||
// user-defined varying or a per-patch input, as opposed to a built-in.
|
||||
//
|
||||
|
||||
@@ -29,6 +29,16 @@ namespace MobileGL {
|
||||
// Espryt path never even reaches the driver. Demotion is what makes `double` in an
|
||||
// application's GLSL compile and run everywhere, at fp32 precision.
|
||||
//
|
||||
// WHEN IT RUNS AT ALL. This pass is CAPABILITY-GATED at its one production caller,
|
||||
// ShaderCompiler::SanitizeAndOptimizeBinary: a backend that can consume Float64 itself
|
||||
// (DynamicBackendParameters::SupportsShaderFloat64, i.e. shaderFloat64 on DirectVulkan
|
||||
// - lavapipe today and nothing else) skips it, and the module keeps its doubles.
|
||||
// DirectGLES can never qualify, and neither can any real mobile device, so everything
|
||||
// below still describes what happens there - which is everywhere that ships. The one
|
||||
// exception that survives the capability: a VERTEX stage declaring a 64-bit float
|
||||
// INPUT demotes the whole program regardless, because no backend here can FETCH 64
|
||||
// bits (see ProgramSpirvTask::GenerateSpirv).
|
||||
//
|
||||
// BLOCK LAYOUT IS RE-DERIVED, NOT PRESERVED, and that was not the first choice - see
|
||||
// BlockRelayout in the .cpp for the measurement that forced it. Preserving the 64-bit
|
||||
// offsets (float + 4 bytes of padding in each slot) keeps the application's byte layout
|
||||
@@ -67,9 +77,11 @@ namespace MobileGL {
|
||||
// Index 0 of the same case PASSES by accident, for the same reason - writing 0.0f into
|
||||
// the low half of 1.0 leaves it unchanged - so a partial pass here is not progress.
|
||||
// Fixing it means carrying a double in the DEFAULT UNIFORM block without re-deriving
|
||||
// its layout, and that block's routing is built by reflecting the module this pass
|
||||
// produces, so the representation change ripples into every glUniform*d. Deliberately
|
||||
// not attempted. compute_shader.fp64-case2 passes today and any attempt has to keep it
|
||||
// its layout - which is precisely what the capability gate now does where the backend
|
||||
// allows it: fp64-case1 PASSES on DirectVulkan/lavapipe (measured) and still fails on
|
||||
// Espryt and on every device without shaderFloat64, where this pass runs. There is no
|
||||
// fix for the demoted path itself; the value simply does not fit.
|
||||
// compute_shader.fp64-case2 passes in both regimes and any attempt has to keep it
|
||||
// green.
|
||||
//
|
||||
// SHADER STORAGE BLOCKS ARE NO LONGER IN THAT LIST, and the two cases that used to be
|
||||
|
||||
@@ -18,7 +18,13 @@ namespace MobileGL {
|
||||
// Rewrites a SHADER STORAGE BLOCK that contains a 64-bit float into a flat
|
||||
// `uint` word array, and turns every access to it into address arithmetic over
|
||||
// that array. The application's byte layout survives exactly; the VALUES are
|
||||
// still narrowed to 32-bit floats, because that is all any target here has.
|
||||
// still narrowed to 32-bit floats, because that is all the target has.
|
||||
//
|
||||
// Registered ONLY on the demoting path, immediately before DemoteFloat64Pass, and
|
||||
// capability-gated with it (ShaderCompiler::SanitizeAndOptimizeBinary). Where the
|
||||
// backend consumes 64-bit floats itself there is no narrowing for this to preserve a
|
||||
// layout across, and flattening a block the driver would have laid out correctly by
|
||||
// itself would only cost the shader its index arithmetic.
|
||||
//
|
||||
// WHY THIS EXISTS. DemoteFloat64Pass rewrites `double` to `float` in place and
|
||||
// lets SPIRV-Cross re-derive the block's packing from the declared types, because
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -65,12 +65,18 @@ namespace MobileGL {
|
||||
// the SPIRV-Cross throw takes the whole stage, every image uniform declared beside it
|
||||
// included.
|
||||
//
|
||||
// The other EIGHT (rgb10_a2, rgb10_a2ui, rgba16, rg16, r16, rgba16_snorm, rg16_snorm,
|
||||
// r16_snorm) are deliberately NOT widened here: core ESSL has no 16-bit normalized
|
||||
// format at all and no 10-bit one, so every carrier for them either loses range or
|
||||
// changes the component TYPE the texture a `sampler2D` would read presents. They keep
|
||||
// the honest "no GLSL ES spelling" diagnostic instead of silently changing an
|
||||
// application's numeric domain.
|
||||
// rgb10_a2ui takes rgba16ui for a simpler reason still: its channels are 10, 10, 10 and
|
||||
// 2 bits of UNSIGNED INTEGER, and rgba16ui gives each of them sixteen. Same component
|
||||
// type, same channel COUNT, every value representable - so no access is rewritten at
|
||||
// all, and only the TRANSFER differs (its shadow is one packed 32-bit word per texel,
|
||||
// which the upload splits into four shorts).
|
||||
//
|
||||
// The other SEVEN (rgb10_a2, rgba16, rg16, r16, rgba16_snorm, rg16_snorm, r16_snorm)
|
||||
// are deliberately NOT widened here: core ESSL has no 16-bit normalized format at all
|
||||
// and no 10-bit one, so every carrier for them either loses range or changes the
|
||||
// component TYPE the texture a `sampler2D` would read presents. They keep the honest
|
||||
// "no GLSL ES spelling" diagnostic instead of silently changing an application's
|
||||
// numeric domain.
|
||||
//
|
||||
// MUST MOVE WITH THE OTHER TWO LAYERS. The widening is not a shader-local rewrite: the
|
||||
// ES texture behind the image has to be allocated in the carrier format too, and
|
||||
@@ -130,6 +136,31 @@ namespace MobileGL {
|
||||
// forty image formats. The count the widened accesses are masked back to.
|
||||
static Uint ImageFormatChannelCount(Uint glInternalFormat);
|
||||
|
||||
// Whether the carrier holds this format's channels as the INTEGER CODES of a
|
||||
// NORMALIZED value rather than as the values themselves - true for the seven
|
||||
// 16-bit and 10-bit normalized formats and nothing else. `outChannelMax` takes the
|
||||
// largest code each channel can hold (2^b - 1 unsigned, 2^(b-1) - 1 signed), which
|
||||
// is the denominator of GL 4.6 2.3.5 for that channel; `outSignedNormalized` says
|
||||
// which of the two conversions applies.
|
||||
//
|
||||
// DirectGLES asks this on both sides of the transfer: the upload pads a missing
|
||||
// alpha with outChannelMax[3] rather than the transfer type's own 1 (through a
|
||||
// uint carrier "one" is the saturated CODE, not the integer one), and
|
||||
// glGetTexImage divides the codes back out, because the ES storage is an integer
|
||||
// texture the client still expects to read as floats.
|
||||
static bool NormalizedImageCarrierCodes(Uint glInternalFormat, Uint32 (&outChannelMax)[4],
|
||||
bool& outSignedNormalized);
|
||||
|
||||
// The core-ESSL single-channel format a non-core BUFFER image is SPLIT into, or 0
|
||||
// when the format needs no split or has no core single-channel base. A buffer
|
||||
// image cannot be WIDENED - its texels are the application's buffer object, which
|
||||
// has no room to restride - but rg32f over N texels and r32f over 2N texels
|
||||
// describe exactly the same bytes, so the shader reads and writes each component
|
||||
// by itself at 2i and 2i+1 instead. DirectGLES asks this for glTexBuffer's
|
||||
// internal format and for glBindImageTexture's, which have to name the same view
|
||||
// the shader addresses.
|
||||
static Uint SplitCoreEsslBufferImageFormat(Uint glInternalFormat);
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateWidenImageFormatsPass(
|
||||
bool onlyFormatsSpirvCrossRefusesToPrint = false);
|
||||
|
||||
|
||||
@@ -30,7 +30,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// key (that map is an output of mapIO, not an input to it), and L1c's PAYLOAD gained
|
||||
// the explicit uniform locations - so a blob written under 3 describes a differently
|
||||
// shaped answer at both levels even where the bytes would have matched.
|
||||
constexpr Uint32 kKeyLayoutVersion = 4u;
|
||||
// 5: L1 gained nativeFloat64. SanitizeAndOptimizeBinary's fp64 tail is now capability-
|
||||
// gated, so one L1 key shape can describe two materially different module sets (real
|
||||
// doubles vs demoted-and-flattened) and a blob written under 4 says nothing about
|
||||
// which one it holds.
|
||||
constexpr Uint32 kKeyLayoutVersion = 5u;
|
||||
|
||||
// The repo's existing cache epoch (MG_Config::CacheVersion, the seed
|
||||
// ProgramFactory::ComputeHash uses). Strictly redundant for an in-memory
|
||||
@@ -123,6 +127,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
builder.Value(inputs.frontendFingerprint);
|
||||
builder.Value(inputs.shaderCompileFlags);
|
||||
builder.Value(static_cast<Uint8>(inputs.enableSpirvValidation));
|
||||
builder.Value(static_cast<Uint8>(inputs.nativeFloat64));
|
||||
builder.Value(static_cast<Uint64>(inputs.stages.size()));
|
||||
for (const auto& stage : inputs.stages) {
|
||||
builder.Value(static_cast<Uint32>(stage.type));
|
||||
|
||||
@@ -341,22 +341,26 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
//
|
||||
// The cached artifact is the module AFTER SanitizeAndOptimizeBinary, not the
|
||||
// raw GlslangToSpv output. That is a deliberate choice and it is safe:
|
||||
// SanitizeAndOptimizeBinary is a fixed 11-pass spirv-opt chain with no
|
||||
// arguments but the module, and its two remaining parameters (`validateOutput`,
|
||||
// `enableSpirvValidation`) only decide whether the OUTPUT is handed to the
|
||||
// validator and logged - RunOptimizerChecked runs the optimizer first and
|
||||
// identically either way. Nothing between GlslangToSpv and Sanitize reads
|
||||
// backend state. So caching after Sanitize saves the 96 us/stage the chain
|
||||
// costs on top of the 40 us GlslangToSpv, and gives the backends exactly the
|
||||
// bytes they would have got.
|
||||
// SanitizeAndOptimizeBinary is a fixed spirv-opt chain whose only
|
||||
// output-changing argument is `nativeFloat64` (below), and whose two other
|
||||
// parameters (`validateOutput`, `enableSpirvValidation`) only decide whether
|
||||
// the OUTPUT is handed to the validator and logged - RunOptimizerChecked runs
|
||||
// the optimizer first and identically either way. Nothing between GlslangToSpv
|
||||
// and Sanitize reads backend state. So caching after Sanitize saves the 96
|
||||
// us/stage the chain costs on top of the 40 us GlslangToSpv, and gives the
|
||||
// backends exactly the bytes they would have got.
|
||||
//
|
||||
// L1 IS BACKEND-AGNOSTIC BY CONTRACT. Two contexts on different GPUs compiling
|
||||
// the same GLSL share one L1 entry: nothing that merely steers a BACKEND
|
||||
// transpile (backend identity, GLES/Vulkan capability bits, driver extension
|
||||
// strings, GPU vendor) is allowed in this key - all of that lives in L2's key,
|
||||
// where it belongs. What IS here is the subset of the environment that changes
|
||||
// what glslang itself produces; see CompileEnv::frontendFingerprint for the
|
||||
// field-by-field classification and the evidence behind each call.
|
||||
// L1 IS BACKEND-AGNOSTIC BY CONTRACT, WITH EXACTLY ONE DECLARED EXCEPTION.
|
||||
// Two contexts on different GPUs compiling the same GLSL share one L1 entry:
|
||||
// nothing that merely steers a BACKEND transpile (backend identity, GLES/Vulkan
|
||||
// capability bits, driver extension strings, GPU vendor) is allowed in this key
|
||||
// - all of that lives in L2's key, where it belongs. What IS here is the subset
|
||||
// of the environment that changes what glslang itself produces (see
|
||||
// CompileEnv::frontendFingerprint for the field-by-field classification), PLUS
|
||||
// `nativeFloat64`, the one capability bit that reaches INSIDE
|
||||
// SanitizeAndOptimizeBinary and therefore changes the cached bytes themselves.
|
||||
// A capability bit belongs in this key if and only if it does that; anything
|
||||
// that only changes what a backend does with the finished module still does not.
|
||||
//
|
||||
// WHAT IS IN THE KEY (each one is an input that can change the modules):
|
||||
// * CompileEnv::frontendFingerprint - the glslang resource limits
|
||||
@@ -376,7 +380,16 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// * the ShaderCompileBits the parse ran under (always 0 in production; in
|
||||
// the key so a future non-zero value cannot alias);
|
||||
// * the SPIR-V validation switch (byte-identical output either way, but it
|
||||
// costs one byte to be sure).
|
||||
// costs one byte to be sure);
|
||||
// * nativeFloat64 - CompileEnv::ConsumesFloat64Natively(). The fp64 tail of
|
||||
// SanitizeAndOptimizeBinary (FlattenFloat64StorageBlockPass +
|
||||
// DemoteFloat64Pass) is skipped when the backend can build a pipeline from
|
||||
// a module that still declares OpCapability Float64, so the SAME GLSL
|
||||
// produces MATERIALLY DIFFERENT modules under the two answers - one with
|
||||
// real doubles, one narrowed to 32 bits with its storage blocks flattened.
|
||||
// Not folded into frontendFingerprint on purpose: glslang produces the same
|
||||
// thing either way, so it is not a front-end input, and L1c shares that
|
||||
// fingerprint and would take a false miss per backend for nothing.
|
||||
//
|
||||
// The key is a PROGRAM-level key, not a per-stage one, and that is forced:
|
||||
// glslang's mapIO resolves a fragment stage's input Locations against the
|
||||
@@ -398,6 +411,9 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
const UnorderedMap<String, Uint>* explicitFragmentOutIndices = nullptr;
|
||||
Uint32 shaderCompileFlags = 0;
|
||||
Bool enableSpirvValidation = false;
|
||||
// CompileEnv::ConsumesFloat64Natively() - the fp64 tail of the sanitize chain. The
|
||||
// one backend capability bit in this key; see the note above for why it has to be.
|
||||
Bool nativeFloat64 = false;
|
||||
// ---- inputs that only matter because the PAYLOAD now carries the reflection ----
|
||||
// When the payload was SPIR-V alone these were provably irrelevant: transform
|
||||
// feedback is resolved by READING the linked intermediates and never writes an XFB
|
||||
|
||||
Reference in New Issue
Block a user