mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ae3245a0d | ||
|
|
7e048fc2bf | ||
|
|
83cdfd6bdd | ||
|
|
1c76f886cf | ||
|
|
a2e109beff | ||
|
|
63f0756644 | ||
|
|
450215d12c | ||
|
|
3a9e520170 | ||
|
|
d2996ba1cf |
@@ -194,9 +194,6 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
|
||||
@@ -80,11 +80,6 @@ namespace MobileGL::MG_Config {
|
||||
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
|
||||
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
||||
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_QUIRK_CLIP_DISTANCE: overrides the DirectGLES quirk that lowers
|
||||
// gl_ClipDistance for Adreno's ESSL compiler (shadow Private arrays with
|
||||
// constant-index builtin flushes, dynamic-index gl_in copy loop, redeclaration
|
||||
// strip, and const struct-array LUT splitting). Auto detects Qualcomm.
|
||||
QuirkOverride ClipDistanceQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
||||
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
||||
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
||||
|
||||
@@ -135,7 +135,6 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
||||
features.ClipDistanceQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_CLIP_DISTANCE");
|
||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
|
||||
@@ -449,20 +449,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Textures attached only to the READ framebuffer (blit / ReadPixels sources) need
|
||||
// their content synced too, or the backend reads stale texel data.
|
||||
const auto& readFBO =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||
if (readFBO && readFBO != currentFBO) {
|
||||
for (const auto& attachment : readFBO->GetAllAttachmentObjects()) {
|
||||
if (!attachment.IsTexture()) continue;
|
||||
auto& textureObject = attachment.GetTexture();
|
||||
if (textureObject) {
|
||||
SyncTextureObjectToBackend(textureObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Bool SupportsLayeredImageBinding(TextureTarget target) {
|
||||
|
||||
@@ -1262,31 +1262,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const auto& allAttributes = stateVAOObject->GetAllAttributes();
|
||||
for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) {
|
||||
const auto& attrib = allAttributes[attribIndex];
|
||||
const Uint32 attribBit = 1u << attribIndex;
|
||||
|
||||
// An enabled attrib with neither a buffer object nor a client pointer has no
|
||||
// source; GL tolerates the state (only draws consuming it are undefined), but
|
||||
// Adreno's ES driver memcpys the "client array" from address 0 at draw time
|
||||
// (SIGSEGV). Keep such attribs disabled on the backend VAO and re-enable them
|
||||
// the moment they gain a source - the mask-vs-current compare below triggers
|
||||
// the enable even when only the Buffer/Format versions changed.
|
||||
const Bool unsourceable = attrib.Enabled && !attrib.Buffer && attrib.Offset == 0;
|
||||
const Bool wasForceDisabled = (m_forceDisabledAttribsMask & attribBit) != 0;
|
||||
|
||||
Bool needsSyncSwitch = allAttributeVersions[attribIndex].SwitchVersion !=
|
||||
m_syncedAttributeVersions[attribIndex].SwitchVersion;
|
||||
if (needsSyncSwitch || unsourceable != wasForceDisabled) {
|
||||
if (attrib.Enabled && !unsourceable) {
|
||||
if (needsSyncSwitch) {
|
||||
if (attrib.Enabled) {
|
||||
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
|
||||
} else {
|
||||
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
||||
}
|
||||
}
|
||||
if (unsourceable) {
|
||||
m_forceDisabledAttribsMask |= attribBit;
|
||||
} else {
|
||||
m_forceDisabledAttribsMask &= ~attribBit;
|
||||
}
|
||||
|
||||
Bool needsSyncFormat = allAttributeVersions[attribIndex].FormatVersion !=
|
||||
m_syncedAttributeVersions[attribIndex].FormatVersion;
|
||||
@@ -1294,17 +1278,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_syncedAttributeVersions[attribIndex].BufferVersion;
|
||||
if (!needsSyncFormat && !needsSyncBuffer) continue;
|
||||
|
||||
if (unsourceable) continue;
|
||||
|
||||
// Client-side array with a non-null pointer: the pointer is uploaded and applied
|
||||
// per draw by SyncClientSideAttributesForDrawArrays.
|
||||
if (!attrib.Buffer) continue;
|
||||
|
||||
if (!BindAttributeBuffer(attrib)) {
|
||||
if (attrib.Enabled) {
|
||||
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
||||
m_forceDisabledAttribsMask |= attribBit;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3291,19 +3265,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv();
|
||||
|
||||
// Adreno's ESSL compiler mishandles gl_ClipDistance (rejects redeclarations,
|
||||
// miscompiles non-constant-index writes and constant-index gl_in element reads,
|
||||
// crashes on whole-array gl_in reads) and cannot dynamically index the global
|
||||
// const struct[] LUTs SPIRV-Cross likes to emit. Gate the workarounds to
|
||||
// Qualcomm; MOBILEGL_QUIRK_CLIP_DISTANCE overrides the device detection.
|
||||
const MG_Config::QuirkOverride clipDistanceQuirkOverride =
|
||||
MG_Config::Features.ClipDistanceQuirk;
|
||||
const Bool applyClipDistanceQuirk =
|
||||
clipDistanceQuirkOverride == MG_Config::QuirkOverride::ForceOn ||
|
||||
(clipDistanceQuirkOverride == MG_Config::QuirkOverride::Auto &&
|
||||
pActiveBackendObject &&
|
||||
pActiveBackendObject->GetDynamicParameters().GpuVendor == GpuVendorKind::Qualcomm);
|
||||
|
||||
for (int index = 0; index < attachedShaders.size(); ++index) {
|
||||
auto& shader = attachedShaders[index];
|
||||
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage());
|
||||
@@ -3326,63 +3287,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
effectiveSpirv = &loweredSpirv;
|
||||
}
|
||||
|
||||
// GL 3.3 only promises undefined *values* for out-of-bounds array indexing, but
|
||||
// Adreno's ESSL compiler constant-folds a provably out-of-bounds local-array
|
||||
// index into poison that corrupts the whole shader's output. Clamp every
|
||||
// access-chain index to its declared bounds before transpiling.
|
||||
Vector<unsigned int> clampedSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ClampAccessChainIndicesForEssl(*effectiveSpirv,
|
||||
clampedSpirv) &&
|
||||
!clampedSpirv.empty()) {
|
||||
effectiveSpirv = &clampedSpirv;
|
||||
} else {
|
||||
MGLOG_W("ClampAccessChainIndicesForEssl failed, continuing with unclamped SPIR-V.");
|
||||
}
|
||||
|
||||
// SPIRV-Cross emulates 1D samplers as 2D for ES: it widens texelFetch coordinates
|
||||
// to ivec2 but keeps the ConstOffset operand scalar, which is not a valid ESSL
|
||||
// texelFetchOffset overload (Adreno rejects it). Fold the constant offset into the
|
||||
// coordinate instead (texelFetchOffset(t,P,l,o) == texelFetch(t,P+o,l)).
|
||||
Vector<unsigned int> foldedOffsetSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::FoldConstOffsetFor1DFetchForEssl(
|
||||
*effectiveSpirv, foldedOffsetSpirv) &&
|
||||
!foldedOffsetSpirv.empty()) {
|
||||
effectiveSpirv = &foldedOffsetSpirv;
|
||||
} else {
|
||||
MGLOG_W("FoldConstOffsetFor1DFetchForEssl failed, continuing with unfolded SPIR-V.");
|
||||
}
|
||||
|
||||
// Adreno quirk: shadow gl_ClipDistance in Private arrays so the transpiled
|
||||
// ESSL only writes the builtin with literal constant indices (flushed before
|
||||
// EmitVertex/return) and only reads gl_in clip distances through dynamic loop
|
||||
// indices - the shapes this driver compiles correctly. Must run after the
|
||||
// access-chain clamp above so the flush indices stay literal constants.
|
||||
Vector<unsigned int> clipDistanceSpirv;
|
||||
if (applyClipDistanceQuirk &&
|
||||
(glShaderType == GL_VERTEX_SHADER || glShaderType == GL_GEOMETRY_SHADER)) {
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerClipDistanceForEssl(
|
||||
*effectiveSpirv, clipDistanceSpirv) &&
|
||||
!clipDistanceSpirv.empty()) {
|
||||
effectiveSpirv = &clipDistanceSpirv;
|
||||
} else {
|
||||
MGLOG_W("LowerClipDistanceForEssl failed, continuing with unlowered SPIR-V.");
|
||||
}
|
||||
}
|
||||
|
||||
// Adreno quirk: split single constant-composite stores of struct arrays so
|
||||
// SPIRV-Cross does not promote them to global const struct[] LUTs, which this
|
||||
// driver cannot dynamically index ("Cannot offset into the structure").
|
||||
Vector<unsigned int> structLutSpirv;
|
||||
if (applyClipDistanceQuirk) {
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::DefeatConstStructArrayLutForEssl(
|
||||
*effectiveSpirv, structLutSpirv) &&
|
||||
!structLutSpirv.empty()) {
|
||||
effectiveSpirv = &structLutSpirv;
|
||||
} else {
|
||||
MGLOG_W("DefeatConstStructArrayLutForEssl failed, continuing with unsplit SPIR-V.");
|
||||
}
|
||||
}
|
||||
|
||||
// ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints
|
||||
// a RelaxedPrecision member as explicit "mediump" in the vertex stage and as
|
||||
// UNQUALIFIED (mediump-by-default) in the fragment stage; after
|
||||
@@ -3441,12 +3345,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
|
||||
source = RemoveLayoutBinding(source);
|
||||
if (applyClipDistanceQuirk) {
|
||||
// Adreno rejects the gl_ClipDistance redeclaration SPIRV-Cross still emits
|
||||
// ("reserved built-in name") but accepts plain usage with
|
||||
// GL_EXT_clip_cull_distance required; drop the line, keep the #extension.
|
||||
source = RemoveClipDistanceRedeclaration(source);
|
||||
}
|
||||
source = ProcessOutColorLocations(source);
|
||||
source = ForceFlatIntegerVaryings(source, glShaderType);
|
||||
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
|
||||
|
||||
@@ -258,11 +258,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
private:
|
||||
Uint m_backendVAOId = 0;
|
||||
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
||||
// Attribs the frontend has Enabled but that have no source at all (no buffer object
|
||||
// and NULL client pointer). GL keeps such attribs latently enabled, but Adreno's ES
|
||||
// driver treats them as client arrays and memcpys from address 0 at draw time
|
||||
// (SIGSEGV), so they are kept disabled on the backend VAO until they gain a source.
|
||||
Uint32 m_forceDisabledAttribsMask = 0;
|
||||
Bool m_isInitialized = false;
|
||||
Uint16 m_syncedIndexBufferVersion = 0;
|
||||
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
|
||||
|
||||
@@ -342,41 +342,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
String RemoveClipDistanceRedeclaration(const String& glslCode) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// Adreno rejects any redeclaration of gl_ClipDistance/gl_CullDistance ("reserved
|
||||
// built-in name") even with GL_EXT_clip_cull_distance required, but accepts plain
|
||||
// usage of the builtin. Drop the desktop-style redeclaration line SPIRV-Cross
|
||||
// prints; the "#extension GL_EXT_clip_cull_distance : require" line stays.
|
||||
static const std::regex redeclarationRegex(
|
||||
R"(^\s*(?:out|in)\s+(?:(?:high|medium|low)p\s+)?float\s+gl_(?:Clip|Cull)Distance\[[0-9]+\];\s*$)");
|
||||
|
||||
String result;
|
||||
result.reserve(glslCode.size());
|
||||
SizeT lineStart = 0;
|
||||
Bool firstLine = true;
|
||||
while (lineStart <= glslCode.size()) {
|
||||
SizeT lineEnd = glslCode.find('\n', lineStart);
|
||||
const Bool lastLine = lineEnd == String::npos;
|
||||
String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart);
|
||||
|
||||
if (!std::regex_match(line, redeclarationRegex)) {
|
||||
if (!firstLine) {
|
||||
result += '\n';
|
||||
}
|
||||
result += line;
|
||||
firstLine = false;
|
||||
}
|
||||
if (lastLine) {
|
||||
break;
|
||||
}
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
|
||||
@@ -105,7 +105,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint32 unormOutputMask);
|
||||
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
|
||||
String RemoveLayoutBinding(const String& glslCode);
|
||||
String RemoveClipDistanceRedeclaration(const String& glslCode);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
|
||||
@@ -1910,8 +1910,10 @@ void main() {
|
||||
case VK_FORMAT_R16G16_UNORM: out = {ReadbackSourceClass::Float, 2, 16}; return true;
|
||||
case VK_FORMAT_R16G16B16A16_UNORM: out = {ReadbackSourceClass::Float, 4, 16}; return true;
|
||||
// --- SRGB (decode to linear like GL readback of sRGB textures) ---
|
||||
case VK_FORMAT_R8G8B8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, true}; return true;
|
||||
case VK_FORMAT_B8G8R8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, true, true}; return true;
|
||||
// GL GetTexImage/ReadPixels of sRGB textures return the raw sRGB-encoded
|
||||
// bytes (GL 3.3 has no FRAMEBUFFER_SRGB read decode) - do NOT linearize.
|
||||
case VK_FORMAT_R8G8B8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8}; return true;
|
||||
case VK_FORMAT_B8G8R8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, false, true}; return true;
|
||||
// --- SNORM ---
|
||||
case VK_FORMAT_R8_SNORM: out = {ReadbackSourceClass::Float, 1, 8, true}; return true;
|
||||
case VK_FORMAT_R8G8_SNORM: out = {ReadbackSourceClass::Float, 2, 8, true}; return true;
|
||||
@@ -1949,6 +1951,8 @@ void main() {
|
||||
// --- packed / special ---
|
||||
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
|
||||
case VK_FORMAT_A2B10G10R10_UINT_PACK32:
|
||||
case VK_FORMAT_A2R10G10B10_UNORM_PACK32:
|
||||
case VK_FORMAT_A2R10G10B10_UINT_PACK32:
|
||||
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
|
||||
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
|
||||
case VK_FORMAT_R5G6B5_UNORM_PACK16:
|
||||
@@ -1958,7 +1962,8 @@ void main() {
|
||||
case VK_FORMAT_B5G5R5A1_UNORM_PACK16:
|
||||
case VK_FORMAT_R4G4B4A4_UNORM_PACK16:
|
||||
case VK_FORMAT_B4G4R4A4_UNORM_PACK16:
|
||||
out.sourceClass = format == VK_FORMAT_A2B10G10R10_UINT_PACK32 ?
|
||||
out.sourceClass = (format == VK_FORMAT_A2B10G10R10_UINT_PACK32 ||
|
||||
format == VK_FORMAT_A2R10G10B10_UINT_PACK32) ?
|
||||
ReadbackSourceClass::UnsignedInt : ReadbackSourceClass::Float;
|
||||
out.special = format;
|
||||
return true;
|
||||
@@ -2034,6 +2039,15 @@ void main() {
|
||||
rgba[3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
|
||||
return;
|
||||
}
|
||||
case VK_FORMAT_A2R10G10B10_UNORM_PACK32: {
|
||||
Uint32 word = 0;
|
||||
Memcpy(&word, source, sizeof(word));
|
||||
rgba[2] = static_cast<Float>(word & 0x3FFu) / 1023.0f;
|
||||
rgba[1] = static_cast<Float>((word >> 10) & 0x3FFu) / 1023.0f;
|
||||
rgba[0] = static_cast<Float>((word >> 20) & 0x3FFu) / 1023.0f;
|
||||
rgba[3] = static_cast<Float>((word >> 30) & 0x3u) / 3.0f;
|
||||
return;
|
||||
}
|
||||
case VK_FORMAT_B10G11R11_UFLOAT_PACK32: {
|
||||
Uint32 word = 0;
|
||||
Memcpy(&word, source, sizeof(word));
|
||||
@@ -2185,6 +2199,13 @@ void main() {
|
||||
rgba[1] = (word >> 10) & 0x3FFu;
|
||||
rgba[2] = (word >> 20) & 0x3FFu;
|
||||
rgba[3] = (word >> 30) & 0x3u;
|
||||
} else if (srcFormat == VK_FORMAT_A2R10G10B10_UINT_PACK32) {
|
||||
Uint32 word = 0;
|
||||
Memcpy(&word, source, sizeof(word));
|
||||
rgba[2] = word & 0x3FFu;
|
||||
rgba[1] = (word >> 10) & 0x3FFu;
|
||||
rgba[0] = (word >> 20) & 0x3FFu;
|
||||
rgba[3] = (word >> 30) & 0x3u;
|
||||
} else {
|
||||
for (Int c = 0; c < desc.channels; ++c) {
|
||||
if (desc.componentBits == 8) {
|
||||
@@ -2216,8 +2237,9 @@ void main() {
|
||||
}
|
||||
|
||||
static Bool PackReadbackToClientOrPbo(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width,
|
||||
GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
GLsizei sliceHeight, GLsizei sliceCount, GLenum format, GLenum type,
|
||||
void* pixels, Bool applyPackImageParams) {
|
||||
if (width <= 0 || sliceHeight <= 0 || sliceCount <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2230,7 +2252,8 @@ void main() {
|
||||
|
||||
Vector<Uint8> wide;
|
||||
GLenum wideType = GL_FLOAT;
|
||||
if (!DecodeReadbackRowsToWide(srcPixels, srcFormat, width, height, wide, wideType)) {
|
||||
if (!DecodeReadbackRowsToWide(srcPixels, srcFormat, width,
|
||||
sliceHeight * sliceCount, wide, wideType)) {
|
||||
MGLOG_E("DirectVulkan readback skipped: unsupported source format=%d",
|
||||
static_cast<Int>(srcFormat));
|
||||
return false;
|
||||
@@ -2243,9 +2266,9 @@ void main() {
|
||||
return false;
|
||||
}
|
||||
|
||||
return DirectGLES::ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, height,
|
||||
/*sliceCount=*/1, mapping, type, pixels,
|
||||
/*applyPackImageParams=*/false);
|
||||
return DirectGLES::ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight,
|
||||
sliceCount, mapping, type, pixels,
|
||||
applyPackImageParams);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -6333,7 +6356,8 @@ void main() {
|
||||
if (RemapDefaultFboReadbackToGLOrientation(mapped, swapchainExtent, preTransform,
|
||||
sourceTexelSize,
|
||||
remapped.data())) {
|
||||
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, format, type, pixels);
|
||||
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, 1, format, type, pixels,
|
||||
/*applyPackImageParams=*/false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -6342,7 +6366,8 @@ void main() {
|
||||
width, height, swapchainExtent.width, swapchainExtent.height,
|
||||
static_cast<Int>(preTransform));
|
||||
}
|
||||
PackReadbackToClientOrPbo(mapped, srcFormat, width, height, format, type, pixels);
|
||||
PackReadbackToClientOrPbo(mapped, srcFormat, width, height, 1, format, type, pixels,
|
||||
/*applyPackImageParams=*/false);
|
||||
}
|
||||
|
||||
void VulkanRenderer::GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||
@@ -6395,6 +6420,17 @@ void main() {
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
// GetTexImage returns every slice of a 3D level and every layer of an array
|
||||
// level; GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply to the 3D/array
|
||||
// destination layout (GL 3.3 section 6.1.4).
|
||||
const auto imageTextureTarget = textureObject->GetTarget();
|
||||
const Bool is3dImage = imageTextureTarget == TextureTarget::Texture3D;
|
||||
const Bool isArrayImage = imageTextureTarget == TextureTarget::Texture1DArray ||
|
||||
imageTextureTarget == TextureTarget::Texture2DArray ||
|
||||
imageTextureTarget == TextureTarget::TextureCubeMapArray;
|
||||
const GLsizei depthSlices = is3dImage ? std::max<GLsizei>(texelSize.z(), 1) : 1;
|
||||
const GLsizei arrayLayers = isArrayImage ? static_cast<GLsizei>(resource->arrayLayers) : 1;
|
||||
const GLsizei sliceCount = std::max<GLsizei>(depthSlices * arrayLayers, 1);
|
||||
if (bufSize >= 0) {
|
||||
const Int dstChannels = GetReadbackChannelCount(format);
|
||||
if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) {
|
||||
@@ -6415,7 +6451,8 @@ void main() {
|
||||
return;
|
||||
}
|
||||
const VkDeviceSize readbackSize = static_cast<VkDeviceSize>(width) *
|
||||
static_cast<VkDeviceSize>(height) * sourceTexelSize;
|
||||
static_cast<VkDeviceSize>(height) *
|
||||
static_cast<VkDeviceSize>(sliceCount) * sourceTexelSize;
|
||||
VkBufferObject readback;
|
||||
if (!readback.Create({
|
||||
.allocator = m_allocator,
|
||||
@@ -6443,8 +6480,9 @@ void main() {
|
||||
copyRegion.imageSubresource.aspectMask = resource->aspect;
|
||||
copyRegion.imageSubresource.mipLevel = static_cast<Uint32>(level);
|
||||
copyRegion.imageSubresource.baseArrayLayer = 0;
|
||||
copyRegion.imageSubresource.layerCount = 1;
|
||||
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
||||
copyRegion.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers);
|
||||
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height),
|
||||
static_cast<Uint32>(depthSlices)};
|
||||
vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
readback.GetHandle(), 1, ©Region);
|
||||
|
||||
@@ -6470,7 +6508,8 @@ void main() {
|
||||
MGLOG_E("DirectVulkan::GetTextureImage skipped: failed to invalidate readback buffer");
|
||||
return;
|
||||
}
|
||||
PackReadbackToClientOrPbo(mapped, resource->format, width, height, format, type, pixels);
|
||||
PackReadbackToClientOrPbo(mapped, resource->format, width, height, sliceCount, format, type, pixels,
|
||||
/*applyPackImageParams=*/is3dImage || isArrayImage);
|
||||
}
|
||||
|
||||
void VulkanRenderer::GenerateMipmap(GLenum target) {
|
||||
|
||||
@@ -133,9 +133,11 @@ namespace MobileGL {
|
||||
case TextureInternalFormat::RGBA8Snorm:
|
||||
return VK_FORMAT_R8G8B8A8_SNORM;
|
||||
case TextureInternalFormat::RGB10A2:
|
||||
return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
|
||||
// GL_UNSIGNED_INT_2_10_10_10_REV puts R in bits 0-9, which is Vulkan's
|
||||
// A2B10G10R10 layout - A2R10G10B10 silently swaps R and B on upload.
|
||||
return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return VK_FORMAT_A2R10G10B10_UINT_PACK32;
|
||||
return VK_FORMAT_A2B10G10R10_UINT_PACK32;
|
||||
case TextureInternalFormat::RGBA16:
|
||||
return VK_FORMAT_R16G16B16A16_UNORM;
|
||||
case TextureInternalFormat::RGBA16Snorm:
|
||||
|
||||
@@ -9,14 +9,10 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
// Severity-ordered: a build compiled at level X keeps X and everything MORE
|
||||
// severe. INFO builds must keep WARN/ERROR/FATAL — the old ordering
|
||||
// (WARN=1/ERROR=2 below INFO=3) compiled every warning and error out of
|
||||
// release builds and hid real backend failures.
|
||||
#define MOBILEGL_LOG_LEVEL_DEBUG 0
|
||||
#define MOBILEGL_LOG_LEVEL_INFO 1
|
||||
#define MOBILEGL_LOG_LEVEL_WARN 2
|
||||
#define MOBILEGL_LOG_LEVEL_ERROR 3
|
||||
#define MOBILEGL_LOG_LEVEL_WARN 1
|
||||
#define MOBILEGL_LOG_LEVEL_ERROR 2
|
||||
#define MOBILEGL_LOG_LEVEL_INFO 3
|
||||
#define MOBILEGL_LOG_LEVEL_FATAL 4
|
||||
|
||||
#define MOBILEGL_LOG_INTERNAL(levelTag, androidLogLevel, fmt, ...) \
|
||||
|
||||
@@ -22,9 +22,6 @@
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
#include "SpirvPasses/FoldConstOffsetFor1DFetchPass.h"
|
||||
#include "SpirvPasses/LowerClipDistanceForEsslPass.h"
|
||||
#include "SpirvPasses/DefeatConstStructArrayLutPass.h"
|
||||
#include "spirv-tools/libspirv.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
@@ -326,55 +323,6 @@ namespace MobileGL {
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::ClampAccessChainIndicesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(CreateGraphicsRobustAccessPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::FoldConstOffsetFor1DFetchForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(FoldConstOffsetFor1DFetchPass::CreateFoldConstOffsetFor1DFetchPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerClipDistanceForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(LowerClipDistanceForEsslPass::CreateLowerClipDistanceForEsslPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DefeatConstStructArrayLutForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(
|
||||
DefeatConstStructArrayLutPass::CreateDefeatConstStructArrayLutPass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -27,36 +27,6 @@ namespace MobileGL {
|
||||
// Only for backends without native draw-parameter support (DirectGLES).
|
||||
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Clamps every access-chain index to its declared bounds (spirv-tools
|
||||
// GraphicsRobustAccessPass). GL 3.3 only promises undefined *values* for
|
||||
// out-of-bounds indexing, but Adreno's ESSL compiler constant-folds a provably
|
||||
// out-of-bounds local-array index into poison that corrupts the whole shader's
|
||||
// output; clamping restores the "some value from the array" contract. Only for
|
||||
// the DirectGLES transpile path.
|
||||
static bool ClampAccessChainIndicesForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Folds the ConstOffset image operand of Dim1D OpImageFetch into the integer
|
||||
// coordinate (texelFetchOffset(t,P,l,o) == texelFetch(t,P+o,l)). SPIRV-Cross
|
||||
// emulates 1D samplers as 2D for ES: it widens the coordinate to ivec2 but keeps
|
||||
// the scalar offset, and ESSL has no texelFetchOffset(sampler2D, ivec2, int,
|
||||
// scalar) overload, so Adreno rejects the shader. Only for the DirectGLES
|
||||
// transpile path.
|
||||
static bool FoldConstOffsetFor1DFetchForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Shadows gl_ClipDistance in Private mg_ClipDistance/mg_ClipDistanceIn arrays so
|
||||
// the decompiled ESSL only writes the builtin with literal constant indices
|
||||
// (flush before EmitVertex/return) and only reads gl_in clip distances with
|
||||
// dynamic loop indices (copy loop): the other shapes miscompile or crash
|
||||
// Adreno's ESSL compiler. Vertex/geometry stages; DirectGLES transpile path on
|
||||
// Qualcomm only (quirk-gated). See LowerClipDistanceForEsslPass.
|
||||
static bool LowerClipDistanceForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Splits a Function-storage array-of-structs variable's single constant-composite
|
||||
// store into per-element stores so SPIRV-Cross does not hoist it into a global
|
||||
// const struct[] LUT, which Adreno cannot dynamically index. DirectGLES
|
||||
// transpile path on Qualcomm only (quirk-gated). See DefeatConstStructArrayLutPass.
|
||||
static bool DefeatConstStructArrayLutForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Drops RelaxedPrecision member decorations from uniform-block structs so
|
||||
// SPIRV-Cross prints the same (highp) member precision in every stage; ES
|
||||
// drivers reject cross-stage uniform blocks whose member precisions differ.
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "DefeatConstStructArrayLutPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::BasicBlock;
|
||||
using spvtools::opt::Function;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) {
|
||||
analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId);
|
||||
analysis::Pointer ptr(pointee, sc);
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&ptr);
|
||||
}
|
||||
|
||||
uint32_t SignedIntConstant(IRContext* ctx, uint32_t value) {
|
||||
analysis::Integer i(32, true);
|
||||
analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i);
|
||||
const analysis::Constant* c = ctx->get_constant_mgr()->GetConstant(reg, {value});
|
||||
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
|
||||
}
|
||||
|
||||
// True when |var| (a Function-storage OpVariable) points to an array of structs.
|
||||
// Reports the struct type id on success.
|
||||
bool IsArrayOfStructsVariable(IRContext* ctx, Instruction* var, uint32_t& structTypeId) {
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
Instruction* ptrType = defUse->GetDef(var->type_id());
|
||||
if (ptrType == nullptr || ptrType->opcode() != spv::Op::OpTypePointer) return false;
|
||||
Instruction* pointee = defUse->GetDef(ptrType->GetSingleWordInOperand(1));
|
||||
if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeArray) return false;
|
||||
Instruction* element = defUse->GetDef(pointee->GetSingleWordInOperand(0));
|
||||
if (element == nullptr || element->opcode() != spv::Op::OpTypeStruct) return false;
|
||||
structTypeId = element->result_id();
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status DefeatConstStructArrayLutPass::Process() {
|
||||
auto* ctx = context();
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
bool modified = false;
|
||||
|
||||
for (Function& function : *get_module()) {
|
||||
if (function.begin() == function.end()) continue;
|
||||
BasicBlock* entryBlock = &*function.begin();
|
||||
|
||||
// Candidate variables: Function-storage arrays of structs declared in this
|
||||
// function's entry block (where OpVariables must live).
|
||||
struct Candidate {
|
||||
Instruction* var;
|
||||
uint32_t structTypeId;
|
||||
};
|
||||
std::vector<Candidate> candidates;
|
||||
for (Instruction& inst : *entryBlock) {
|
||||
if (inst.opcode() != spv::Op::OpVariable) break;
|
||||
// Variables with initializers keep SPIRV-Cross's initializer path; the
|
||||
// glslang pattern under attack is initializer-free with one OpStore.
|
||||
if (inst.NumInOperands() > 1) continue;
|
||||
uint32_t structTypeId = 0;
|
||||
if (IsArrayOfStructsVariable(ctx, &inst, structTypeId)) {
|
||||
candidates.push_back({&inst, structTypeId});
|
||||
}
|
||||
}
|
||||
|
||||
for (const Candidate& candidate : candidates) {
|
||||
Instruction* var = candidate.var;
|
||||
|
||||
// The variable qualifies only when its single write is one direct
|
||||
// OpStore of an OpConstantComposite; any other write shape already
|
||||
// defeats SPIRV-Cross's LUT promotion, so it is left untouched.
|
||||
Instruction* singleStore = nullptr;
|
||||
bool disqualified = false;
|
||||
defUse->ForEachUser(var, [&](Instruction* user) {
|
||||
if (user->opcode() == spv::Op::OpStore &&
|
||||
user->GetSingleWordInOperand(0) == var->result_id()) {
|
||||
if (singleStore != nullptr) {
|
||||
disqualified = true;
|
||||
} else {
|
||||
singleStore = user;
|
||||
}
|
||||
} else if (user->opcode() == spv::Op::OpCopyMemory) {
|
||||
disqualified = true;
|
||||
} else if (user->opcode() == spv::Op::OpAccessChain ||
|
||||
user->opcode() == spv::Op::OpInBoundsAccessChain) {
|
||||
defUse->ForEachUser(user, [&](Instruction* chainUser) {
|
||||
if (chainUser->opcode() == spv::Op::OpStore ||
|
||||
chainUser->opcode() == spv::Op::OpCopyMemory) {
|
||||
disqualified = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (disqualified || singleStore == nullptr) continue;
|
||||
|
||||
Instruction* composite = defUse->GetDef(singleStore->GetSingleWordInOperand(1));
|
||||
if (composite == nullptr ||
|
||||
composite->opcode() != spv::Op::OpConstantComposite) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The store must sit in the entry block: that is the only placement
|
||||
// SPIRV-Cross treats as a LUT initializer.
|
||||
bool storeInEntryBlock = false;
|
||||
for (Instruction& inst : *entryBlock) {
|
||||
if (&inst == singleStore) {
|
||||
storeInEntryBlock = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!storeInEntryBlock) continue;
|
||||
|
||||
// Split the composite store into one constant-index store per element.
|
||||
const uint32_t ptrFnStruct =
|
||||
PointerTypeTo(ctx, candidate.structTypeId, spv::StorageClass::Function);
|
||||
for (uint32_t element = 0; element < composite->NumInOperands(); ++element) {
|
||||
const uint32_t elementConstId = composite->GetSingleWordInOperand(element);
|
||||
const uint32_t chainId = ctx->TakeNextId();
|
||||
Instruction* chain =
|
||||
singleStore->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpAccessChain, ptrFnStruct, chainId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {var->result_id()}},
|
||||
{SPV_OPERAND_TYPE_ID, {SignedIntConstant(ctx, element)}}}));
|
||||
ctx->AnalyzeDefUse(chain);
|
||||
Instruction* store =
|
||||
singleStore->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpStore, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {chainId}},
|
||||
{SPV_OPERAND_TYPE_ID, {elementConstId}}}));
|
||||
ctx->AnalyzeDefUse(store);
|
||||
}
|
||||
ctx->KillInst(singleStore);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!modified) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
ctx->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
DefeatConstStructArrayLutPass::CreateDefeatConstStructArrayLutPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<DefeatConstStructArrayLutPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -1,36 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// SPIRV-Cross hoists a Function-storage array variable whose only write is a single
|
||||
// constant-composite store into a global `const struct[]` LUT (variable_is_lut).
|
||||
// Adreno's ESSL compiler cannot dynamically index such a global const struct array
|
||||
// ("Cannot offset into the structure" - device-verified on Adreno 750). Splitting
|
||||
// the one composite store into per-element constant-index stores makes
|
||||
// variable_is_lut fail, so SPIRV-Cross keeps the array as an ordinary local that
|
||||
// Adreno indexes fine. Scalar/vector const arrays are unaffected on Adreno and are
|
||||
// left alone - only arrays OF STRUCTS are rewritten. Only meant for the DirectGLES
|
||||
// transpile path on Qualcomm devices.
|
||||
class DefeatConstStructArrayLutPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "defeat-const-struct-array-lut"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateDefeatConstStructArrayLutPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -1,131 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "FoldConstOffsetFor1DFetchPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_builder.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::InstructionBuilder;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
// Number of ImageOperands ids that precede the ConstOffset id: one per
|
||||
// lower-order bit set in the mask, except Grad which carries two ids.
|
||||
uint32_t CountIdsBeforeConstOffset(uint32_t mask) {
|
||||
uint32_t count = 0;
|
||||
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Bias)) count += 1;
|
||||
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Lod)) count += 1;
|
||||
if (mask & static_cast<uint32_t>(spv::ImageOperandsMask::Grad)) count += 2;
|
||||
return count;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status FoldConstOffsetFor1DFetchPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
Bool modified = false;
|
||||
|
||||
constexpr uint32_t kConstOffsetBit =
|
||||
static_cast<uint32_t>(spv::ImageOperandsMask::ConstOffset);
|
||||
|
||||
for (auto& function : *get_module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& inst : block) {
|
||||
if (inst.opcode() != spv::Op::OpImageFetch) continue;
|
||||
// In-operands: image, coordinate, [ImageOperands mask, ids...].
|
||||
if (inst.NumInOperands() < 3) continue;
|
||||
const uint32_t operandsMask = inst.GetSingleWordInOperand(2);
|
||||
if ((operandsMask & kConstOffsetBit) == 0) continue;
|
||||
|
||||
Instruction* imageInst = defUseMgr->GetDef(inst.GetSingleWordInOperand(0));
|
||||
if (imageInst == nullptr) continue;
|
||||
Instruction* imageType = defUseMgr->GetDef(imageInst->type_id());
|
||||
if (imageType == nullptr || imageType->opcode() != spv::Op::OpTypeImage ||
|
||||
static_cast<spv::Dim>(imageType->GetSingleWordInOperand(1)) != spv::Dim::Dim1D) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t offsetOperandIndex = 3 + CountIdsBeforeConstOffset(operandsMask);
|
||||
const uint32_t offsetId = inst.GetSingleWordInOperand(offsetOperandIndex);
|
||||
|
||||
const uint32_t coordId = inst.GetSingleWordInOperand(1);
|
||||
Instruction* coordType = defUseMgr->GetDef(defUseMgr->GetDef(coordId)->type_id());
|
||||
|
||||
InstructionBuilder builder(
|
||||
irContext, &inst,
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
|
||||
uint32_t newCoordId = 0;
|
||||
if (coordType->opcode() == spv::Op::OpTypeVector) {
|
||||
// Arrayed 1D fetch: component 0 is the texel coordinate,
|
||||
// component 1 the layer - only component 0 takes the offset.
|
||||
const uint32_t componentTypeId = coordType->GetSingleWordInOperand(0);
|
||||
Instruction* extracted = builder.AddCompositeExtract(componentTypeId, coordId, {0});
|
||||
Instruction* sum =
|
||||
builder.AddIAdd(componentTypeId, extracted->result_id(), offsetId);
|
||||
Instruction* inserted = builder.AddInstruction(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpCompositeInsert, coordType->result_id(),
|
||||
irContext->TakeNextId(),
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {sum->result_id()}},
|
||||
{SPV_OPERAND_TYPE_ID, {coordId}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {0}}}));
|
||||
newCoordId = inserted->result_id();
|
||||
} else {
|
||||
Instruction* sum = builder.AddIAdd(coordType->result_id(), coordId, offsetId);
|
||||
newCoordId = sum->result_id();
|
||||
}
|
||||
|
||||
const uint32_t newMask = operandsMask & ~kConstOffsetBit;
|
||||
// 3 fixed operands + the offset id: anything beyond that is another
|
||||
// image-operand id that must keep the mask word alive.
|
||||
const Bool otherOperandIdsRemain = inst.NumInOperands() > 4;
|
||||
|
||||
irContext->ForgetUses(&inst);
|
||||
std::vector<Operand> newOperands;
|
||||
newOperands.push_back(inst.GetInOperand(0));
|
||||
newOperands.push_back({SPV_OPERAND_TYPE_ID, {newCoordId}});
|
||||
if (newMask != 0 || otherOperandIdsRemain) {
|
||||
Operand maskOperand = inst.GetInOperand(2);
|
||||
maskOperand.words[0] = newMask;
|
||||
newOperands.push_back(maskOperand);
|
||||
for (uint32_t i = 3; i < inst.NumInOperands(); ++i) {
|
||||
if (i == offsetOperandIndex) continue;
|
||||
newOperands.push_back(inst.GetInOperand(i));
|
||||
}
|
||||
}
|
||||
inst.SetInOperands(std::move(newOperands));
|
||||
irContext->AnalyzeUses(&inst);
|
||||
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken FoldConstOffsetFor1DFetchPass::CreateFoldConstOffsetFor1DFetchPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<FoldConstOffsetFor1DFetchPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -1,36 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// SPIRV-Cross emulates 1D textures as 2D for ES targets: it widens the texelFetch
|
||||
// coordinate to ivec2 but keeps the ConstOffset image operand scalar, and ESSL has
|
||||
// no texelFetchOffset(sampler2D, ivec2, int, scalar-offset) overload, so drivers
|
||||
// (Adreno) reject the transpiled shader. This pass folds the constant offset into
|
||||
// the integer coordinate before the fetch - texelFetchOffset(t, P, l, o) ==
|
||||
// texelFetch(t, P + o, l) per the GLSL spec - and drops the ConstOffset operand,
|
||||
// so SPIRV-Cross emits a plain texelFetch. For arrayed 1D fetches only coordinate
|
||||
// component 0 is offset (component 1 is the layer). Only meant for the DirectGLES
|
||||
// transpile path.
|
||||
class FoldConstOffsetFor1DFetchPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "fold-const-offset-for-1d-fetch"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateFoldConstOffsetFor1DFetchPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -1,613 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "LowerClipDistanceForEsslPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/basic_block.h"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::BasicBlock;
|
||||
using spvtools::opt::Function;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
spv::ExecutionModel EntryExecutionModel(IRContext* ctx) {
|
||||
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||
return static_cast<spv::ExecutionModel>(ep.GetSingleWordInOperand(0));
|
||||
}
|
||||
return spv::ExecutionModel::Max;
|
||||
}
|
||||
|
||||
uint32_t EntryFunctionId(IRContext* ctx) {
|
||||
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||
// OpEntryPoint <model> <function> "name" <interface...>
|
||||
return ep.GetSingleWordInOperand(1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t VariablePointeeType(IRContext* ctx, Instruction* var) {
|
||||
Instruction* ptrType = ctx->get_def_use_mgr()->GetDef(var->type_id());
|
||||
// OpTypePointer <storage-class> <pointee>
|
||||
return ptrType->GetSingleWordInOperand(1);
|
||||
}
|
||||
|
||||
uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) {
|
||||
analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId);
|
||||
analysis::Pointer ptr(pointee, sc);
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&ptr);
|
||||
}
|
||||
|
||||
uint32_t IntConstant(IRContext* ctx, bool isSigned, uint32_t value) {
|
||||
analysis::Integer i(32, isSigned);
|
||||
analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i);
|
||||
const analysis::Constant* c = ctx->get_constant_mgr()->GetConstant(reg, {value});
|
||||
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
|
||||
}
|
||||
|
||||
uint32_t UintType(IRContext* ctx) {
|
||||
analysis::Integer i(32, false);
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&i);
|
||||
}
|
||||
|
||||
uint32_t BoolType(IRContext* ctx) {
|
||||
analysis::Bool b;
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&b);
|
||||
}
|
||||
|
||||
// Constant length of OpTypeArray |arrayTypeId| (0 when not a sized constant).
|
||||
uint32_t ArrayLength(IRContext* ctx, uint32_t arrayTypeId) {
|
||||
Instruction* arrayType = ctx->get_def_use_mgr()->GetDef(arrayTypeId);
|
||||
if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray) {
|
||||
return 0;
|
||||
}
|
||||
Instruction* length = ctx->get_def_use_mgr()->GetDef(arrayType->GetSingleWordInOperand(1));
|
||||
if (length == nullptr || length->opcode() != spv::Op::OpConstant) {
|
||||
return 0;
|
||||
}
|
||||
return length->GetSingleWordInOperand(0);
|
||||
}
|
||||
|
||||
bool IsConstantWithValue(IRContext* ctx, uint32_t id, uint32_t value) {
|
||||
Instruction* def = ctx->get_def_use_mgr()->GetDef(id);
|
||||
return def != nullptr && def->opcode() == spv::Op::OpConstant &&
|
||||
def->GetSingleWordInOperand(0) == value;
|
||||
}
|
||||
|
||||
bool IsAccessChain(const Instruction* inst) {
|
||||
return inst->opcode() == spv::Op::OpAccessChain ||
|
||||
inst->opcode() == spv::Op::OpInBoundsAccessChain;
|
||||
}
|
||||
|
||||
Instruction* AddPrivateVariable(IRContext* ctx, uint32_t pointeeTypeId, const char* name) {
|
||||
const uint32_t ptrType = PointerTypeTo(ctx, pointeeTypeId, spv::StorageClass::Private);
|
||||
const uint32_t varId = ctx->TakeNextId();
|
||||
ctx->AddGlobalValue(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpVariable, ptrType, varId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{static_cast<uint32_t>(spv::StorageClass::Private)}}}));
|
||||
ctx->AddDebug2Inst(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpName, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {varId}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)}}));
|
||||
return ctx->get_def_use_mgr()->GetDef(varId);
|
||||
}
|
||||
|
||||
// Retargets |chain| onto |newBaseId|, dropping the first |dropIndexCount| index
|
||||
// operands and switching the result pointer's storage class to Private.
|
||||
void RetargetChainToPrivate(IRContext* ctx, Instruction* chain, uint32_t newBaseId,
|
||||
uint32_t dropIndexCount) {
|
||||
Instruction* chainPtrType = ctx->get_def_use_mgr()->GetDef(chain->type_id());
|
||||
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
|
||||
const uint32_t newPtrType = PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
|
||||
|
||||
ctx->ForgetUses(chain);
|
||||
std::vector<Operand> newOperands;
|
||||
newOperands.push_back({SPV_OPERAND_TYPE_ID, {newBaseId}});
|
||||
for (uint32_t i = 1 + dropIndexCount; i < chain->NumInOperands(); ++i) {
|
||||
newOperands.push_back(chain->GetInOperand(i));
|
||||
}
|
||||
chain->SetResultType(newPtrType);
|
||||
chain->SetInOperands(std::move(newOperands));
|
||||
ctx->AnalyzeUses(chain);
|
||||
}
|
||||
|
||||
// ---- Output side --------------------------------------------------------------
|
||||
|
||||
struct OutputTarget {
|
||||
Instruction* var = nullptr; // Output gl_PerVertex block or standalone builtin
|
||||
bool isBlockMember = false;
|
||||
uint32_t memberIndex = 0; // valid when isBlockMember
|
||||
uint32_t arrayTypeId = 0; // float[N]
|
||||
uint32_t elemTypeId = 0; // float
|
||||
uint32_t arrayLen = 0; // N
|
||||
};
|
||||
|
||||
// Inserts "gl_ClipDistance[k] = mg_ClipDistance[k]" for every literal k before
|
||||
// |before|. Constant-index writes are the only write shape Adreno links correctly.
|
||||
void InsertFlushBefore(IRContext* ctx, Instruction* before, const OutputTarget& target,
|
||||
uint32_t mgVarId) {
|
||||
const uint32_t ptrPrivElem =
|
||||
PointerTypeTo(ctx, target.elemTypeId, spv::StorageClass::Private);
|
||||
const uint32_t ptrOutElem =
|
||||
PointerTypeTo(ctx, target.elemTypeId, spv::StorageClass::Output);
|
||||
for (uint32_t k = 0; k < target.arrayLen; ++k) {
|
||||
const uint32_t kConst = IntConstant(ctx, true, k);
|
||||
const uint32_t srcChainId = ctx->TakeNextId();
|
||||
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpAccessChain, ptrPrivElem, srcChainId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {mgVarId}},
|
||||
{SPV_OPERAND_TYPE_ID, {kConst}}}));
|
||||
const uint32_t valId = ctx->TakeNextId();
|
||||
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLoad, target.elemTypeId, valId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {srcChainId}}}));
|
||||
const uint32_t dstChainId = ctx->TakeNextId();
|
||||
std::vector<Operand> dstOperands;
|
||||
dstOperands.push_back({SPV_OPERAND_TYPE_ID, {target.var->result_id()}});
|
||||
if (target.isBlockMember) {
|
||||
dstOperands.push_back(
|
||||
{SPV_OPERAND_TYPE_ID, {IntConstant(ctx, true, target.memberIndex)}});
|
||||
}
|
||||
dstOperands.push_back({SPV_OPERAND_TYPE_ID, {kConst}});
|
||||
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpAccessChain, ptrOutElem, dstChainId, dstOperands));
|
||||
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpStore, 0, 0,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {dstChainId}},
|
||||
{SPV_OPERAND_TYPE_ID, {valId}}}));
|
||||
}
|
||||
}
|
||||
|
||||
bool LowerOutputClipDistance(IRContext* ctx, bool isGeometry) {
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
|
||||
// Collect (struct type, member) pairs decorated BuiltIn ClipDistance and
|
||||
// standalone variables decorated BuiltIn ClipDistance.
|
||||
std::vector<std::pair<uint32_t, uint32_t>> memberTargets; // (structId, member)
|
||||
std::vector<uint32_t> plainTargets; // variable ids
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() == spv::Op::OpMemberDecorate && ann.NumInOperands() >= 4 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) ==
|
||||
spv::Decoration::BuiltIn &&
|
||||
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) ==
|
||||
spv::BuiltIn::ClipDistance) {
|
||||
memberTargets.emplace_back(ann.GetSingleWordInOperand(0),
|
||||
ann.GetSingleWordInOperand(1));
|
||||
} else if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 3 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) ==
|
||||
spv::Decoration::BuiltIn &&
|
||||
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(2)) ==
|
||||
spv::BuiltIn::ClipDistance) {
|
||||
plainTargets.push_back(ann.GetSingleWordInOperand(0));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<OutputTarget> targets;
|
||||
for (Instruction& inst : ctx->module()->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Output) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t pointee = VariablePointeeType(ctx, &inst);
|
||||
for (const auto& [structId, member] : memberTargets) {
|
||||
if (pointee != structId) continue;
|
||||
Instruction* structType = defUse->GetDef(structId);
|
||||
if (structType == nullptr || member >= structType->NumInOperands()) continue;
|
||||
OutputTarget target;
|
||||
target.var = &inst;
|
||||
target.isBlockMember = true;
|
||||
target.memberIndex = member;
|
||||
target.arrayTypeId = structType->GetSingleWordInOperand(member);
|
||||
target.arrayLen = ArrayLength(ctx, target.arrayTypeId);
|
||||
targets.push_back(target);
|
||||
}
|
||||
for (const uint32_t varId : plainTargets) {
|
||||
if (inst.result_id() != varId) continue;
|
||||
OutputTarget target;
|
||||
target.var = &inst;
|
||||
target.isBlockMember = false;
|
||||
target.arrayTypeId = pointee;
|
||||
target.arrayLen = ArrayLength(ctx, target.arrayTypeId);
|
||||
targets.push_back(target);
|
||||
}
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
for (OutputTarget& target : targets) {
|
||||
if (target.arrayLen == 0) continue;
|
||||
Instruction* arrayType = defUse->GetDef(target.arrayTypeId);
|
||||
target.elemTypeId = arrayType->GetSingleWordInOperand(0);
|
||||
|
||||
// Collect the accesses to redirect. For the block form only chains whose
|
||||
// leading index selects the ClipDistance member count; for the standalone
|
||||
// form every chain plus whole-variable loads/stores.
|
||||
std::vector<Instruction*> chains;
|
||||
std::vector<Instruction*> directAccesses;
|
||||
bool unsupportedUse = false;
|
||||
defUse->ForEachUser(target.var, [&](Instruction* user) {
|
||||
if (IsAccessChain(user) &&
|
||||
user->GetSingleWordInOperand(0) == target.var->result_id()) {
|
||||
if (target.isBlockMember) {
|
||||
if (user->NumInOperands() >= 2 &&
|
||||
IsConstantWithValue(ctx, user->GetSingleWordInOperand(1),
|
||||
target.memberIndex)) {
|
||||
chains.push_back(user);
|
||||
}
|
||||
} else {
|
||||
chains.push_back(user);
|
||||
}
|
||||
} else if (!target.isBlockMember) {
|
||||
if (user->opcode() == spv::Op::OpLoad ||
|
||||
(user->opcode() == spv::Op::OpStore &&
|
||||
user->GetSingleWordInOperand(0) == target.var->result_id())) {
|
||||
directAccesses.push_back(user);
|
||||
} else if (user->opcode() == spv::Op::OpCopyMemory) {
|
||||
unsupportedUse = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (unsupportedUse || (chains.empty() && directAccesses.empty())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Instruction* mgVar = AddPrivateVariable(ctx, target.arrayTypeId, "mg_ClipDistance");
|
||||
const uint32_t mgVarId = mgVar->result_id();
|
||||
|
||||
for (Instruction* chain : chains) {
|
||||
const uint32_t dropCount = target.isBlockMember ? 1u : 0u;
|
||||
if (chain->NumInOperands() == 1 + dropCount) {
|
||||
// Pointer to the whole float[N]: reuse the private variable itself.
|
||||
ctx->ReplaceAllUsesWith(chain->result_id(), mgVarId);
|
||||
ctx->KillInst(chain);
|
||||
} else {
|
||||
RetargetChainToPrivate(ctx, chain, mgVarId, dropCount);
|
||||
}
|
||||
}
|
||||
for (Instruction* access : directAccesses) {
|
||||
ctx->ForgetUses(access);
|
||||
access->SetInOperand(0, {mgVarId});
|
||||
ctx->AnalyzeUses(access);
|
||||
}
|
||||
|
||||
// Flush the shadow into the real builtin: geometry right before every
|
||||
// EmitVertex, vertex before every return of the entry point. The flush is
|
||||
// also what keeps the builtin statically used for cross-stage IO matching.
|
||||
std::vector<Instruction*> flushSites;
|
||||
if (isGeometry) {
|
||||
for (Function& function : *ctx->module()) {
|
||||
function.ForEachInst([&](Instruction* inst) {
|
||||
if (inst->opcode() == spv::Op::OpEmitVertex) {
|
||||
flushSites.push_back(inst);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const uint32_t entryFuncId = EntryFunctionId(ctx);
|
||||
for (Function& function : *ctx->module()) {
|
||||
if (function.result_id() != entryFuncId) continue;
|
||||
function.ForEachInst([&](Instruction* inst) {
|
||||
if (inst->opcode() == spv::Op::OpReturn ||
|
||||
inst->opcode() == spv::Op::OpReturnValue) {
|
||||
flushSites.push_back(inst);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
for (Instruction* site : flushSites) {
|
||||
InsertFlushBefore(ctx, site, target, mgVarId);
|
||||
}
|
||||
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ---- Input side (geometry gl_in) ----------------------------------------------
|
||||
|
||||
bool LowerInputClipDistance(IRContext* ctx) {
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
auto* typeMgr = ctx->get_type_mgr();
|
||||
|
||||
// Locate the gl_in block member decorated ClipDistance.
|
||||
Instruction* glInVar = nullptr;
|
||||
uint32_t memberIndex = 0;
|
||||
uint32_t arrayTypeId = 0; // float[N]
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() != spv::Op::OpMemberDecorate || ann.NumInOperands() < 4 ||
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) !=
|
||||
spv::Decoration::BuiltIn ||
|
||||
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) !=
|
||||
spv::BuiltIn::ClipDistance) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t structId = ann.GetSingleWordInOperand(0);
|
||||
const uint32_t member = ann.GetSingleWordInOperand(1);
|
||||
for (Instruction& inst : ctx->module()->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Input) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t pointee = VariablePointeeType(ctx, &inst);
|
||||
Instruction* pointeeType = defUse->GetDef(pointee);
|
||||
if (pointeeType == nullptr || pointeeType->opcode() != spv::Op::OpTypeArray ||
|
||||
pointeeType->GetSingleWordInOperand(0) != structId) {
|
||||
continue;
|
||||
}
|
||||
Instruction* structType = defUse->GetDef(structId);
|
||||
if (structType == nullptr || member >= structType->NumInOperands()) continue;
|
||||
glInVar = &inst;
|
||||
memberIndex = member;
|
||||
arrayTypeId = structType->GetSingleWordInOperand(member);
|
||||
break;
|
||||
}
|
||||
if (glInVar != nullptr) break;
|
||||
}
|
||||
if (glInVar == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t clipCount = ArrayLength(ctx, arrayTypeId);
|
||||
const uint32_t vertexCount = ArrayLength(ctx, VariablePointeeType(ctx, glInVar));
|
||||
if (clipCount == 0 || vertexCount == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Every gl_in chain that selects the ClipDistance member:
|
||||
// (vertex, member) yields a whole float[N], (vertex, member, k) an element.
|
||||
std::vector<Instruction*> chains;
|
||||
defUse->ForEachUser(glInVar, [&](Instruction* user) {
|
||||
if (IsAccessChain(user) && user->GetSingleWordInOperand(0) == glInVar->result_id() &&
|
||||
user->NumInOperands() >= 3 &&
|
||||
IsConstantWithValue(ctx, user->GetSingleWordInOperand(2), memberIndex)) {
|
||||
chains.push_back(user);
|
||||
}
|
||||
});
|
||||
if (chains.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Instruction* arrayTypeInst = defUse->GetDef(arrayTypeId);
|
||||
const uint32_t elemTypeId = arrayTypeInst->GetSingleWordInOperand(0);
|
||||
|
||||
// Private mg_ClipDistanceIn = float[vertexCount][clipCount].
|
||||
const uint32_t vertexCountConst = IntConstant(ctx, false, vertexCount);
|
||||
analysis::Type* innerType = typeMgr->GetType(arrayTypeId);
|
||||
analysis::Array outerArray(
|
||||
innerType, analysis::Array::LengthInfo{
|
||||
vertexCountConst,
|
||||
{analysis::Array::LengthInfo::kConstant, vertexCount}});
|
||||
const uint32_t outerArrayTypeId = typeMgr->GetTypeInstruction(&outerArray);
|
||||
Instruction* mgInVar = AddPrivateVariable(ctx, outerArrayTypeId, "mg_ClipDistanceIn");
|
||||
const uint32_t mgInVarId = mgInVar->result_id();
|
||||
|
||||
// Copy loop at the top of the entry point:
|
||||
// for (uint t = 0; t < vertexCount * clipCount; ++t)
|
||||
// mg_ClipDistanceIn[t / clipCount][t % clipCount] =
|
||||
// gl_in[t / clipCount].gl_ClipDistance[t % clipCount];
|
||||
// Both gl_in indices are loop-derived (dynamic): constant-index element reads
|
||||
// miscompile and whole-array reads crash the Adreno compiler.
|
||||
const uint32_t entryFuncId = EntryFunctionId(ctx);
|
||||
Function* entryFn = nullptr;
|
||||
for (Function& function : *ctx->module()) {
|
||||
if (function.result_id() == entryFuncId) {
|
||||
entryFn = &function;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (entryFn == nullptr || entryFn->begin() == entryFn->end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t uintTypeId = UintType(ctx);
|
||||
const uint32_t boolTypeId = BoolType(ctx);
|
||||
const uint32_t ptrFnUint = PointerTypeTo(ctx, uintTypeId, spv::StorageClass::Function);
|
||||
const uint32_t ptrInElem = PointerTypeTo(ctx, elemTypeId, spv::StorageClass::Input);
|
||||
const uint32_t ptrPrivElem = PointerTypeTo(ctx, elemTypeId, spv::StorageClass::Private);
|
||||
const uint32_t uint0 = IntConstant(ctx, false, 0);
|
||||
const uint32_t uint1 = IntConstant(ctx, false, 1);
|
||||
const uint32_t uintN = IntConstant(ctx, false, clipCount);
|
||||
const uint32_t uintTotal = IntConstant(ctx, false, vertexCount * clipCount);
|
||||
const uint32_t memberConst = IntConstant(ctx, true, memberIndex);
|
||||
|
||||
BasicBlock* entryBlock = &*entryFn->begin();
|
||||
auto splitPoint = entryBlock->begin();
|
||||
while (splitPoint != entryBlock->end() &&
|
||||
splitPoint->opcode() == spv::Op::OpVariable) {
|
||||
++splitPoint;
|
||||
}
|
||||
|
||||
// Loop counter lives with the other function-local variables.
|
||||
const uint32_t counterVarId = ctx->TakeNextId();
|
||||
splitPoint->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpVariable, ptrFnUint, counterVarId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{static_cast<uint32_t>(spv::StorageClass::Function)}}}));
|
||||
|
||||
const uint32_t restLabelId = ctx->TakeNextId();
|
||||
BasicBlock* restBlock = entryBlock->SplitBasicBlock(ctx, restLabelId, splitPoint);
|
||||
|
||||
const uint32_t headerLabelId = ctx->TakeNextId();
|
||||
const uint32_t checkLabelId = ctx->TakeNextId();
|
||||
const uint32_t bodyLabelId = ctx->TakeNextId();
|
||||
const uint32_t continueLabelId = ctx->TakeNextId();
|
||||
|
||||
auto makeBlock = [&](uint32_t labelId) {
|
||||
return spvtools::MakeUnique<BasicBlock>(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLabel, 0, labelId, std::initializer_list<Operand>{}));
|
||||
};
|
||||
auto addInst = [&](BasicBlock* block, spv::Op opcode, uint32_t typeId,
|
||||
uint32_t resultId, std::vector<Operand> operands) {
|
||||
block->AddInstruction(spvtools::MakeUnique<Instruction>(
|
||||
ctx, opcode, typeId, resultId, std::move(operands)));
|
||||
};
|
||||
|
||||
// entry: t = 0; branch header
|
||||
addInst(entryBlock, spv::Op::OpStore, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}, {SPV_OPERAND_TYPE_ID, {uint0}}});
|
||||
addInst(entryBlock, spv::Op::OpBranch, 0, 0, {{SPV_OPERAND_TYPE_ID, {headerLabelId}}});
|
||||
|
||||
// header: structured loop header
|
||||
auto headerBlock = makeBlock(headerLabelId);
|
||||
addInst(headerBlock.get(), spv::Op::OpLoopMerge, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {restLabelId}},
|
||||
{SPV_OPERAND_TYPE_ID, {continueLabelId}},
|
||||
{SPV_OPERAND_TYPE_LOOP_CONTROL,
|
||||
{static_cast<uint32_t>(spv::LoopControlMask::MaskNone)}}});
|
||||
addInst(headerBlock.get(), spv::Op::OpBranch, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {checkLabelId}}});
|
||||
|
||||
// check: t < vertexCount * clipCount ?
|
||||
auto checkBlock = makeBlock(checkLabelId);
|
||||
const uint32_t tCheckId = ctx->TakeNextId();
|
||||
addInst(checkBlock.get(), spv::Op::OpLoad, uintTypeId, tCheckId,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
|
||||
const uint32_t condId = ctx->TakeNextId();
|
||||
addInst(checkBlock.get(), spv::Op::OpULessThan, boolTypeId, condId,
|
||||
{{SPV_OPERAND_TYPE_ID, {tCheckId}}, {SPV_OPERAND_TYPE_ID, {uintTotal}}});
|
||||
addInst(checkBlock.get(), spv::Op::OpBranchConditional, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {condId}},
|
||||
{SPV_OPERAND_TYPE_ID, {bodyLabelId}},
|
||||
{SPV_OPERAND_TYPE_ID, {restLabelId}}});
|
||||
|
||||
// body: mg_ClipDistanceIn[t / N][t % N] = gl_in[t / N].gl_ClipDistance[t % N]
|
||||
auto bodyBlock = makeBlock(bodyLabelId);
|
||||
const uint32_t tBodyId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpLoad, uintTypeId, tBodyId,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
|
||||
const uint32_t vertexIdxId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpUDiv, uintTypeId, vertexIdxId,
|
||||
{{SPV_OPERAND_TYPE_ID, {tBodyId}}, {SPV_OPERAND_TYPE_ID, {uintN}}});
|
||||
const uint32_t clipIdxId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpUMod, uintTypeId, clipIdxId,
|
||||
{{SPV_OPERAND_TYPE_ID, {tBodyId}}, {SPV_OPERAND_TYPE_ID, {uintN}}});
|
||||
const uint32_t srcChainId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpAccessChain, ptrInElem, srcChainId,
|
||||
{{SPV_OPERAND_TYPE_ID, {glInVar->result_id()}},
|
||||
{SPV_OPERAND_TYPE_ID, {vertexIdxId}},
|
||||
{SPV_OPERAND_TYPE_ID, {memberConst}},
|
||||
{SPV_OPERAND_TYPE_ID, {clipIdxId}}});
|
||||
const uint32_t valId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpLoad, elemTypeId, valId,
|
||||
{{SPV_OPERAND_TYPE_ID, {srcChainId}}});
|
||||
const uint32_t dstChainId = ctx->TakeNextId();
|
||||
addInst(bodyBlock.get(), spv::Op::OpAccessChain, ptrPrivElem, dstChainId,
|
||||
{{SPV_OPERAND_TYPE_ID, {mgInVarId}},
|
||||
{SPV_OPERAND_TYPE_ID, {vertexIdxId}},
|
||||
{SPV_OPERAND_TYPE_ID, {clipIdxId}}});
|
||||
addInst(bodyBlock.get(), spv::Op::OpStore, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {dstChainId}}, {SPV_OPERAND_TYPE_ID, {valId}}});
|
||||
addInst(bodyBlock.get(), spv::Op::OpBranch, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {continueLabelId}}});
|
||||
|
||||
// continue: ++t
|
||||
auto continueBlock = makeBlock(continueLabelId);
|
||||
const uint32_t tContinueId = ctx->TakeNextId();
|
||||
addInst(continueBlock.get(), spv::Op::OpLoad, uintTypeId, tContinueId,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}});
|
||||
const uint32_t tIncId = ctx->TakeNextId();
|
||||
addInst(continueBlock.get(), spv::Op::OpIAdd, uintTypeId, tIncId,
|
||||
{{SPV_OPERAND_TYPE_ID, {tContinueId}}, {SPV_OPERAND_TYPE_ID, {uint1}}});
|
||||
addInst(continueBlock.get(), spv::Op::OpStore, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {counterVarId}}, {SPV_OPERAND_TYPE_ID, {tIncId}}});
|
||||
addInst(continueBlock.get(), spv::Op::OpBranch, 0, 0,
|
||||
{{SPV_OPERAND_TYPE_ID, {headerLabelId}}});
|
||||
|
||||
BasicBlock* headerPtr = entryFn->InsertBasicBlockBefore(std::move(headerBlock), restBlock);
|
||||
BasicBlock* checkPtr = entryFn->InsertBasicBlockAfter(std::move(checkBlock), headerPtr);
|
||||
BasicBlock* bodyPtr = entryFn->InsertBasicBlockAfter(std::move(bodyBlock), checkPtr);
|
||||
entryFn->InsertBasicBlockAfter(std::move(continueBlock), bodyPtr);
|
||||
|
||||
// Redirect the pre-existing accesses to the shadow copy.
|
||||
for (Instruction* chain : chains) {
|
||||
if (chain->NumInOperands() == 3) {
|
||||
// (vertex, member): whole float[N] of one vertex.
|
||||
Instruction* chainPtrType = defUse->GetDef(chain->type_id());
|
||||
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
|
||||
const uint32_t newPtrType =
|
||||
PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
|
||||
ctx->ForgetUses(chain);
|
||||
std::vector<Operand> newOperands;
|
||||
newOperands.push_back({SPV_OPERAND_TYPE_ID, {mgInVarId}});
|
||||
newOperands.push_back(chain->GetInOperand(1));
|
||||
chain->SetResultType(newPtrType);
|
||||
chain->SetInOperands(std::move(newOperands));
|
||||
ctx->AnalyzeUses(chain);
|
||||
} else {
|
||||
// (vertex, member, k, ...): drop the member index.
|
||||
Instruction* chainPtrType = defUse->GetDef(chain->type_id());
|
||||
const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1);
|
||||
const uint32_t newPtrType =
|
||||
PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private);
|
||||
ctx->ForgetUses(chain);
|
||||
std::vector<Operand> newOperands;
|
||||
newOperands.push_back({SPV_OPERAND_TYPE_ID, {mgInVarId}});
|
||||
newOperands.push_back(chain->GetInOperand(1));
|
||||
for (uint32_t i = 3; i < chain->NumInOperands(); ++i) {
|
||||
newOperands.push_back(chain->GetInOperand(i));
|
||||
}
|
||||
chain->SetResultType(newPtrType);
|
||||
chain->SetInOperands(std::move(newOperands));
|
||||
ctx->AnalyzeUses(chain);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status LowerClipDistanceForEsslPass::Process() {
|
||||
auto* ctx = context();
|
||||
const spv::ExecutionModel model = EntryExecutionModel(ctx);
|
||||
const bool isVertex = model == spv::ExecutionModel::Vertex;
|
||||
const bool isGeometry = model == spv::ExecutionModel::Geometry;
|
||||
if (!isVertex && !isGeometry) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
bool changed = LowerOutputClipDistance(ctx, isGeometry);
|
||||
if (isGeometry) {
|
||||
changed |= LowerInputClipDistance(ctx);
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
ctx->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
LowerClipDistanceForEsslPass::CreateLowerClipDistanceForEsslPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<LowerClipDistanceForEsslPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -1,44 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Adreno's ESSL compiler mishandles gl_ClipDistance (device-verified on Adreno 750):
|
||||
// - writes through non-constant indices silently fail to link,
|
||||
// - reads of gl_in[i].gl_ClipDistance[k] with a CONSTANT k >= 1 fail to compile
|
||||
// ("array indexing out of boundary") while dynamic-index reads work,
|
||||
// - compiling a whole-array read of gl_in[i].gl_ClipDistance segfaults the
|
||||
// compiler backend (libllvm-qgl.so).
|
||||
// This pass shadows the builtin so the decompiled ESSL only ever touches it in the
|
||||
// shapes Adreno accepts. Output side (vertex + geometry): all accesses to the
|
||||
// Output ClipDistance (gl_PerVertex member or standalone variable) are redirected
|
||||
// to a Private mg_ClipDistance array, and a flush writing the real builtin with
|
||||
// literal constant indices is inserted before every OpEmitVertex (geometry) or
|
||||
// every return of the entry point (vertex). Input side (geometry): accesses to
|
||||
// gl_in[...].gl_ClipDistance are redirected to a Private mg_ClipDistanceIn
|
||||
// array-of-arrays filled once at the top of the entry point by a structured loop
|
||||
// whose gl_in reads use dynamic (loop-variable) indices. The builtin members stay
|
||||
// statically referenced by the flush/copy so cross-stage IO matching is intact.
|
||||
// Only meant for the DirectGLES transpile path on Qualcomm devices.
|
||||
class LowerClipDistanceForEsslPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "lower-clip-distance-for-essl"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateLowerClipDistanceForEsslPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -87,7 +87,7 @@ def main():
|
||||
ap.add_argument("--device-dir", default="/data/local/tmp/mgcts")
|
||||
ap.add_argument("--surface", default="fbo", help="--deqp-surface-type value")
|
||||
ap.add_argument("--max-rounds", type=int, default=4000)
|
||||
ap.add_argument("--max-empty-streak", type=int, default=8,
|
||||
ap.add_argument("--max-empty-streak", type=int, default=64,
|
||||
help="abort after this many consecutive chunks that produce no log at all")
|
||||
ap.add_argument("--min-mem-kb", type=int, default=400000,
|
||||
help="pause when the device drops below this much available memory")
|
||||
|
||||
Reference in New Issue
Block a user