mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[Perf] (DirectGLES): memoize the SPIR-V to ESSL transpile per stage (translation cache L2)
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
#include "BackendObject_DirectGLES.h"
|
||||
#include <Config.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||
@@ -81,6 +82,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so this is the floor MobileGL
|
||||
// advertises whatever the ES driver reports. It steers ClampMultisampleFetchesForEssl
|
||||
// AND is part of the L2 translation-memo key, so the transpile and the key must read
|
||||
// the same constant - hence one definition rather than two locals.
|
||||
// Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples(): this is
|
||||
// backend code and must not reach into the GL frontend. 4 is that translation unit's
|
||||
// kFrontendMaxSamples, which is the source of truth - keep the two in step.
|
||||
constexpr Int kFrontendMaxSamples = 4;
|
||||
|
||||
static Uint ResolveBackendEsslVersion() {
|
||||
const auto& version = g_GLESCapabilities.GLESVersion;
|
||||
if (version.Major > 3 || (version.Major == 3 && version.Minor >= 2)) {
|
||||
@@ -4751,6 +4761,283 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return ComputeImageUnitFormatSignature() == m_imageUnitFormatSignature;
|
||||
}
|
||||
|
||||
|
||||
// ===== THE MEMOIZED SEGMENT (shader translation memo, level 2) =====
|
||||
//
|
||||
// One stage's sanitized SPIR-V turned into the ESSL SPIRV-Cross emits, through the
|
||||
// DirectGLES-specific pass chain. Extracted out of SyncToBackend's loop so that the
|
||||
// boundary the L2 memo keys on is a function signature rather than a comment: every
|
||||
// input this reads is either an argument below or a process-global capability bit,
|
||||
// and EVERY ONE OF THEM IS IN EsslTranslationKeyInputs. If you add a read here, add
|
||||
// it to BuildEsslTranslationKey too - an under-specified key here is a silently
|
||||
// miscompiled shader.
|
||||
//
|
||||
// Reads (audited): the arguments; g_GLESCapabilities.{SupportsViewportArray,
|
||||
// MaxSamples, MaxColorTextureSamples, MaxIntegerSamples, MaxDepthTextureSamples,
|
||||
// SupportsNoperspectiveInterpolation, GLESVersion} (the last via
|
||||
// ResolveBackendEsslVersion); and m_backendProgramId, for a log line only.
|
||||
//
|
||||
// Deliberately NOT in here, and therefore NOT in the key: the text-level passes that
|
||||
// follow in SyncToBackend. They are cheap string work and they read a long tail of
|
||||
// live per-program state (RebindImageUniformsToFrontendUnits walks the ProgramObject
|
||||
// reflection, the norm-clamp masks and the fragColor broadcast count are live
|
||||
// globals, the buffer-texture tier retargets an #extension line) whose inclusion
|
||||
// would make the key both enormous and fragile for no measurable saving.
|
||||
//
|
||||
// Returns false when SPIRV-Cross refused the module; `outError` then holds its
|
||||
// message and nothing is memoized.
|
||||
Bool BackendProgramObjectImpl::TranspileSpirvToEssl(
|
||||
const Vector<unsigned int>& spirvCode, const GLenum glShaderType,
|
||||
const std::set<String>& xfbCaptureBlockNames, const ImageFormatBakeInputs& imageFormatBake,
|
||||
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
|
||||
const Bool enableSpirvValidation, String& outSource,
|
||||
std::set<String>& outFlattenedXfbBlockNames, String& outError) const {
|
||||
// ESSL cannot express gl_DrawID/gl_BaseInstance/gl_BaseVertex; demote them to
|
||||
// plain globals (mg_*) before handing the module to SPIRV-Cross.
|
||||
Vector<unsigned int> loweredSpirv;
|
||||
const Vector<unsigned int>* effectiveSpirv = &spirvCode;
|
||||
if (glShaderType == GL_VERTEX_SHADER &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv, enableSpirvValidation) &&
|
||||
!loweredSpirv.empty()) {
|
||||
effectiveSpirv = &loweredSpirv;
|
||||
}
|
||||
|
||||
// ESSL cannot express gl_ViewportIndex either, but unlike the draw parameters
|
||||
// there IS an extension that provides it - so this runs only when the driver does
|
||||
// NOT advertise GL_OES_viewport_array. A driver that does keeps the builtin and
|
||||
// gets the `#extension` request added to the decompiled source below instead.
|
||||
// Demoting the builtin costs the multi-viewport routing (every invocation lands in
|
||||
// viewport 0), which is the degradation ViewportArrayScenario already documents
|
||||
// for this backend; NOT demoting it costs the whole program, because the stage
|
||||
// fails to compile and every draw made with it silently renders nothing.
|
||||
// Gated on the module actually declaring the output, so no other stage pays an
|
||||
// optimizer round trip for it.
|
||||
// One parse of the module answers every armed pass gate below. The per-gate
|
||||
// Declares* probes each cost a BuildModule per stage, and on a driver where both
|
||||
// gates are armed (Mali: no GL_OES_viewport_array AND integer multisample
|
||||
// squeezed to 1) the doubled parse made compile-heavy workloads ~10% slower.
|
||||
// Probing the pre-lowering module is sound for both gates: demoting
|
||||
// gl_ViewportIndex neither adds nor removes multisampled image types.
|
||||
// Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples():
|
||||
// this is backend code and must not reach into the GL frontend. 4 is that
|
||||
// translation unit's kFrontendMaxSamples, which is the source of truth -
|
||||
// keep the two in step.
|
||||
const Int advertisedMaxSamples =
|
||||
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
|
||||
const Bool viewportLoweringArmed = !g_GLESCapabilities.SupportsViewportArray;
|
||||
const Bool sampleClampArmed =
|
||||
g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples ||
|
||||
g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples ||
|
||||
g_GLESCapabilities.MaxDepthTextureSamples < advertisedMaxSamples;
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvGateFeatures spirvGates;
|
||||
if (viewportLoweringArmed || sampleClampArmed) {
|
||||
spirvGates = MG_Util::ShaderTranspiler::ShaderCompiler::ProbeSpirvGateFeatures(
|
||||
*effectiveSpirv);
|
||||
}
|
||||
|
||||
Vector<unsigned int> loweredViewportSpirv;
|
||||
if (viewportLoweringArmed && spirvGates.WritesViewportIndexOutput &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::LowerViewportIndexForEssl(
|
||||
*effectiveSpirv, loweredViewportSpirv, enableSpirvValidation) &&
|
||||
!loweredViewportSpirv.empty()) {
|
||||
effectiveSpirv = &loweredViewportSpirv;
|
||||
MGLOG_D("Program %u stage %s writes gl_ViewportIndex, which this ES driver has "
|
||||
"no GL_OES_viewport_array for. The builtin was demoted to a plain "
|
||||
"global; every invocation renders into viewport 0.",
|
||||
m_backendProgramId,
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str());
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so every multisample
|
||||
// ceiling MobileGL advertises is floored to 4 no matter what the ES driver
|
||||
// reports - but the realised allocation cannot be, and
|
||||
// ClampSamplesToBackendSupport quietly gives an integer or depth multisample
|
||||
// texture the ONE sample Adreno and Mali actually support for it. A shader
|
||||
// written against the advertised ceiling then fetches a sample that storage does
|
||||
// not have and reads garbage; KHR-GL33/40/41.texture_swizzle.functional_* and
|
||||
// KHR-GLxx.texture_size_promotion.functional bake exactly that literal in. Clamp
|
||||
// the Sample operand to the backend-real per-category maximum so the fetch lands
|
||||
// inside the allocation. Gated on some category actually being squeezed AND the
|
||||
// module actually declaring a multisampled image, so no other stage pays an
|
||||
// optimizer round trip for it. DirectVulkan is deliberately not given this: it
|
||||
// allocates the sample count it was asked for, so its modules are already right.
|
||||
Vector<unsigned int> clampedSampleSpirv;
|
||||
if (sampleClampArmed && spirvGates.DeclaresMultisampledImage &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::ClampMultisampleFetchesForEssl(
|
||||
*effectiveSpirv, clampedSampleSpirv,
|
||||
g_GLESCapabilities.MaxColorTextureSamples,
|
||||
g_GLESCapabilities.MaxIntegerSamples,
|
||||
g_GLESCapabilities.MaxDepthTextureSamples, advertisedMaxSamples,
|
||||
enableSpirvValidation) &&
|
||||
!clampedSampleSpirv.empty()) {
|
||||
effectiveSpirv = &clampedSampleSpirv;
|
||||
}
|
||||
|
||||
// GLSL ES has no ARRAY vertex inputs, and SPIRV-Cross refuses the whole module
|
||||
// rather than emulating them, so this has to happen before it sees the binary.
|
||||
Vector<unsigned int> splitArrayInputSpirv;
|
||||
if (glShaderType == GL_VERTEX_SHADER &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SplitArrayVertexInputsForEssl(
|
||||
*effectiveSpirv, splitArrayInputSpirv, enableSpirvValidation) &&
|
||||
!splitArrayInputSpirv.empty() && splitArrayInputSpirv != *effectiveSpirv) {
|
||||
// Only when the pass ACTUALLY split something. The optimizer hands back a
|
||||
// re-serialised copy either way, and adopting that copy for every vertex
|
||||
// shader would put every one of them through a round trip they do not need
|
||||
// - which is not free: it cost the create-indirect retrace 0.15 SSIM the
|
||||
// first time this gate was missing.
|
||||
effectiveSpirv = &splitArrayInputSpirv;
|
||||
}
|
||||
|
||||
// Adopt the rewritten module only when THIS stage actually had one of the
|
||||
// blocks - the optimizer hands back a re-serialised copy either way, and taking
|
||||
// that copy for a module it did not rewrite is not free (it cost the
|
||||
// create-indirect retrace 0.15 SSIM when the array-input split first missed
|
||||
// this gate). The report has to be per stage, not cumulative: a fragment shader
|
||||
// consuming the same block reports a name the vertex stage already reported,
|
||||
// and its own rewrite must still be taken or the two stages stop matching.
|
||||
Vector<unsigned int> flattenedXfbSpirv;
|
||||
if (!xfbCaptureBlockNames.empty()) {
|
||||
// Reported into a local first, and published only if the module is really
|
||||
// adopted. The caller unions the published set unconditionally (so that a
|
||||
// cache HIT contributes its names too), so publishing a name for a rewrite
|
||||
// that was declined would rename a capture the emitted ESSL never renamed.
|
||||
std::set<String> flattenedNames;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(
|
||||
*effectiveSpirv, xfbCaptureBlockNames, flattenedNames, flattenedXfbSpirv,
|
||||
enableSpirvValidation) &&
|
||||
!flattenedXfbSpirv.empty() && !flattenedNames.empty()) {
|
||||
effectiveSpirv = &flattenedXfbSpirv;
|
||||
outFlattenedXfbBlockNames = Move(flattenedNames);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// ForceSupporterOutput swaps the fragment header to highp, that member reads
|
||||
// back as highp and the ES driver refuses to link ("definitions of uniform
|
||||
// block ... do not match"). Strip the hint from block structs so both stages
|
||||
// declare the member highp; nothing else about emission changes.
|
||||
Vector<unsigned int> uboPrecisionSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(
|
||||
*effectiveSpirv, uboPrecisionSpirv, enableSpirvValidation) &&
|
||||
!uboPrecisionSpirv.empty()) {
|
||||
effectiveSpirv = &uboPrecisionSpirv;
|
||||
}
|
||||
|
||||
// noperspective is core desktop GLSL and reaches here as the SPIR-V NoPerspective
|
||||
// decoration. SPIRV-Cross renders it as ESSL `noperspective` + `#extension
|
||||
// GL_NV_shader_noperspective_interpolation : require`; a driver without that extension
|
||||
// rejects the require. So on such devices emulate screen-linear interpolation instead
|
||||
// (pre-multiply outputs by gl_Position.w, recover inputs via gl_FragCoord.w) and drop
|
||||
// the decoration - exact, extension-free. Devices that have the extension keep the
|
||||
// decoration and let the hardware do it natively.
|
||||
Vector<unsigned int> noperspectiveSpirv;
|
||||
if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::EmulateNoPerspectiveForEssl(
|
||||
*effectiveSpirv, noperspectiveSpirv, enableSpirvValidation) &&
|
||||
!noperspectiveSpirv.empty()) {
|
||||
effectiveSpirv = &noperspectiveSpirv;
|
||||
}
|
||||
|
||||
// ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather
|
||||
// than approximating one. The shared pass turns the type into the 2D one and
|
||||
// divides the coordinate of every normalized-coordinate lookup by the texture
|
||||
// size, which is the whole of the difference between the two.
|
||||
Vector<unsigned int> rectLoweredSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv, enableSpirvValidation) &&
|
||||
!rectLoweredSpirv.empty()) {
|
||||
effectiveSpirv = &rectLoweredSpirv;
|
||||
}
|
||||
|
||||
// ES has no 1D texture at all, so a 1D ARRAY is stored as a 2D array with height
|
||||
// 1 (MapToBackendTextureTarget / GetBackendUploadSize). SPIRV-Cross emulates 1D
|
||||
// as 2D for images without ever asking whether the type is arrayed, so a
|
||||
// 1D-array image comes out as ivec2(ivec2(u, layer), 0) - three components in a
|
||||
// two-component constructor, which every driver rejects, taking the whole
|
||||
// program with it. The pass does the conversion properly - type to 2D array,
|
||||
// coordinate to (u, 0, layer) - before SPIRV-Cross can apply its own.
|
||||
Vector<unsigned int> arrayImageSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DArrayImagesForEssl(*effectiveSpirv,
|
||||
arrayImageSpirv, enableSpirvValidation) &&
|
||||
!arrayImageSpirv.empty()) {
|
||||
effectiveSpirv = &arrayImageSpirv;
|
||||
}
|
||||
|
||||
// GLSL ES has no format-less image: `writeonly uniform uimage2D` is legal desktop
|
||||
// GLSL 4.2 and an Adreno ES compile error ("all images have to define layout
|
||||
// format"), which loses the whole program. Give each such image the format the
|
||||
// application bound to its unit - the one GL's format-class rules make correct -
|
||||
// so SPIRV-Cross prints a qualifier. AFTER the 1D-array lowering above, which
|
||||
// also rewrites image types, so this one is looking at the final shapes.
|
||||
//
|
||||
// Gated on the module actually declaring one: the map is empty for every program
|
||||
// whose images all declare formats, and the cheap probe keeps a program that has
|
||||
// an unbound format-less image from paying an optimizer round trip per stage.
|
||||
Vector<unsigned int> imageFormatSpirv;
|
||||
if (!imageFormatBake.glFormatByUniformName.empty() &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresFormatlessStorageImage(*effectiveSpirv) &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::BakeImageFormatsForEssl(
|
||||
*effectiveSpirv, imageFormatBake.glFormatByUniformName, imageFormatSpirv,
|
||||
enableSpirvValidation) &&
|
||||
!imageFormatSpirv.empty()) {
|
||||
effectiveSpirv = &imageFormatSpirv;
|
||||
}
|
||||
|
||||
// GLSL ES demands a constant integral expression to index a fragment output
|
||||
// array; SPIR-V does not, so a shader that writes coeff[i] from a loop
|
||||
// reaches SPIRV-Cross intact and comes out as ESSL a strict driver rejects
|
||||
// outright ("array indexes for fragment outputs must be constant integral
|
||||
// expressions"), linking no program and silently no-oping every draw that
|
||||
// uses it. Mesa accepts it, ANGLE does not - which is the whole of the
|
||||
// improved-transparency-minecraft-26.3 failure. Fold or lower the index here,
|
||||
// on the ESSL path only: the same module is legal for DirectVulkan.
|
||||
Vector<unsigned int> outputIndexSpirv;
|
||||
if (glShaderType == GL_FRAGMENT_SHADER &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(
|
||||
*effectiveSpirv, outputIndexSpirv, enableSpirvValidation) &&
|
||||
!outputIndexSpirv.empty()) {
|
||||
effectiveSpirv = &outputIndexSpirv;
|
||||
}
|
||||
|
||||
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
||||
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||
|
||||
spvc_compiler_options options;
|
||||
spvcSession.CreateOptions(&options);
|
||||
|
||||
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION,
|
||||
ResolveBackendEsslVersion());
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
||||
|
||||
spvcSession.SetOptions(options);
|
||||
|
||||
// ES fixes a storage block's binding at link from its layout(binding=) qualifier
|
||||
// and has no glShaderStorageBlockBinding to move it afterwards, so a rebinding
|
||||
// can only be honoured by printing it INTO the qualifier. Rewriting the Binding
|
||||
// decoration before SPIRV-Cross emits is what does that; RemoveLayoutBinding
|
||||
// then deliberately preserves the qualifier for `buffer` declarations.
|
||||
if (!storageBlockBindingOverrides.empty()) { // empty for almost every program
|
||||
spvcSession.SetShaderStorageBlockBinding(storageBlockBindingOverrides);
|
||||
}
|
||||
|
||||
const char* result = nullptr;
|
||||
spvcSession.Compile(&result);
|
||||
|
||||
if (!result) {
|
||||
// The caller owns the diagnostic: it is the one that knows the
|
||||
// frontend program id, and a failed transpile must NOT be memoized -
|
||||
// the message names the stage and is worth re-emitting every time.
|
||||
const char* lastError = spvcSession.GetLastErrorString();
|
||||
outError = lastError ? lastError : "";
|
||||
return false;
|
||||
}
|
||||
|
||||
outSource = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
void BackendProgramObjectImpl::SyncToBackend(
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -4889,254 +5176,79 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ESSL cannot express gl_DrawID/gl_BaseInstance/gl_BaseVertex; demote them to
|
||||
// plain globals (mg_*) before handing the module to SPIRV-Cross.
|
||||
Vector<unsigned int> loweredSpirv;
|
||||
const Vector<unsigned int>* effectiveSpirv = &spirvCode;
|
||||
if (glShaderType == GL_VERTEX_SHADER &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::LowerDrawParametersForEssl(spirvCode, loweredSpirv, enableSpirvValidation) &&
|
||||
!loweredSpirv.empty()) {
|
||||
effectiveSpirv = &loweredSpirv;
|
||||
}
|
||||
|
||||
// ESSL cannot express gl_ViewportIndex either, but unlike the draw parameters
|
||||
// there IS an extension that provides it - so this runs only when the driver does
|
||||
// NOT advertise GL_OES_viewport_array. A driver that does keeps the builtin and
|
||||
// gets the `#extension` request added to the decompiled source below instead.
|
||||
// Demoting the builtin costs the multi-viewport routing (every invocation lands in
|
||||
// viewport 0), which is the degradation ViewportArrayScenario already documents
|
||||
// for this backend; NOT demoting it costs the whole program, because the stage
|
||||
// fails to compile and every draw made with it silently renders nothing.
|
||||
// Gated on the module actually declaring the output, so no other stage pays an
|
||||
// optimizer round trip for it.
|
||||
// One parse of the module answers every armed pass gate below. The per-gate
|
||||
// Declares* probes each cost a BuildModule per stage, and on a driver where both
|
||||
// gates are armed (Mali: no GL_OES_viewport_array AND integer multisample
|
||||
// squeezed to 1) the doubled parse made compile-heavy workloads ~10% slower.
|
||||
// Probing the pre-lowering module is sound for both gates: demoting
|
||||
// gl_ViewportIndex neither adds nor removes multisampled image types.
|
||||
// Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples():
|
||||
// this is backend code and must not reach into the GL frontend. 4 is that
|
||||
// translation unit's kFrontendMaxSamples, which is the source of truth -
|
||||
// keep the two in step.
|
||||
constexpr Int kFrontendMaxSamples = 4;
|
||||
const Int advertisedMaxSamples =
|
||||
// ---- L2 of the shader translation memo -------------------------------
|
||||
// The whole DirectGLES SPIR-V pass chain plus SPIRV-Cross for this stage,
|
||||
// memoized on the module bytes and on every capability bit and per-program
|
||||
// input that steers them. See TranslationCache.h for the key inventory and
|
||||
// for why the text-level passes below stay outside the boundary.
|
||||
MG_Util::ShaderTranspiler::EsslTranslationKeyInputs esslKeyInputs;
|
||||
esslKeyInputs.spirv = &spirvCode;
|
||||
esslKeyInputs.shaderType = glShaderType;
|
||||
esslKeyInputs.supportsViewportArray = g_GLESCapabilities.SupportsViewportArray;
|
||||
esslKeyInputs.supportsNoperspectiveInterpolation =
|
||||
g_GLESCapabilities.SupportsNoperspectiveInterpolation;
|
||||
esslKeyInputs.maxColorTextureSamples = g_GLESCapabilities.MaxColorTextureSamples;
|
||||
esslKeyInputs.maxIntegerSamples = g_GLESCapabilities.MaxIntegerSamples;
|
||||
esslKeyInputs.maxDepthTextureSamples = g_GLESCapabilities.MaxDepthTextureSamples;
|
||||
esslKeyInputs.advertisedMaxSamples =
|
||||
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
|
||||
const Bool viewportLoweringArmed = !g_GLESCapabilities.SupportsViewportArray;
|
||||
const Bool sampleClampArmed =
|
||||
g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples ||
|
||||
g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples ||
|
||||
g_GLESCapabilities.MaxDepthTextureSamples < advertisedMaxSamples;
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvGateFeatures spirvGates;
|
||||
if (viewportLoweringArmed || sampleClampArmed) {
|
||||
spirvGates = MG_Util::ShaderTranspiler::ShaderCompiler::ProbeSpirvGateFeatures(
|
||||
*effectiveSpirv);
|
||||
esslKeyInputs.xfbCaptureBlockNames = &xfbCaptureBlockNames;
|
||||
esslKeyInputs.glFormatByUniformName = &imageFormatBake.glFormatByUniformName;
|
||||
esslKeyInputs.storageBlockBindingOverrides = &storageBlockBindingOverrides;
|
||||
esslKeyInputs.esslVersion = ResolveBackendEsslVersion();
|
||||
esslKeyInputs.enableSpirvValidation = enableSpirvValidation;
|
||||
|
||||
auto& esslCache = MG_Util::ShaderTranspiler::GetEsslTranslationCache();
|
||||
MG_Util::ShaderTranspiler::TranslationCacheKey esslCacheKey;
|
||||
if (MG_Util::ShaderTranspiler::ShaderTranslationCacheEnabled()) {
|
||||
esslCacheKey = MG_Util::ShaderTranspiler::BuildEsslTranslationKey(esslKeyInputs);
|
||||
}
|
||||
|
||||
Vector<unsigned int> loweredViewportSpirv;
|
||||
if (viewportLoweringArmed && spirvGates.WritesViewportIndexOutput &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::LowerViewportIndexForEssl(
|
||||
*effectiveSpirv, loweredViewportSpirv, enableSpirvValidation) &&
|
||||
!loweredViewportSpirv.empty()) {
|
||||
effectiveSpirv = &loweredViewportSpirv;
|
||||
MGLOG_D("Program %u stage %s writes gl_ViewportIndex, which this ES driver has "
|
||||
"no GL_OES_viewport_array for. The builtin was demoted to a plain "
|
||||
"global; every invocation renders into viewport 0.",
|
||||
m_backendProgramId,
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str());
|
||||
}
|
||||
|
||||
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so every multisample
|
||||
// ceiling MobileGL advertises is floored to 4 no matter what the ES driver
|
||||
// reports - but the realised allocation cannot be, and
|
||||
// ClampSamplesToBackendSupport quietly gives an integer or depth multisample
|
||||
// texture the ONE sample Adreno and Mali actually support for it. A shader
|
||||
// written against the advertised ceiling then fetches a sample that storage does
|
||||
// not have and reads garbage; KHR-GL33/40/41.texture_swizzle.functional_* and
|
||||
// KHR-GLxx.texture_size_promotion.functional bake exactly that literal in. Clamp
|
||||
// the Sample operand to the backend-real per-category maximum so the fetch lands
|
||||
// inside the allocation. Gated on some category actually being squeezed AND the
|
||||
// module actually declaring a multisampled image, so no other stage pays an
|
||||
// optimizer round trip for it. DirectVulkan is deliberately not given this: it
|
||||
// allocates the sample count it was asked for, so its modules are already right.
|
||||
Vector<unsigned int> clampedSampleSpirv;
|
||||
if (sampleClampArmed && spirvGates.DeclaresMultisampledImage &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::ClampMultisampleFetchesForEssl(
|
||||
*effectiveSpirv, clampedSampleSpirv,
|
||||
g_GLESCapabilities.MaxColorTextureSamples,
|
||||
g_GLESCapabilities.MaxIntegerSamples,
|
||||
g_GLESCapabilities.MaxDepthTextureSamples, advertisedMaxSamples,
|
||||
enableSpirvValidation) &&
|
||||
!clampedSampleSpirv.empty()) {
|
||||
effectiveSpirv = &clampedSampleSpirv;
|
||||
}
|
||||
|
||||
// GLSL ES has no ARRAY vertex inputs, and SPIRV-Cross refuses the whole module
|
||||
// rather than emulating them, so this has to happen before it sees the binary.
|
||||
Vector<unsigned int> splitArrayInputSpirv;
|
||||
if (glShaderType == GL_VERTEX_SHADER &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::SplitArrayVertexInputsForEssl(
|
||||
*effectiveSpirv, splitArrayInputSpirv, enableSpirvValidation) &&
|
||||
!splitArrayInputSpirv.empty() && splitArrayInputSpirv != *effectiveSpirv) {
|
||||
// Only when the pass ACTUALLY split something. The optimizer hands back a
|
||||
// re-serialised copy either way, and adopting that copy for every vertex
|
||||
// shader would put every one of them through a round trip they do not need
|
||||
// - which is not free: it cost the create-indirect retrace 0.15 SSIM the
|
||||
// first time this gate was missing.
|
||||
effectiveSpirv = &splitArrayInputSpirv;
|
||||
}
|
||||
|
||||
// Adopt the rewritten module only when THIS stage actually had one of the
|
||||
// blocks - the optimizer hands back a re-serialised copy either way, and taking
|
||||
// that copy for a module it did not rewrite is not free (it cost the
|
||||
// create-indirect retrace 0.15 SSIM when the array-input split first missed
|
||||
// this gate). The report has to be per stage, not cumulative: a fragment shader
|
||||
// consuming the same block reports a name the vertex stage already reported,
|
||||
// and its own rewrite must still be taken or the two stages stop matching.
|
||||
Vector<unsigned int> flattenedXfbSpirv;
|
||||
std::set<String> stageFlattenedXfbBlockNames;
|
||||
if (!xfbCaptureBlockNames.empty() &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(
|
||||
*effectiveSpirv, xfbCaptureBlockNames, stageFlattenedXfbBlockNames,
|
||||
flattenedXfbSpirv, enableSpirvValidation) &&
|
||||
!flattenedXfbSpirv.empty() && !stageFlattenedXfbBlockNames.empty()) {
|
||||
effectiveSpirv = &flattenedXfbSpirv;
|
||||
flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(),
|
||||
stageFlattenedXfbBlockNames.end());
|
||||
const MG_Util::ShaderTranspiler::EsslTranslationResultPtr esslHit =
|
||||
esslCacheKey.Valid() ? esslCache.Find(esslCacheKey) : nullptr;
|
||||
if (esslHit) {
|
||||
source = esslHit->essl;
|
||||
stageFlattenedXfbBlockNames = esslHit->flattenedXfbBlockNames;
|
||||
} else {
|
||||
String transpileError;
|
||||
if (!TranspileSpirvToEssl(spirvCode, glShaderType, xfbCaptureBlockNames,
|
||||
imageFormatBake, storageBlockBindingOverrides,
|
||||
enableSpirvValidation, source,
|
||||
stageFlattenedXfbBlockNames, transpileError)) {
|
||||
// MGLOG_E, unlatched, like the compile- and link-failure diagnostics
|
||||
// below: one line per failing stage is bounded by program count and
|
||||
// naming the stage is the entire diagnostic value. A stage that never
|
||||
// reaches the driver leaves the program short of that stage, so the link
|
||||
// fails with an EMPTY driver info log - the least debuggable failure
|
||||
// MobileGL can produce, and what hid the whole
|
||||
// KHR-GL43.vertex_attrib_binding family behind "the draw captured zeros".
|
||||
MGLOG_E("Shader transpilation to ESSL failed. State program ID: %u, stage: %s, "
|
||||
"SPIRV-Cross error: %s",
|
||||
stateProgramObject->GetExternalIndex(),
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
|
||||
transpileError.c_str());
|
||||
m_backendProgramUsable = false;
|
||||
continue;
|
||||
}
|
||||
if (esslCacheKey.Valid()) {
|
||||
auto payload = MakeShared<MG_Util::ShaderTranspiler::EsslTranslationResult>();
|
||||
payload->essl = source;
|
||||
payload->flattenedXfbBlockNames = stageFlattenedXfbBlockNames;
|
||||
const SizeT payloadBytes =
|
||||
MG_Util::ShaderTranspiler::EsslTranslationResultBytes(*payload);
|
||||
esslCache.Insert(
|
||||
esslCacheKey,
|
||||
MG_Util::ShaderTranspiler::EsslTranslationResultPtr(Move(payload)),
|
||||
payloadBytes);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// ForceSupporterOutput swaps the fragment header to highp, that member reads
|
||||
// back as highp and the ES driver refuses to link ("definitions of uniform
|
||||
// block ... do not match"). Strip the hint from block structs so both stages
|
||||
// declare the member highp; nothing else about emission changes.
|
||||
Vector<unsigned int> uboPrecisionSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(
|
||||
*effectiveSpirv, uboPrecisionSpirv, enableSpirvValidation) &&
|
||||
!uboPrecisionSpirv.empty()) {
|
||||
effectiveSpirv = &uboPrecisionSpirv;
|
||||
}
|
||||
|
||||
// noperspective is core desktop GLSL and reaches here as the SPIR-V NoPerspective
|
||||
// decoration. SPIRV-Cross renders it as ESSL `noperspective` + `#extension
|
||||
// GL_NV_shader_noperspective_interpolation : require`; a driver without that extension
|
||||
// rejects the require. So on such devices emulate screen-linear interpolation instead
|
||||
// (pre-multiply outputs by gl_Position.w, recover inputs via gl_FragCoord.w) and drop
|
||||
// the decoration - exact, extension-free. Devices that have the extension keep the
|
||||
// decoration and let the hardware do it natively.
|
||||
Vector<unsigned int> noperspectiveSpirv;
|
||||
if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::EmulateNoPerspectiveForEssl(
|
||||
*effectiveSpirv, noperspectiveSpirv, enableSpirvValidation) &&
|
||||
!noperspectiveSpirv.empty()) {
|
||||
effectiveSpirv = &noperspectiveSpirv;
|
||||
}
|
||||
|
||||
// ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather
|
||||
// than approximating one. The shared pass turns the type into the 2D one and
|
||||
// divides the coordinate of every normalized-coordinate lookup by the texture
|
||||
// size, which is the whole of the difference between the two.
|
||||
Vector<unsigned int> rectLoweredSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv, enableSpirvValidation) &&
|
||||
!rectLoweredSpirv.empty()) {
|
||||
effectiveSpirv = &rectLoweredSpirv;
|
||||
}
|
||||
|
||||
// ES has no 1D texture at all, so a 1D ARRAY is stored as a 2D array with height
|
||||
// 1 (MapToBackendTextureTarget / GetBackendUploadSize). SPIRV-Cross emulates 1D
|
||||
// as 2D for images without ever asking whether the type is arrayed, so a
|
||||
// 1D-array image comes out as ivec2(ivec2(u, layer), 0) - three components in a
|
||||
// two-component constructor, which every driver rejects, taking the whole
|
||||
// program with it. The pass does the conversion properly - type to 2D array,
|
||||
// coordinate to (u, 0, layer) - before SPIRV-Cross can apply its own.
|
||||
Vector<unsigned int> arrayImageSpirv;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::Lower1DArrayImagesForEssl(*effectiveSpirv,
|
||||
arrayImageSpirv, enableSpirvValidation) &&
|
||||
!arrayImageSpirv.empty()) {
|
||||
effectiveSpirv = &arrayImageSpirv;
|
||||
}
|
||||
|
||||
// GLSL ES has no format-less image: `writeonly uniform uimage2D` is legal desktop
|
||||
// GLSL 4.2 and an Adreno ES compile error ("all images have to define layout
|
||||
// format"), which loses the whole program. Give each such image the format the
|
||||
// application bound to its unit - the one GL's format-class rules make correct -
|
||||
// so SPIRV-Cross prints a qualifier. AFTER the 1D-array lowering above, which
|
||||
// also rewrites image types, so this one is looking at the final shapes.
|
||||
//
|
||||
// Gated on the module actually declaring one: the map is empty for every program
|
||||
// whose images all declare formats, and the cheap probe keeps a program that has
|
||||
// an unbound format-less image from paying an optimizer round trip per stage.
|
||||
Vector<unsigned int> imageFormatSpirv;
|
||||
if (!imageFormatBake.glFormatByUniformName.empty() &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresFormatlessStorageImage(*effectiveSpirv) &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::BakeImageFormatsForEssl(
|
||||
*effectiveSpirv, imageFormatBake.glFormatByUniformName, imageFormatSpirv,
|
||||
enableSpirvValidation) &&
|
||||
!imageFormatSpirv.empty()) {
|
||||
effectiveSpirv = &imageFormatSpirv;
|
||||
}
|
||||
|
||||
// GLSL ES demands a constant integral expression to index a fragment output
|
||||
// array; SPIR-V does not, so a shader that writes coeff[i] from a loop
|
||||
// reaches SPIRV-Cross intact and comes out as ESSL a strict driver rejects
|
||||
// outright ("array indexes for fragment outputs must be constant integral
|
||||
// expressions"), linking no program and silently no-oping every draw that
|
||||
// uses it. Mesa accepts it, ANGLE does not - which is the whole of the
|
||||
// improved-transparency-minecraft-26.3 failure. Fold or lower the index here,
|
||||
// on the ESSL path only: the same module is legal for DirectVulkan.
|
||||
Vector<unsigned int> outputIndexSpirv;
|
||||
if (glShaderType == GL_FRAGMENT_SHADER &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(
|
||||
*effectiveSpirv, outputIndexSpirv, enableSpirvValidation) &&
|
||||
!outputIndexSpirv.empty()) {
|
||||
effectiveSpirv = &outputIndexSpirv;
|
||||
}
|
||||
|
||||
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
||||
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||
|
||||
spvc_compiler_options options;
|
||||
spvcSession.CreateOptions(&options);
|
||||
|
||||
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION,
|
||||
ResolveBackendEsslVersion());
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
||||
|
||||
spvcSession.SetOptions(options);
|
||||
|
||||
// ES fixes a storage block's binding at link from its layout(binding=) qualifier
|
||||
// and has no glShaderStorageBlockBinding to move it afterwards, so a rebinding
|
||||
// can only be honoured by printing it INTO the qualifier. Rewriting the Binding
|
||||
// decoration before SPIRV-Cross emits is what does that; RemoveLayoutBinding
|
||||
// then deliberately preserves the qualifier for `buffer` declarations.
|
||||
if (!storageBlockBindingOverrides.empty()) { // empty for almost every program
|
||||
spvcSession.SetShaderStorageBlockBinding(storageBlockBindingOverrides);
|
||||
}
|
||||
|
||||
const char* result = nullptr;
|
||||
spvcSession.Compile(&result);
|
||||
|
||||
if (!result) {
|
||||
// MGLOG_E, unlatched, like the compile- and link-failure diagnostics below:
|
||||
// one line per failing stage is bounded by program count and naming the
|
||||
// stage is the entire diagnostic value. A stage that
|
||||
// never reaches the driver leaves the program short of that stage, so the
|
||||
// link fails with an EMPTY driver info log - the least debuggable failure
|
||||
// MobileGL can produce, and what hid the whole
|
||||
// KHR-GL43.vertex_attrib_binding family behind "the draw captured zeros".
|
||||
MGLOG_E("Shader transpilation to ESSL failed. State program ID: %u, stage: %s, "
|
||||
"SPIRV-Cross error: %s",
|
||||
stateProgramObject->GetExternalIndex(),
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
|
||||
spvcSession.GetLastErrorString());
|
||||
m_backendProgramUsable = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
source = result;
|
||||
// Per stage, never cumulative: a fragment shader consuming the same block
|
||||
// reports a name the vertex stage already reported, and its own rewrite must
|
||||
// still be taken or the two stages stop matching. Done here rather than inside
|
||||
// the transpile so a cache HIT contributes its names too.
|
||||
flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(),
|
||||
stageFlattenedXfbBlockNames.end());
|
||||
|
||||
// Position in the chain is arbitrary: this is the only header-level rewrite, it
|
||||
// edits #extension directives and never the body, and the replacement is the
|
||||
|
||||
@@ -1076,6 +1076,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
namespace PrgramImpl {
|
||||
// Defined further down, next to CollectImageFormatBakeInputs; only referenced here.
|
||||
struct ImageFormatBakeInputs;
|
||||
|
||||
class BackendProgramObjectImpl {
|
||||
public:
|
||||
// Per-link cache of a sampler-style uniform's backend location: built once in
|
||||
@@ -1209,6 +1212,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
private:
|
||||
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||
|
||||
// One stage's SPIR-V through the DirectGLES pass chain and SPIRV-Cross, producing
|
||||
// the raw emitted ESSL and the interface blocks this stage's XFB flattening
|
||||
// rewrote. This is the segment the L2 shader-translation memo keys on, so every
|
||||
// input it reads must appear in EsslTranslationKeyInputs - see the definition's
|
||||
// header comment in Managers.cpp and MG_Util/ShaderTranspiler/TranslationCache.h.
|
||||
// False means SPIRV-Cross refused the module; `outError` then carries its message.
|
||||
Bool TranspileSpirvToEssl(const Vector<unsigned int>& spirvCode, GLenum glShaderType,
|
||||
const std::set<String>& xfbCaptureBlockNames,
|
||||
const ImageFormatBakeInputs& imageFormatBake,
|
||||
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
|
||||
Bool enableSpirvValidation, String& outSource,
|
||||
std::set<String>& outFlattenedXfbBlockNames,
|
||||
String& outError) const;
|
||||
|
||||
Uint m_backendProgramId = 0;
|
||||
// GL name of the frontend program this was last synced from; diagnostics only, so
|
||||
// an unusable backend program can be traced back to the glCreateProgram id the app
|
||||
|
||||
Reference in New Issue
Block a user