mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
Compare commits
8
Commits
b6a7807a3a
...
3049c4b82b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3049c4b82b | ||
|
|
2b3850b76b | ||
|
|
2a0ae743a0 | ||
|
|
6839219c10 | ||
|
|
52ddb440ca | ||
|
|
79feeffd25 | ||
|
|
202037b5a3 | ||
|
|
bce9c48c8e |
@@ -192,6 +192,8 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
|
||||
@@ -4002,6 +4002,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLuint backendTexId = backendTextureIt->second->GetBackendTextureId();
|
||||
MGLOG_D("GetTexImage: backend texture id = %u", backendTexId);
|
||||
|
||||
// Force pending rendering to complete before reading the texture back through the temp READ FBO.
|
||||
// Tile-based GPUs (Mali) do not guarantee that a render into this texture through its own FBO has
|
||||
// been resolved to memory when it is subsequently sampled through a *different* (temp) FBO: the
|
||||
// cross-FBO glReadPixels below races the deferred tile resolve and returns the pre-render (clear)
|
||||
// contents, so distinct render targets read back byte-identical (e.g. KHR-GLxx.glsl_noperspective
|
||||
// fails on Mali-G715, all four programs reading as the clear colour). glGetTexImage is already a
|
||||
// CPU/GPU sync point, so the extra drain is negligible; Adreno resolves eagerly and is unaffected.
|
||||
g_GLESFuncs.glFinish();
|
||||
|
||||
MGLOG_D("GetTexImage: Binding temporary FBO");
|
||||
TempFBOBinder tempFBOBinder(true);
|
||||
|
||||
|
||||
@@ -1563,6 +1563,56 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return convertedData.data();
|
||||
}
|
||||
|
||||
// RGB565/RGB5_A1 shadow data is stored as 8-bit unorm; uploading it as GL_UNSIGNED_BYTE
|
||||
// leaves the 8-bit -> 5/6-bit requantization to the driver, whose rounding direction is
|
||||
// implementation-defined: Adreno rounds to nearest (lossless round trip) but Mali floors,
|
||||
// drifting mid-range texels one 5-bit step down and failing the KHR-GL3x
|
||||
// pixelstoragemodes.teximage3d rgb565/rgb5a1 1/32-eps checks. Repack the shadow rows into
|
||||
// the packed 16-bit client type with round-to-nearest instead - that recovers the original
|
||||
// 5/6-bit values exactly (the shadow expansion round(v * 255 / max) is injective), so the
|
||||
// driver stores them verbatim with no requantization left to its discretion. 4-bit formats
|
||||
// (RGBA4) are exempt: their 8-bit expansion (v * 17) is exact under either rounding.
|
||||
// Always retargets *inOutType for these formats (even for null data) so every upload of a
|
||||
// level uses the same client type.
|
||||
static const void* PreparePackedNormUpload(TextureInternalFormat format, const IntVec3& texelSize,
|
||||
const void* data, SizeT byteSize, GLenum* inOutType,
|
||||
Vector<Uint8>& packedData) {
|
||||
if (format != TextureInternalFormat::RGB5 && format != TextureInternalFormat::RGB5A1) {
|
||||
return data;
|
||||
}
|
||||
const Bool hasAlpha = format == TextureInternalFormat::RGB5A1;
|
||||
const GLenum packedType = hasAlpha ? GL_UNSIGNED_SHORT_5_5_5_1 : GL_UNSIGNED_SHORT_5_6_5;
|
||||
// Idempotent across a region's level loop: glType is shared, so later levels arrive with
|
||||
// the already-retargeted packed type and must still be converted.
|
||||
if (*inOutType != GL_UNSIGNED_BYTE && *inOutType != packedType) {
|
||||
return data;
|
||||
}
|
||||
*inOutType = packedType;
|
||||
if (data == nullptr || byteSize == 0) {
|
||||
return data;
|
||||
}
|
||||
const SizeT srcPixelBytes = hasAlpha ? 4 : 3;
|
||||
const SizeT texelCount = std::min(static_cast<SizeT>(std::max(texelSize.x(), 0)) *
|
||||
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
|
||||
static_cast<SizeT>(std::max(texelSize.z(), 1)),
|
||||
byteSize / srcPixelBytes);
|
||||
packedData.resize(texelCount * sizeof(Uint16));
|
||||
const Uint8* src = static_cast<const Uint8*>(data);
|
||||
auto* dst = reinterpret_cast<Uint16*>(packedData.data());
|
||||
for (SizeT i = 0; i < texelCount; ++i, src += srcPixelBytes) {
|
||||
const Uint32 r = (static_cast<Uint32>(src[0]) * 31u + 127u) / 255u;
|
||||
const Uint32 b = (static_cast<Uint32>(src[2]) * 31u + 127u) / 255u;
|
||||
if (hasAlpha) {
|
||||
const Uint32 g = (static_cast<Uint32>(src[1]) * 31u + 127u) / 255u;
|
||||
dst[i] = static_cast<Uint16>((r << 11) | (g << 6) | (b << 1) | (src[3] >= 128 ? 1u : 0u));
|
||||
} else {
|
||||
const Uint32 g = (static_cast<Uint32>(src[1]) * 63u + 127u) / 255u;
|
||||
dst[i] = static_cast<Uint16>((r << 11) | (g << 5) | b);
|
||||
}
|
||||
}
|
||||
return packedData.data();
|
||||
}
|
||||
|
||||
void BackendTextureObject::SyncMipmapsToBackend(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
||||
if (!stateTextureObject) {
|
||||
@@ -1706,6 +1756,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const void* uploadData = PrepareNormFloatFallbackUpload(
|
||||
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
|
||||
convertedUploadData);
|
||||
Vector<Uint8> packedUploadData;
|
||||
uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
|
||||
uploadData, levelByteSize, &glType, packedUploadData);
|
||||
|
||||
DebugImpl::ErrorLopper::Clear();
|
||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
@@ -1831,6 +1884,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const void* uploadData = PrepareNormFloatFallbackUpload(
|
||||
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
|
||||
convertedUploadData);
|
||||
Vector<Uint8> packedUploadData;
|
||||
uploadData =
|
||||
PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
|
||||
uploadData, levelByteSize, &glType, packedUploadData);
|
||||
|
||||
DebugImpl::ErrorLopper::Clear();
|
||||
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
@@ -1885,6 +1942,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const void* uploadData = PrepareNormFloatFallbackUpload(
|
||||
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
|
||||
convertedUploadData);
|
||||
Vector<Uint8> packedUploadData;
|
||||
uploadData =
|
||||
PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
|
||||
uploadData, levelByteSize, &glType, packedUploadData);
|
||||
MGLOG_D("%s: target: %s: syncing mip %d: %dx%dx%d, byteSize = %d, pData = %p, "
|
||||
"levelDirty = %s",
|
||||
__func__, MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||
@@ -1990,6 +2051,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const void* uploadData = PrepareNormFloatFallbackUpload(
|
||||
textureMipmapObject->GetFormat(), texelSize, mipData, byteSize, glType,
|
||||
convertedUploadData);
|
||||
Vector<Uint8> packedUploadData;
|
||||
uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), texelSize,
|
||||
uploadData, byteSize, &glType, packedUploadData);
|
||||
const IntVec3 uploadSize =
|
||||
GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize);
|
||||
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
|
||||
@@ -2790,6 +2854,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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) &&
|
||||
!noperspectiveSpirv.empty()) {
|
||||
effectiveSpirv = &noperspectiveSpirv;
|
||||
}
|
||||
|
||||
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
|
||||
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||
|
||||
|
||||
@@ -131,25 +131,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Order-independent accumulation blending: the write order of overlapping fragments
|
||||
// does not change the result, which is what lets multi-pass chains re-rasterize the
|
||||
// same geometry and combine per-pass contributions (MC 26.3 OIT: GL_MAX depth
|
||||
// bounds, additive ONE+ONE transmittance/accumulate). Sorted-transparency "over"
|
||||
// compositing (SRC_ALPHA-style factors) is order-dependent, drawn once per surface,
|
||||
// and relies on its depth writes for occlusion - it must not be treated as hazardous.
|
||||
// MIN/MAX ignore blend factors entirely per the Vulkan spec.
|
||||
// MIN/MAX extremum blending: the signature of a depth-bounds accumulation pass
|
||||
// (MC 26.3 OIT writes vec4(-linD, linD, deviceZ, 0) under GL_MAX while writing
|
||||
// depth for its equality chain). MIN/MAX ignore blend factors per the Vulkan spec.
|
||||
//
|
||||
// Deliberately color-channel only. A separate-alpha accumulation
|
||||
// (glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX)) whose color channel is an ordinary
|
||||
// over-blend is not treated as hazardous: no known content pairs that shape with a
|
||||
// depth-equality chain, and widening the test would re-capture sorted transparency.
|
||||
// Deliberately the ONLY shape stripped. A quirk should touch as little unrelated
|
||||
// content as possible, and a trace sweep of every fixture showed the wider
|
||||
// alternatives all cost more than they fix:
|
||||
// - additive ONE+ONE with a depth write matched zero draws of the 26.3 chain
|
||||
// (its transmittance/accumulate passes disable depth writes themselves) - the
|
||||
// only real content it caught was harmless additive glow effects (Create);
|
||||
// - sorted-transparency "over" blends (SRC_ALPHA-style) are order-dependent,
|
||||
// drawn once per surface, and rely on their depth writes for occlusion;
|
||||
// - separate-alpha accumulation over an over-blending color channel has no
|
||||
// known pairing with a depth-equality chain (color channel only, see tests).
|
||||
// If a future workload pairs another blend shape with an equality chain, widen
|
||||
// this with that evidence in hand rather than pre-emptively.
|
||||
Bool IsAccumulationBlend(const VkPipelineColorBlendAttachmentState& attachment) {
|
||||
if (attachment.colorBlendOp == VK_BLEND_OP_MIN || attachment.colorBlendOp == VK_BLEND_OP_MAX) {
|
||||
return true;
|
||||
}
|
||||
return attachment.colorBlendOp == VK_BLEND_OP_ADD &&
|
||||
attachment.srcColorBlendFactor == VK_BLEND_FACTOR_ONE &&
|
||||
attachment.dstColorBlendFactor == VK_BLEND_FACTOR_ONE;
|
||||
return attachment.colorBlendOp == VK_BLEND_OP_MIN ||
|
||||
attachment.colorBlendOp == VK_BLEND_OP_MAX;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -70,11 +70,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// with an equality-inclusive compare on the re-rasterized geometry) requires
|
||||
// cross-pipeline position invariance that some mobile compilers do not provide, even
|
||||
// with the SPIR-V Invariant decoration; whole primitives then drop out of the later
|
||||
// passes. Only order-independent accumulation blends (MIN/MAX, additive ONE+ONE) are
|
||||
// stripped - that is the signature of such equality chains (MC 26.3 OIT) - while
|
||||
// sorted-transparency "over" compositing (e.g. vanilla MC water, SRC_ALPHA factors),
|
||||
// which draws each surface once and depends on its depth writes to occlude later
|
||||
// passes, keeps them. Set at renderer initialization based on the active driver.
|
||||
// passes. Only MIN/MAX extremum blends are stripped - the signature of such a
|
||||
// chain's depth-bounds pass (MC 26.3 OIT), and per a fixture-wide trace sweep the
|
||||
// only depth-writing shape the chain actually uses - so every other blend
|
||||
// (sorted-transparency "over" like vanilla MC water, additive glows, ...) keeps
|
||||
// its depth writes. Set at renderer initialization based on the active driver.
|
||||
static void SetSuppressBlendedDepthWrite(Bool enabled);
|
||||
static Bool IsSuppressBlendedDepthWriteEnabled() { return s_suppressBlendedDepthWrite; }
|
||||
// Device gate for the quirk: ForceOn/ForceOff bypass detection, Auto enables it on
|
||||
|
||||
@@ -1547,6 +1547,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"ProgramFactory::ReflectFragmentOutputs: failed to create reflection module (result=%d)",
|
||||
static_cast<Int>(createResult));
|
||||
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
|
||||
// Fail toward the exemption: stripping a genuine gl_FragDepth writer would
|
||||
// corrupt its depth output outright, while wrongly exempting an accumulation
|
||||
// pass merely reverts that one program to the pre-quirk behavior.
|
||||
entry.fragmentReplacesDepth = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -3552,6 +3552,17 @@ void main() {
|
||||
(bufferMask.a() ? VK_COLOR_COMPONENT_A_BIT : 0u));
|
||||
Bool effectiveBlendEnabled = blendEnabled;
|
||||
MG_State::GLState::ITextureObject* colorAttachmentTexture = nullptr;
|
||||
if (isDefaultDrawFbo && i < drawBuffers.size() &&
|
||||
drawBuffers[i] == FramebufferAttachmentType::None) {
|
||||
// The default framebuffer spans the same MAX_DRAW_BUFFERS slots as an FBO
|
||||
// (slot 0 is the back buffer, or None after glDrawBuffer(GL_NONE); slots
|
||||
// 1+ are always None). Discard writes and blend state for the None slots
|
||||
// like the FBO path below does, so stale indexed blend state on phantom
|
||||
// slots cannot leak into the pipeline - most notably into the blended
|
||||
// depth-write quirk's accumulation scan.
|
||||
attachmentColorWriteMask = 0;
|
||||
effectiveBlendEnabled = false;
|
||||
}
|
||||
if (!isDefaultDrawFbo && i < drawBuffers.size()) {
|
||||
const auto drawBuffer = drawBuffers[i];
|
||||
colorAttachmentTexture = resolveCompleteColorAttachmentTexture(i);
|
||||
|
||||
@@ -424,7 +424,7 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawRangeElementsBaseVertex, GLenum mode, GLuint
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertex, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertex, mode, count, type, indices, instancecount, basevertex)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture, GLenum target, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture, target, attachment, texture, level)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBox, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrimitiveBoundingBox, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_STUB_END(GLenum, GetGraphicsResetStatus)
|
||||
DECLARE_GL_FUNCTION_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_END(GLenum, GetGraphicsResetStatus)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
|
||||
|
||||
@@ -1996,4 +1996,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
return MG_Util::ConvertErrorCodeToGLEnum(error->get()->code);
|
||||
}
|
||||
|
||||
GLenum GetGraphicsResetStatus() {
|
||||
// MobileGL does not implement robustness reset notification, so report GL_NO_ERROR
|
||||
// ("no reset detected"). Returning the generic stub's (GLenum)1 makes dEQP read a lost
|
||||
// device after every case (gl3cTestPackages.cpp:121) and, under the default
|
||||
// --deqp-terminate-on-device-lost=enable, tear the whole CTS run down.
|
||||
return GL_NO_ERROR;
|
||||
}
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -21,4 +21,5 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||
GLenum GetError();
|
||||
GLenum GetGraphicsResetStatus();
|
||||
} // namespace MobileGL::MG_Impl::GLImpl
|
||||
|
||||
@@ -258,11 +258,14 @@ TEST(PipelineQuirkStripDecision, MinBlendIsStripped) {
|
||||
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||
}
|
||||
|
||||
TEST(PipelineQuirkStripDecision, AdditiveOnePlusOneIsStripped) {
|
||||
// MC 26.3 OIT transmittance/accumulate: ONE+ONE additive accumulation writing depth.
|
||||
TEST(PipelineQuirkStripDecision, AdditiveOnePlusOneIsNotStripped) {
|
||||
// ONE+ONE additive with a depth write matched zero draws of the 26.3 chain in the
|
||||
// fixture sweep (transmittance/accumulate disable depth writes themselves); the only
|
||||
// real content with this shape was harmless additive glow effects (Create). A quirk
|
||||
// touches as little unrelated content as possible, so the shape stays exempt.
|
||||
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask));
|
||||
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||
}
|
||||
|
||||
TEST(PipelineQuirkStripDecision, SortedTransparencyOverBlendIsNotStripped) {
|
||||
@@ -285,9 +288,10 @@ TEST(PipelineQuirkStripDecision, EffectivelyOpaqueBlendIsNotStripped) {
|
||||
|
||||
TEST(PipelineQuirkStripDecision, FullyMaskedAccumulationBlendIsNotStripped) {
|
||||
// Depth-prepass pattern: colorMask(0,0,0,0) with blending left enabled - blending is
|
||||
// moot, and stripping would delete the entire prepass.
|
||||
// moot, and stripping would delete the entire prepass. MAX so the exemption, not the
|
||||
// blend-op filter, is what keeps the depth write.
|
||||
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, 0));
|
||||
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, 0));
|
||||
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||
}
|
||||
|
||||
@@ -314,8 +318,8 @@ TEST(PipelineQuirkStripDecision, FragDepthWriterIsExempt) {
|
||||
}
|
||||
|
||||
TEST(PipelineQuirkStripDecision, AccumulationOnSecondaryAttachmentIsStripped) {
|
||||
// The hazard is not limited to attachment 0: the 26.3 transmittance pass accumulates
|
||||
// into a 2-target MRT.
|
||||
// The scan is not limited to attachment 0: an extremum accumulation on any live
|
||||
// attachment marks the pipeline.
|
||||
PipelineFactory::PipelineCreatePayload payload{};
|
||||
payload.colorAttachmentCount = 2;
|
||||
payload.depthTestEnable = true;
|
||||
@@ -323,21 +327,20 @@ TEST(PipelineQuirkStripDecision, AccumulationOnSecondaryAttachmentIsStripped) {
|
||||
payload.colorBlendAttachments[0] = MakeBlendAttachment(
|
||||
false, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, kFullColorWriteMask);
|
||||
payload.colorBlendAttachments[1] = MakeBlendAttachment(
|
||||
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask);
|
||||
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask);
|
||||
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||
}
|
||||
|
||||
TEST(PipelineQuirkStripDecision, AlphaWeightedAdditiveIsNotStripped) {
|
||||
// SRC_ALPHA,ONE additive is order-independent in the color channel but is the classic
|
||||
// *sorted* particle/glow blend, not an OIT accumulation pass. Pins the src==ONE clause:
|
||||
// without it this state would be stripped.
|
||||
// SRC_ALPHA,ONE additive: the classic *sorted* particle/glow blend. Kept exempt like
|
||||
// every other ADD-op shape now that the strip is extremum-only.
|
||||
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||
true, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask));
|
||||
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||
}
|
||||
|
||||
TEST(PipelineQuirkStripDecision, ReverseSubtractIsNotStripped) {
|
||||
// Deliberate narrowing: only MIN/MAX and ONE+ONE ADD carry the equality-chain
|
||||
// Deliberate narrowing: only the MIN/MAX extremum ops carry the depth-bounds
|
||||
// signature. SUBTRACT-class ops stay outside the quirk until content demands them.
|
||||
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_REVERSE_SUBTRACT,
|
||||
@@ -348,7 +351,7 @@ TEST(PipelineQuirkStripDecision, ReverseSubtractIsNotStripped) {
|
||||
TEST(PipelineQuirkStripDecision, PartiallyMaskedAccumulationIsStripped) {
|
||||
// Only a fully masked attachment is exempt; a live alpha channel still accumulates.
|
||||
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
|
||||
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, VK_COLOR_COMPONENT_A_BIT));
|
||||
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, VK_COLOR_COMPONENT_A_BIT));
|
||||
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
|
||||
}
|
||||
|
||||
|
||||
@@ -238,6 +238,77 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
// KHR-GL33.shaders.preprocessor.* — a block comment is one preprocessing token that the C/GLSL
|
||||
// preprocessor replaces with a single space, even when it spans newlines inside a directive. glslang
|
||||
// handles this natively, so MobileGL must not mangle it. These reproduce the CTS cases that failed
|
||||
// because comment blanking preserved the interior newline, truncating multi-line #define bodies.
|
||||
static void ExpectCompiles(MobileGL::ShaderStage stage, GLenum glStage, MobileGL::String source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
PreprocessShaderSource(stage, source);
|
||||
ShaderAttrib attrib{.shaderType = glStage, .sourceStr = source};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) {
|
||||
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, PreprocessMultilineCommentInDefineBodyCompiles) {
|
||||
ExpectCompiles(ShaderStage::Fragment, GL_FRAGMENT_SHADER,
|
||||
R"(#version 330
|
||||
precision mediump float;
|
||||
out float out0;
|
||||
#define VALUE /* current
|
||||
value */ 4.2
|
||||
|
||||
void main()
|
||||
{
|
||||
out0 = VALUE;
|
||||
})");
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, PreprocessRedefineObjectMultilineCommentCompiles) {
|
||||
ExpectCompiles(ShaderStage::Fragment, GL_FRAGMENT_SHADER,
|
||||
R"(#version 330
|
||||
precision mediump float;
|
||||
out float out0;
|
||||
# define VAL1 1.0
|
||||
#define VAL2 2.0
|
||||
|
||||
#define RES2 /* fdsjklfdsjkl
|
||||
dsfjkhfdsjkh
|
||||
fdsjklhfdsjkh */ (RES1 * VAL2)
|
||||
#define RES1 (VAL2 / VAL1)
|
||||
#define RES2 /* ewrlkjhsadf */ (RES1 * VAL2)
|
||||
#define VALUE (RES2 + RES1)
|
||||
|
||||
void main()
|
||||
{
|
||||
out0 = VALUE;
|
||||
})");
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, PreprocessFunctionMacroRedefinitionMultilineCommentCompiles) {
|
||||
ExpectCompiles(ShaderStage::Fragment, GL_FRAGMENT_SHADER,
|
||||
R"(#version 330
|
||||
precision mediump float;
|
||||
out float out0;
|
||||
# define FUNC(a,b) (a +b)
|
||||
# define FUNC(a,b)(a /* comment
|
||||
*/ +b)
|
||||
|
||||
void main()
|
||||
{
|
||||
out0 = FUNC(1.0, 2.0);
|
||||
})");
|
||||
}
|
||||
|
||||
// Note: KHR-GL3x.shaders.preprocessor.conditional_inclusion.basic_2 (`#define AAA defined(BBB)` used
|
||||
// in `#if !AAA`) is intentionally NOT handled here. Generating the `defined` operator via macro
|
||||
// expansion is undefined per the C/GLSL preprocessor spec, and glslang deliberately rejects it
|
||||
// ("'defined' : cannot use in preprocessor expression when expanded from macros"). Making it pass
|
||||
// would require MobileGL to run its own macro expansion ahead of glslang, which is exactly the
|
||||
// preprocessing we defer to glslang; the two cases stay failing by design.
|
||||
|
||||
TEST_F(ProgramUtilTest, PreprocessLegacyFragmentShaderModernizesGlmarkStyleSource) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
@@ -1089,6 +1160,353 @@ TEST_F(ProgramUtilTest, CompileFragmentShaderWithDiscard) {
|
||||
}
|
||||
}
|
||||
|
||||
// noperspective is core desktop GLSL (1.30+) and maps to the SPIR-V NoPerspective decoration. It must
|
||||
// reach glslang (not be stripped as text) so the SPIR-V carries the decoration; SPIRV-Cross then emits
|
||||
// ESSL `noperspective` + the GL_NV_shader_noperspective_interpolation extension. Shader packs
|
||||
// (Iris/Complementary) depend on it, and KHR-GL33.glsl_noperspective fails if the result matches
|
||||
// smooth. This is the DirectGLES path with the NV extension available (SPIRV-Cross's default).
|
||||
TEST_F(ProgramUtilTest, NoperspectiveInterpolationSurvivesToEssl) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String fs = R"(#version 330 core
|
||||
noperspective in vec4 vColor;
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vColor; }
|
||||
)";
|
||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fs};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) FAIL() << "compile errc: " << res.error().errc << "\nlog: " << res.error().log;
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {res.value()}};
|
||||
auto program_res = ShaderCompiler::LinkProgram(programAttrib);
|
||||
if (!program_res) FAIL() << "link errc: " << program_res.error().errc << "\nlog: " << program_res.error().log;
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *program_res.value()};
|
||||
auto bin_res = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
if (!bin_res) FAIL() << "spirv errc: " << bin_res.error().errc << "\nlog: " << bin_res.error().log;
|
||||
ASSERT_EQ(bin_res.value().size(), 1u);
|
||||
|
||||
SpvcSession session(bin_res.value()[0], SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
if (!essl) FAIL() << "decompile errc: " << essl.error().errc << "\nlog: " << essl.error().log;
|
||||
|
||||
EXPECT_NE(essl.value().find("noperspective"), String::npos)
|
||||
<< "noperspective was lost before it reached SPIR-V:\n" << essl.value();
|
||||
EXPECT_NE(essl.value().find("GL_NV_shader_noperspective_interpolation"), String::npos)
|
||||
<< "SPIRV-Cross must require the NV extension for ES noperspective:\n" << essl.value();
|
||||
}
|
||||
|
||||
// The old handling was a naked substring erase of "noperspective", so any identifier that merely
|
||||
// contained those characters (a uniform named noperspectiveBlend, say) got mangled. Removing the
|
||||
// strip fixes it - glslang, which is identifier-aware, is the only thing that should see the keyword.
|
||||
TEST_F(ProgramUtilTest, PreprocessDoesNotCorruptIdentifiersContainingNoperspective) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String source = R"(#version 330 core
|
||||
uniform float noperspectiveBlend;
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(noperspectiveBlend); }
|
||||
)";
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
EXPECT_NE(source.find("noperspectiveBlend"), String::npos)
|
||||
<< "identifier was corrupted by substring stripping:\n" << source;
|
||||
}
|
||||
|
||||
// The DirectGLES fallback for devices without GL_NV_shader_noperspective_interpolation: stripping the
|
||||
// NoPerspective decoration makes SPIRV-Cross emit a plain smooth varying with no `#extension … :
|
||||
// require`, so the shader still compiles (rendering as smooth) instead of being rejected by the driver.
|
||||
TEST_F(ProgramUtilTest, StripNoPerspectiveFallbackProducesPlainEssl) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String fs = R"(#version 330 core
|
||||
noperspective in vec4 vColor;
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vColor; }
|
||||
)";
|
||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fs};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) FAIL() << "compile errc: " << res.error().errc << "\nlog: " << res.error().log;
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {res.value()}};
|
||||
auto program_res = ShaderCompiler::LinkProgram(programAttrib);
|
||||
if (!program_res) FAIL() << "link errc: " << program_res.error().errc << "\nlog: " << program_res.error().log;
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *program_res.value()};
|
||||
auto bin_res = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
if (!bin_res) FAIL() << "spirv errc: " << bin_res.error().errc << "\nlog: " << bin_res.error().log;
|
||||
ASSERT_EQ(bin_res.value().size(), 1u);
|
||||
|
||||
// Precondition: with the decoration present the default decompile requires the NV extension.
|
||||
{
|
||||
SpvcSession session(bin_res.value()[0], SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
if (!essl) FAIL() << "decompile errc: " << essl.error().errc;
|
||||
ASSERT_NE(essl.value().find("noperspective"), String::npos) << essl.value();
|
||||
}
|
||||
|
||||
// The fallback strips the decoration -> plain smooth ESSL, no extension require.
|
||||
Vector<Uint32> stripped;
|
||||
ASSERT_TRUE(ShaderCompiler::StripNoPerspectiveForEssl(bin_res.value()[0], stripped));
|
||||
ASSERT_FALSE(stripped.empty());
|
||||
|
||||
SpvcSession session(stripped, SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
if (!essl) FAIL() << "decompile errc: " << essl.error().errc << "\nlog: " << essl.error().log;
|
||||
EXPECT_EQ(essl.value().find("noperspective"), String::npos)
|
||||
<< "the decoration should be gone:\n" << essl.value();
|
||||
EXPECT_EQ(essl.value().find("GL_NV_shader_noperspective_interpolation"), String::npos)
|
||||
<< "no extension require without the decoration:\n" << essl.value();
|
||||
}
|
||||
|
||||
// Directly exercises BOTH decoration forms StripNoPerspectivePass handles: a plain-variable
|
||||
// OpDecorate NoPerspective (in-operand 1) and an interface-block-member OpMemberDecorate NoPerspective
|
||||
// (in-operand 2). The ESSL round-trip tests above use only a scalar input, so they never reach the
|
||||
// member-decorate branch, which a block varying like `in Block { noperspective vec4 c; }` (common in
|
||||
// shader packs) produces. Unrelated decorations (Flat, Location) must survive untouched.
|
||||
TEST_F(ProgramUtilTest, StripNoPerspectivePassRemovesBothDecorateForms) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
const String spirvText = R"(
|
||||
OpCapability Shader
|
||||
OpMemoryModel Logical GLSL450
|
||||
OpEntryPoint Fragment %main "main" %plainVar %blockVar %flatVar
|
||||
OpExecutionMode %main OriginUpperLeft
|
||||
OpName %main "main"
|
||||
OpDecorate %plainVar Location 0
|
||||
OpDecorate %plainVar NoPerspective
|
||||
OpMemberDecorate %Block 0 NoPerspective
|
||||
OpDecorate %blockVar Location 1
|
||||
OpDecorate %flatVar Location 2
|
||||
OpDecorate %flatVar Flat
|
||||
%void = OpTypeVoid
|
||||
%mainFn = OpTypeFunction %void
|
||||
%float = OpTypeFloat 32
|
||||
%v4float = OpTypeVector %float 4
|
||||
%int = OpTypeInt 32 1
|
||||
%inV4Ptr = OpTypePointer Input %v4float
|
||||
%plainVar = OpVariable %inV4Ptr Input
|
||||
%Block = OpTypeStruct %v4float
|
||||
%inBlockPtr = OpTypePointer Input %Block
|
||||
%blockVar = OpVariable %inBlockPtr Input
|
||||
%inIntPtr = OpTypePointer Input %int
|
||||
%flatVar = OpVariable %inIntPtr Input
|
||||
%main = OpFunction %void None %mainFn
|
||||
%mainBody = OpLabel
|
||||
OpReturn
|
||||
OpFunctionEnd
|
||||
)";
|
||||
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
Vector<uint32_t> inputBinary;
|
||||
ASSERT_TRUE(tools.Assemble(spirvText, &inputBinary));
|
||||
|
||||
const auto countNoPerspective = [](const String& text) {
|
||||
SizeT count = 0, offset = 0;
|
||||
while ((offset = text.find("NoPerspective", offset)) != String::npos) {
|
||||
++count;
|
||||
offset += std::strlen("NoPerspective");
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
||||
String inputText;
|
||||
ASSERT_TRUE(tools.Disassemble(inputBinary, &inputText));
|
||||
ASSERT_EQ(countNoPerspective(inputText), 2u)
|
||||
<< "fixture must carry both a plain and a member NoPerspective:\n" << inputText;
|
||||
|
||||
Vector<uint32_t> outputBinary;
|
||||
ASSERT_TRUE(ShaderCompiler::StripNoPerspectiveForEssl(inputBinary, outputBinary));
|
||||
ASSERT_FALSE(outputBinary.empty());
|
||||
|
||||
String outputText;
|
||||
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
|
||||
EXPECT_EQ(countNoPerspective(outputText), 0u)
|
||||
<< "both NoPerspective decorations (OpDecorate and OpMemberDecorate) must be stripped:\n" << outputText;
|
||||
EXPECT_NE(outputText.find("Flat"), String::npos)
|
||||
<< "the unrelated Flat decoration must survive:\n" << outputText;
|
||||
EXPECT_NE(outputText.find("Location"), String::npos)
|
||||
<< "Location decorations must survive:\n" << outputText;
|
||||
}
|
||||
|
||||
// Phase 2 emulation - fragment side. On a device without the NV extension the NoPerspective input is
|
||||
// recovered as `load * gl_FragCoord.w` and the decoration removed; gl_FragCoord is synthesized because
|
||||
// the shader did not otherwise use it. The emulated SPIR-V must validate and decompile without the
|
||||
// extension require.
|
||||
TEST_F(ProgramUtilTest, EmulateNoperspectiveFragmentRecoversWithFragCoordW) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String fs = R"(#version 330 core
|
||||
noperspective in vec4 vColor;
|
||||
out vec4 f;
|
||||
void main() { f = vColor; }
|
||||
)";
|
||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fs};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) FAIL() << "compile: " << res.error().log;
|
||||
ProgramAttrib pa{.shaders = {res.value()}};
|
||||
auto pr = ShaderCompiler::LinkProgram(pa);
|
||||
if (!pr) FAIL() << "link: " << pr.error().log;
|
||||
ProgramBinaryAttrib ba{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *pr.value()};
|
||||
auto br = ShaderCompiler::GetSpirvBinaryFromProgram(ba);
|
||||
if (!br) FAIL() << "spirv: " << br.error().log;
|
||||
ASSERT_EQ(br.value().size(), 1u);
|
||||
|
||||
Vector<uint32_t> emulated;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(br.value()[0], emulated));
|
||||
ASSERT_FALSE(emulated.empty());
|
||||
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String dis;
|
||||
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||
ASSERT_TRUE(tools.Validate(emulated)) << "emulated SPIR-V must be valid:\n" << dis;
|
||||
EXPECT_EQ(dis.find("NoPerspective"), String::npos) << "decoration must be stripped:\n" << dis;
|
||||
EXPECT_NE(dis.find("FragCoord"), String::npos) << "gl_FragCoord must be synthesized:\n" << dis;
|
||||
EXPECT_NE(dis.find("OpVectorTimesScalar"), String::npos) << "the recovery multiply must be present:\n" << dis;
|
||||
|
||||
SpvcSession session(emulated, SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
if (!essl) FAIL() << "decompile: " << essl.error().log;
|
||||
EXPECT_EQ(essl.value().find("noperspective"), String::npos) << essl.value();
|
||||
EXPECT_EQ(essl.value().find("GL_NV_shader_noperspective_interpolation"), String::npos) << essl.value();
|
||||
EXPECT_NE(essl.value().find("gl_FragCoord"), String::npos) << "recovery must reference gl_FragCoord:\n" << essl.value();
|
||||
}
|
||||
|
||||
// Phase 2 emulation - vertex side. The NoPerspective output is pre-multiplied by gl_Position.w before
|
||||
// return and the decoration removed. Emulated SPIR-V must validate and decompile without the extension.
|
||||
TEST_F(ProgramUtilTest, EmulateNoperspectiveVertexPreMultipliesByPositionW) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String vs = R"(#version 330 core
|
||||
in vec4 pos;
|
||||
noperspective out vec4 vColor;
|
||||
void main() { gl_Position = pos; vColor = pos; }
|
||||
)";
|
||||
ShaderAttrib attrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = vs};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) FAIL() << "compile: " << res.error().log;
|
||||
ProgramAttrib pa{.shaders = {res.value()}};
|
||||
auto pr = ShaderCompiler::LinkProgram(pa);
|
||||
if (!pr) FAIL() << "link: " << pr.error().log;
|
||||
ProgramBinaryAttrib ba{.shaderTypes = {GL_VERTEX_SHADER}, .program = *pr.value()};
|
||||
auto br = ShaderCompiler::GetSpirvBinaryFromProgram(ba);
|
||||
if (!br) FAIL() << "spirv: " << br.error().log;
|
||||
ASSERT_EQ(br.value().size(), 1u);
|
||||
|
||||
Vector<uint32_t> emulated;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(br.value()[0], emulated));
|
||||
ASSERT_FALSE(emulated.empty());
|
||||
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String dis;
|
||||
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||
ASSERT_TRUE(tools.Validate(emulated)) << "emulated SPIR-V must be valid:\n" << dis;
|
||||
EXPECT_EQ(dis.find("NoPerspective"), String::npos) << "decoration must be stripped:\n" << dis;
|
||||
EXPECT_NE(dis.find("OpVectorTimesScalar"), String::npos) << "the pre-multiply must be present:\n" << dis;
|
||||
|
||||
SpvcSession session(emulated, SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
if (!essl) FAIL() << "decompile: " << essl.error().log;
|
||||
EXPECT_EQ(essl.value().find("noperspective"), String::npos) << essl.value();
|
||||
EXPECT_NE(essl.value().find("gl_Position"), String::npos) << "pre-multiply must reference gl_Position:\n" << essl.value();
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Compiles one shader stage through the full pipeline and returns its SPIR-V, or fails the test.
|
||||
MobileGL::Vector<uint32_t> CompileStageSpirv(GLenum type, const char* src) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib attrib{.shaderType = type, .sourceStr = src};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
EXPECT_TRUE(static_cast<bool>(res)) << (res ? "" : res.error().log);
|
||||
if (!res) return {};
|
||||
ProgramAttrib pa{.shaders = {res.value()}};
|
||||
auto pr = ShaderCompiler::LinkProgram(pa);
|
||||
EXPECT_TRUE(static_cast<bool>(pr)) << (pr ? "" : pr.error().log);
|
||||
if (!pr) return {};
|
||||
ProgramBinaryAttrib ba{.shaderTypes = {type}, .program = *pr.value()};
|
||||
auto br = ShaderCompiler::GetSpirvBinaryFromProgram(ba);
|
||||
EXPECT_TRUE(static_cast<bool>(br)) << (br ? "" : br.error().log);
|
||||
if (!br || br.value().empty()) return {};
|
||||
return br.value()[0];
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Regression: the vertex pre-multiply must be applied exactly once (in main), not once per function.
|
||||
// glslang does not inline, so a helper function survives as its own OpFunction; instrumenting its
|
||||
// return too would scale the varying by gl_Position.w twice (w^2).
|
||||
TEST_F(ProgramUtilTest, EmulateNoperspectiveVertexWithHelperScalesExactlyOnce) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
// helper() returns via OpReturnValue and adds (no vector*scalar), so the ONLY OpVectorTimesScalar
|
||||
// in the module is the emulation's pre-multiply. The old all-functions code injected it at both
|
||||
// helper's and main's return -> count 2; restricted to the entry function it is 1.
|
||||
auto spirv = CompileStageSpirv(GL_VERTEX_SHADER, R"(#version 330 core
|
||||
in vec4 pos;
|
||||
noperspective out vec4 vColor;
|
||||
vec4 helper(vec4 x) { return x + vec4(1.0); }
|
||||
void main() { gl_Position = pos; vColor = helper(pos); }
|
||||
)");
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<uint32_t> emulated;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(spirv, emulated));
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String dis;
|
||||
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||
ASSERT_TRUE(tools.Validate(emulated)) << dis;
|
||||
|
||||
SizeT count = 0, off = 0;
|
||||
while ((off = dis.find("OpVectorTimesScalar", off)) != String::npos) {
|
||||
++count;
|
||||
off += std::strlen("OpVectorTimesScalar");
|
||||
}
|
||||
EXPECT_EQ(count, 1u) << "the gl_Position.w pre-multiply must happen exactly once, not per function:\n" << dis;
|
||||
}
|
||||
|
||||
// Regression: a single-component read (vColor.x), which glslang lowers via OpAccessChain, must still be
|
||||
// recovered with gl_FragCoord.w - not silently left un-scaled.
|
||||
TEST_F(ProgramUtilTest, EmulateNoperspectiveFragmentComponentReadIsRecovered) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
auto spirv = CompileStageSpirv(GL_FRAGMENT_SHADER, R"(#version 330 core
|
||||
noperspective in vec4 vColor;
|
||||
out vec4 f;
|
||||
void main() { f = vec4(vColor.x); }
|
||||
)");
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<uint32_t> emulated;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(spirv, emulated));
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String dis;
|
||||
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||
ASSERT_TRUE(tools.Validate(emulated)) << dis;
|
||||
EXPECT_EQ(dis.find("NoPerspective"), String::npos) << dis;
|
||||
EXPECT_NE(dis.find("FragCoord"), String::npos)
|
||||
<< "the component read must still be recovered via gl_FragCoord.w:\n" << dis;
|
||||
}
|
||||
|
||||
// Coverage: a scalar float varying exercises the OpFMul path; a vector varying the OpVectorTimesScalar
|
||||
// path; multiple noperspective varyings in one stage are all handled.
|
||||
TEST_F(ProgramUtilTest, EmulateNoperspectiveHandlesScalarAndMultipleVaryings) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
auto spirv = CompileStageSpirv(GL_FRAGMENT_SHADER, R"(#version 330 core
|
||||
noperspective in float a;
|
||||
noperspective in vec2 b;
|
||||
out vec4 f;
|
||||
void main() { f = vec4(a, b, 1.0); }
|
||||
)");
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<uint32_t> emulated;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(spirv, emulated));
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String dis;
|
||||
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||
ASSERT_TRUE(tools.Validate(emulated)) << dis;
|
||||
EXPECT_EQ(dis.find("NoPerspective"), String::npos) << dis;
|
||||
EXPECT_NE(dis.find("OpFMul"), String::npos) << "the scalar varying must scale with OpFMul:\n" << dis;
|
||||
EXPECT_NE(dis.find("OpVectorTimesScalar"), String::npos)
|
||||
<< "the vector varying must scale with OpVectorTimesScalar:\n" << dis;
|
||||
}
|
||||
|
||||
const char* vs_location = R"(#version 460
|
||||
|
||||
in vec4 Position;
|
||||
|
||||
@@ -811,6 +811,9 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
if (std::strcmp(extension, "GL_EXT_blend_func_extended") == 0) {
|
||||
caps.SupportsDualSourceBlend = true;
|
||||
}
|
||||
if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) {
|
||||
caps.SupportsNoperspectiveInterpolation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1050,6 +1050,11 @@ namespace MobileGL {
|
||||
// factors and layout(index = 1) fragment outputs. GLES core has no dual-source blending,
|
||||
// so without this a draw using a SRC1 factor cannot proceed.
|
||||
Bool SupportsDualSourceBlend = false;
|
||||
// GL_NV_shader_noperspective_interpolation is present: the driver accepts the
|
||||
// `noperspective` interpolation qualifier in ESSL. GLES core has none, so without this
|
||||
// SPIRV-Cross's `#extension ... : require` would fail to compile and MobileGL falls back
|
||||
// to stripping the NoPerspective decoration (smooth interpolation) via StripNoPerspectivePass.
|
||||
Bool SupportsNoperspectiveInterpolation = false;
|
||||
// GL_RENDERER contains "ANGLE".
|
||||
Bool IsAngleRenderer = false;
|
||||
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
||||
|
||||
@@ -401,6 +401,230 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
disabledNote);
|
||||
}
|
||||
|
||||
// Compiles + links a two-stage program on the probe context. Returns 0 on failure and writes a
|
||||
// human-readable reason into |detail|.
|
||||
GLuint CompileLinkProgram(const MG_External::GLESFunctionsTable& g, const char* vs, const char* fs,
|
||||
String& detail) {
|
||||
const auto compile = [&](GLenum stage, const char* src, GLuint& out) -> bool {
|
||||
out = g.glCreateShader(stage);
|
||||
if (out == 0) {
|
||||
detail = "glCreateShader returned 0";
|
||||
return false;
|
||||
}
|
||||
g.glShaderSource(out, 1, &src, nullptr);
|
||||
g.glCompileShader(out);
|
||||
GLint ok = GL_FALSE;
|
||||
g.glGetShaderiv(out, GL_COMPILE_STATUS, &ok);
|
||||
if (ok != GL_TRUE) {
|
||||
GLchar log[512] = {};
|
||||
GLsizei len = 0;
|
||||
g.glGetShaderInfoLog(out, static_cast<GLsizei>(sizeof(log) - 1), &len, log);
|
||||
detail = format("{} shader compile failed: {}",
|
||||
stage == GL_VERTEX_SHADER ? "vertex" : "fragment",
|
||||
len > 0 ? log : "(no info log)");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
GLuint v = 0, f = 0;
|
||||
const ScopeGuard delV([&]() { if (v) g.glDeleteShader(v); });
|
||||
const ScopeGuard delF([&]() { if (f) g.glDeleteShader(f); });
|
||||
if (!compile(GL_VERTEX_SHADER, vs, v) || !compile(GL_FRAGMENT_SHADER, fs, f)) {
|
||||
return 0;
|
||||
}
|
||||
const GLuint prog = g.glCreateProgram();
|
||||
if (prog == 0) {
|
||||
detail = "glCreateProgram returned 0";
|
||||
return 0;
|
||||
}
|
||||
g.glAttachShader(prog, v);
|
||||
g.glAttachShader(prog, f);
|
||||
g.glLinkProgram(prog);
|
||||
GLint linked = GL_FALSE;
|
||||
g.glGetProgramiv(prog, GL_LINK_STATUS, &linked);
|
||||
if (linked != GL_TRUE) {
|
||||
detail = "program link failed";
|
||||
g.glDeleteProgram(prog);
|
||||
return 0;
|
||||
}
|
||||
return prog;
|
||||
}
|
||||
|
||||
// "noperspective interpolation" row - a real correctness render, not just a compile. A viewport-
|
||||
// filling quad is drawn with strong perspective (left clip-w 1, right clip-w 8) and a varying that
|
||||
// runs 0..1 across it. At the screen centre screen-linear interpolation gives 0.5 while perspective-
|
||||
// correct gives 1/(w+1) ~= 0.11, so reading the centre texel tells the two apart. The varying is
|
||||
// carried either through the native `noperspective` qualifier (extension present) or through the
|
||||
// exact gl_Position.w / gl_FragCoord.w rewrite MobileGL applies when it is absent. Verdict:
|
||||
// PASS - extension present and the native noperspective result is screen-linear;
|
||||
// WARN - extension absent but the gl_Position.w/gl_FragCoord.w emulation renders screen-linear
|
||||
// (correct, just the fallback path shipping shader packs hit on such devices);
|
||||
// FAIL - either path renders perspective-correct / wrong (noperspective does not actually work),
|
||||
// or the program will not compile/link, or the render errors.
|
||||
// Requires the probe context to still be current.
|
||||
void ProbeGlesNoperspective(ReportBuilder& builder, const MG_External::GLESCapabilities& caps,
|
||||
const MG_External::GLESFunctionsTable& g) {
|
||||
const Bool native = caps.SupportsNoperspectiveInterpolation;
|
||||
const String pathNote = native ? "GL_NV_shader_noperspective_interpolation present (native path)"
|
||||
: "GL_NV_shader_noperspective_interpolation absent (gl_Position.w / "
|
||||
"gl_FragCoord.w emulation path)";
|
||||
const auto fail = [&](const String& detail) {
|
||||
builder.Fail("noperspective interpolation", pathNote + "; " + detail);
|
||||
};
|
||||
|
||||
if (!g.glCreateShader || !g.glShaderSource || !g.glCompileShader || !g.glGetShaderiv ||
|
||||
!g.glGetShaderInfoLog || !g.glDeleteShader || !g.glCreateProgram || !g.glAttachShader ||
|
||||
!g.glLinkProgram || !g.glGetProgramiv || !g.glUseProgram || !g.glDeleteProgram ||
|
||||
!g.glGenFramebuffers || !g.glBindFramebuffer || !g.glDeleteFramebuffers ||
|
||||
!g.glGenRenderbuffers || !g.glBindRenderbuffer || !g.glRenderbufferStorage ||
|
||||
!g.glFramebufferRenderbuffer || !g.glDeleteRenderbuffers || !g.glCheckFramebufferStatus ||
|
||||
!g.glGenBuffers || !g.glBindBuffer || !g.glBufferData || !g.glDeleteBuffers ||
|
||||
!g.glGetAttribLocation || !g.glVertexAttribPointer || !g.glEnableVertexAttribArray ||
|
||||
!g.glViewport || !g.glClearColor || !g.glClear || !g.glDrawArrays || !g.glReadPixels ||
|
||||
!g.glFinish || !g.glGetError) {
|
||||
fail("the render entry points did not resolve through eglGetProcAddress");
|
||||
return;
|
||||
}
|
||||
|
||||
// Match MobileGL's own ESSL target (the device's version). At #version 300 es some drivers
|
||||
// (Adreno) still treat `noperspective` as reserved even with the extension enabled; the ES 3.2
|
||||
// form the backend actually emits compiles. Emulated shaders are version-agnostic but use the
|
||||
// same header for consistency.
|
||||
const Int esslVer = caps.GLESVersion.Major * 100 + caps.GLESVersion.Minor * 10;
|
||||
const String header = format("#version {} es\n", esslVer >= 300 ? esslVer : 300);
|
||||
static const char* const kVsNativeBody =
|
||||
"#extension GL_NV_shader_noperspective_interpolation : require\n"
|
||||
"in vec4 a_pos;\n"
|
||||
"in float a_v;\n"
|
||||
"noperspective out highp float v_out;\n"
|
||||
"void main() { gl_Position = a_pos; v_out = a_v; }\n";
|
||||
static const char* const kFsNativeBody =
|
||||
"#extension GL_NV_shader_noperspective_interpolation : require\n"
|
||||
"precision highp float;\n"
|
||||
"noperspective in highp float v_out;\n"
|
||||
"out vec4 fragColor;\n"
|
||||
"void main() { fragColor = vec4(v_out, 0.0, 0.0, 1.0); }\n";
|
||||
// Exactly MobileGL's emulation (verified against EmulateNoPerspectivePass output): pre-multiply
|
||||
// the varying by clip-w in the vertex stage, recover with gl_FragCoord.w in the fragment stage,
|
||||
// no noperspective qualifier (so the driver interpolates it perspective-correct).
|
||||
static const char* const kVsEmuBody =
|
||||
"in vec4 a_pos;\n"
|
||||
"in float a_v;\n"
|
||||
"out highp float v_out;\n"
|
||||
"void main() { gl_Position = a_pos; v_out = a_v * gl_Position.w; }\n";
|
||||
static const char* const kFsEmuBody =
|
||||
"precision highp float;\n"
|
||||
"in highp float v_out;\n"
|
||||
"out vec4 fragColor;\n"
|
||||
"void main() { fragColor = vec4(v_out * gl_FragCoord.w, 0.0, 0.0, 1.0); }\n";
|
||||
|
||||
while (g.glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
|
||||
const String vsSrc = header + (native ? kVsNativeBody : kVsEmuBody);
|
||||
const String fsSrc = header + (native ? kFsNativeBody : kFsEmuBody);
|
||||
String linkDetail;
|
||||
const GLuint prog = CompileLinkProgram(g, vsSrc.c_str(), fsSrc.c_str(), linkDetail);
|
||||
if (prog == 0) {
|
||||
fail(native ? "a noperspective program failed to build though the extension is advertised: " +
|
||||
linkDetail
|
||||
: "the emulation program failed to build: " + linkDetail);
|
||||
return;
|
||||
}
|
||||
const ScopeGuard delProg([&]() { g.glDeleteProgram(prog); });
|
||||
|
||||
// 9x9 so the centre texel (4,4) sits exactly at NDC (0,0).
|
||||
constexpr GLsizei kDim = 9;
|
||||
GLuint rbo = 0, fbo = 0, vbo = 0;
|
||||
g.glGenRenderbuffers(1, &rbo);
|
||||
const ScopeGuard delRbo([&]() { if (rbo) g.glDeleteRenderbuffers(1, &rbo); });
|
||||
g.glBindRenderbuffer(GL_RENDERBUFFER, rbo);
|
||||
g.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kDim, kDim);
|
||||
g.glGenFramebuffers(1, &fbo);
|
||||
const ScopeGuard delFbo([&]() {
|
||||
if (fbo) {
|
||||
g.glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
g.glDeleteFramebuffers(1, &fbo);
|
||||
}
|
||||
});
|
||||
g.glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
g.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo);
|
||||
if (g.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
fail("the probe framebuffer is incomplete");
|
||||
return;
|
||||
}
|
||||
|
||||
// Interleaved [vec4 clip-pos, float v]. Left w=1, right w=8; x/y pre-multiplied by w so the quad
|
||||
// still fills NDC after the perspective divide.
|
||||
const GLfloat verts[] = {
|
||||
-1.f, -1.f, 0.f, 1.f, 0.f, //
|
||||
8.f, -8.f, 0.f, 8.f, 1.f, //
|
||||
-1.f, 1.f, 0.f, 1.f, 0.f, //
|
||||
8.f, 8.f, 0.f, 8.f, 1.f, //
|
||||
};
|
||||
g.glGenBuffers(1, &vbo);
|
||||
const ScopeGuard delVbo([&]() { if (vbo) g.glDeleteBuffers(1, &vbo); });
|
||||
g.glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
g.glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
|
||||
|
||||
g.glUseProgram(prog);
|
||||
const GLint posLoc = g.glGetAttribLocation(prog, "a_pos");
|
||||
const GLint vLoc = g.glGetAttribLocation(prog, "a_v");
|
||||
if (posLoc < 0 || vLoc < 0) {
|
||||
fail("the probe vertex attributes did not resolve");
|
||||
return;
|
||||
}
|
||||
g.glEnableVertexAttribArray(static_cast<GLuint>(posLoc));
|
||||
g.glVertexAttribPointer(static_cast<GLuint>(posLoc), 4, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat),
|
||||
reinterpret_cast<const void*>(0));
|
||||
g.glEnableVertexAttribArray(static_cast<GLuint>(vLoc));
|
||||
g.glVertexAttribPointer(static_cast<GLuint>(vLoc), 1, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat),
|
||||
reinterpret_cast<const void*>(4 * sizeof(GLfloat)));
|
||||
|
||||
g.glViewport(0, 0, kDim, kDim);
|
||||
g.glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||
g.glClear(GL_COLOR_BUFFER_BIT);
|
||||
g.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
g.glFinish();
|
||||
|
||||
const GLenum drawError = g.glGetError();
|
||||
if (drawError != GL_NO_ERROR) {
|
||||
fail(format("GL error 0x{:x} while rendering the probe quad", drawError));
|
||||
return;
|
||||
}
|
||||
|
||||
GLubyte center[4] = {};
|
||||
g.glReadPixels(kDim / 2, kDim / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, center);
|
||||
const GLenum readError = g.glGetError();
|
||||
if (readError != GL_NO_ERROR) {
|
||||
fail(format("GL error 0x{:x} while reading the probe pixel back", readError));
|
||||
return;
|
||||
}
|
||||
|
||||
// At the centre: screen-linear -> 0.5 (~128); perspective-correct -> 1/(8+1) ~= 0.111 (~28).
|
||||
const float observed = static_cast<float>(center[0]) / 255.0f;
|
||||
const int observedByte = center[0];
|
||||
constexpr float kScreenLinear = 0.5f;
|
||||
const bool screenLinear = observed > 0.5f * (kScreenLinear + 1.0f / 9.0f); // midpoint ~= 0.306
|
||||
if (!screenLinear) {
|
||||
fail(format("the centre texel read {} (~{:.3f}); expected the screen-linear ~0.5 - "
|
||||
"interpolation came out perspective-correct, so noperspective does not work here",
|
||||
observedByte, observed));
|
||||
return;
|
||||
}
|
||||
if (native) {
|
||||
builder.Pass("noperspective interpolation",
|
||||
pathNote + format("; native noperspective renders screen-linear (centre {} ~= 0.5)",
|
||||
observedByte));
|
||||
} else {
|
||||
builder.Warn("noperspective interpolation",
|
||||
pathNote +
|
||||
format("; the emulation renders screen-linear correctly (centre {} ~= 0.5), "
|
||||
"but this is the fallback path with less driver coverage",
|
||||
observedByte));
|
||||
}
|
||||
}
|
||||
|
||||
// Everything the "MobileGL reported ..." rows need from the GLES device probe.
|
||||
struct GlesProbeSummary {
|
||||
Bool capsValid = false;
|
||||
@@ -527,6 +751,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.report.rendererInfo = format("{} ({})", caps.GLESRendererString, caps.GLESVersionString);
|
||||
EvaluateGlesChecklist(builder, caps, glesFuncs);
|
||||
ProbeGlesTimerQuery(builder, caps, glesFuncs);
|
||||
ProbeGlesNoperspective(builder, caps, glesFuncs);
|
||||
builder.report.formatCapabilities.emplace();
|
||||
MG_Backend::DirectGLES::PopulateFormatCapabilities(
|
||||
glesFuncs, caps, builder.report.formatCapabilities.value());
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
#include "spirv-tools/libspirv.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
@@ -334,6 +336,30 @@ namespace MobileGL {
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::StripNoPerspectiveForEssl(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(StripNoPerspectivePass::CreateStripNoPerspectivePass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::EmulateNoPerspectiveForEssl(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(EmulateNoPerspectivePass::CreateEmulateNoPerspectivePass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -33,6 +33,16 @@ namespace MobileGL {
|
||||
// Only for the DirectGLES transpile path.
|
||||
static bool StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Removes NoPerspective decorations so SPIRV-Cross emits plain (smooth) ESSL varyings.
|
||||
// DirectGLES fallback only, for devices lacking GL_NV_shader_noperspective_interpolation
|
||||
// (SPIRV-Cross would otherwise require that extension and the driver would reject it).
|
||||
static bool StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Emulates noperspective (screen-linear) interpolation via gl_Position.w / gl_FragCoord.w
|
||||
// so no NV extension is needed; strips what it cannot emulate. DirectGLES fallback for
|
||||
// devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass.
|
||||
static bool EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
|
||||
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
|
||||
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
|
||||
|
||||
@@ -956,15 +956,28 @@ namespace {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect the directive on a comment/string-masked copy so a commented-out
|
||||
// "#extension GL_ARB_gpu_shader_int64" is never turned into a synthesized #error. Comments are
|
||||
// no longer blanked in the delivered source (glslang handles them), so this pass must mask
|
||||
// locally like its siblings. Masking preserves offsets, so edits collected against the scan
|
||||
// apply verbatim to `source`; they are applied back-to-front to keep earlier offsets valid.
|
||||
const MobileGL::String scan = MaskCommentsAndQuotedText(source);
|
||||
struct DirectiveEdit {
|
||||
SizeT pos;
|
||||
SizeT len;
|
||||
MobileGL::String replacement;
|
||||
};
|
||||
Vector<DirectiveEdit> edits;
|
||||
|
||||
SizeT lineStart = 0;
|
||||
while (lineStart < source.size()) {
|
||||
SizeT lineEnd = source.find('\n', lineStart);
|
||||
while (lineStart < scan.size()) {
|
||||
SizeT lineEnd = scan.find('\n', lineStart);
|
||||
const bool hasLineBreak = lineEnd != MobileGL::String::npos;
|
||||
if (!hasLineBreak) {
|
||||
lineEnd = source.size();
|
||||
lineEnd = scan.size();
|
||||
}
|
||||
|
||||
const MobileGL::String line = source.substr(lineStart, lineEnd - lineStart);
|
||||
const MobileGL::String line = scan.substr(lineStart, lineEnd - lineStart);
|
||||
SizeT probe = 0;
|
||||
while (probe < line.size() && std::isspace(static_cast<unsigned char>(line[probe]))) {
|
||||
probe++;
|
||||
@@ -1006,16 +1019,12 @@ namespace {
|
||||
const MobileGL::String behavior = TrimDirectiveToken(line.substr(probe));
|
||||
const SizeT replaceLen = lineEnd - lineStart + (hasLineBreak ? 1 : 0);
|
||||
if (behavior == "require") {
|
||||
const MobileGL::String replacement =
|
||||
"#error GL_ARB_gpu_shader_int64 is not advertised by MobileGL\n";
|
||||
source.replace(lineStart, replaceLen, replacement);
|
||||
lineStart += replacement.size();
|
||||
edits.push_back({lineStart, replaceLen,
|
||||
"#error GL_ARB_gpu_shader_int64 is not advertised by MobileGL\n"});
|
||||
} else if (behavior == "enable" || behavior == "warn") {
|
||||
source.replace(lineStart, replaceLen, "\n");
|
||||
lineStart++;
|
||||
} else {
|
||||
lineStart = lineEnd + (hasLineBreak ? 1 : 0);
|
||||
edits.push_back({lineStart, replaceLen, "\n"});
|
||||
}
|
||||
lineStart = lineEnd + (hasLineBreak ? 1 : 0);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1025,6 +1034,10 @@ namespace {
|
||||
lineStart = lineEnd + (hasLineBreak ? 1 : 0);
|
||||
}
|
||||
|
||||
for (auto it = edits.rbegin(); it != edits.rend(); ++it) {
|
||||
source.replace(it->pos, it->len, it->replacement);
|
||||
}
|
||||
|
||||
ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64");
|
||||
}
|
||||
|
||||
@@ -1247,19 +1260,22 @@ namespace MobileGL {
|
||||
const ShaderLanguageInfo originalLanguage = InspectShaderLanguage(source);
|
||||
NormalizeVersionDirective(source, originalLanguage);
|
||||
|
||||
BlankBlockComments(source);
|
||||
|
||||
// Comments are left intact for glslang's own preprocessor: a block comment is a single
|
||||
// preprocessing token that collapses to one space even across newlines and inside a
|
||||
// directive, so blanking it here (which preserved the interior newlines) truncated
|
||||
// multi-line #define bodies and broke otherwise-valid shaders (KHR-GL3x.shaders.
|
||||
// preprocessor multiline_comment_define / redefine_object / function_redefinition).
|
||||
// Every MobileGL pass that must ignore comment/string text already masks them locally
|
||||
// via MaskCommentsAndQuotedText/TokenizeCode, so the source we hand glslang keeps them.
|
||||
NormalizeLineDirectives(source);
|
||||
|
||||
// remove "noperspective"
|
||||
const char* str_np = "noperspective";
|
||||
const SizeT len_np = strlen(str_np);
|
||||
SizeT noperspectivePos = source.find(str_np);
|
||||
while (noperspectivePos != String::npos) {
|
||||
// + length of "\n"
|
||||
source = source.replace(noperspectivePos, len_np, "");
|
||||
noperspectivePos = source.find(str_np);
|
||||
}
|
||||
// noperspective is intentionally NOT touched here. It is core in desktop GLSL (1.30+)
|
||||
// and maps to the core SPIR-V NoPerspective decoration, which DirectVulkan renders
|
||||
// natively and SPIRV-Cross turns into ESSL `noperspective` + the
|
||||
// GL_NV_shader_noperspective_interpolation extension. The old naked substring erase
|
||||
// both discarded that interpolation (shader packs need it) and corrupted any
|
||||
// identifier that merely contained the word. The GLES fallback for devices without
|
||||
// the extension lives in the backend, where device capabilities are known.
|
||||
|
||||
FilterUnsupportedGpuShaderInt64(source);
|
||||
CoerceUniformBlockPackingToStd140(source);
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.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 "EmulateNoPerspectivePass.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 <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
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 VariablePointeeType(IRContext* ctx, Instruction* var) {
|
||||
Instruction* ptrType = ctx->get_def_use_mgr()->GetDef(var->type_id());
|
||||
// OpTypePointer <storage-class> <pointee>
|
||||
return ptrType->GetSingleWordInOperand(1);
|
||||
}
|
||||
|
||||
// If |typeId| is float or a vector of float, returns true and reports the scalar float
|
||||
// type and whether it is a vector. Matrices, structs, ints etc. are not emulatable.
|
||||
bool IsFloatScalarOrVector(IRContext* ctx, uint32_t typeId, uint32_t& floatTypeId, bool& isVector) {
|
||||
Instruction* t = ctx->get_def_use_mgr()->GetDef(typeId);
|
||||
if (t == nullptr) return false;
|
||||
if (t->opcode() == spv::Op::OpTypeFloat) {
|
||||
floatTypeId = typeId;
|
||||
isVector = false;
|
||||
return true;
|
||||
}
|
||||
if (t->opcode() == spv::Op::OpTypeVector) {
|
||||
const uint32_t comp = t->GetSingleWordInOperand(0);
|
||||
Instruction* ct = ctx->get_def_use_mgr()->GetDef(comp);
|
||||
if (ct != nullptr && ct->opcode() == spv::Op::OpTypeFloat) {
|
||||
floatTypeId = comp;
|
||||
isVector = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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 V4FloatType(IRContext* ctx) {
|
||||
analysis::Float f(32);
|
||||
analysis::Type* freg = ctx->get_type_mgr()->GetRegisteredType(&f);
|
||||
analysis::Vector v(freg, 4);
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&v);
|
||||
}
|
||||
|
||||
uint32_t FloatType(IRContext* ctx) {
|
||||
analysis::Float f(32);
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&f);
|
||||
}
|
||||
|
||||
uint32_t SignedIntConstant(IRContext* ctx, int32_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, {static_cast<uint32_t>(value)});
|
||||
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
|
||||
}
|
||||
|
||||
// Multiply |valueId| (of type |valueTypeId|) by the scalar |scalarId|, inserting the op
|
||||
// before |before|. Returns the product's id.
|
||||
uint32_t InsertScale(IRContext* ctx, Instruction* before, uint32_t valueTypeId,
|
||||
uint32_t valueId, uint32_t scalarId, bool isVector) {
|
||||
const uint32_t productId = ctx->TakeNextId();
|
||||
const spv::Op op = isVector ? spv::Op::OpVectorTimesScalar : spv::Op::OpFMul;
|
||||
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, op, valueTypeId, productId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {valueId}},
|
||||
{SPV_OPERAND_TYPE_ID, {scalarId}}}));
|
||||
return productId;
|
||||
}
|
||||
|
||||
// --- Vertex stage: gl_Position discovery ------------------------------------------
|
||||
|
||||
// Finds gl_Position as member |memberIndex| of a gl_PerVertex-style block whose Output
|
||||
// variable is |blockVarId|; |v4floatTypeId| is that member's (vec4) type. Returns false
|
||||
// if gl_Position is not a block member (older plain-variable form is left to the strip).
|
||||
bool FindPositionBlock(IRContext* ctx, uint32_t& blockVarId, uint32_t& memberIndex,
|
||||
uint32_t& v4floatTypeId) {
|
||||
uint32_t structId = 0;
|
||||
uint32_t member = 0;
|
||||
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::Position) {
|
||||
structId = ann.GetSingleWordInOperand(0);
|
||||
member = ann.GetSingleWordInOperand(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (structId == 0) return false;
|
||||
|
||||
Instruction* structType = ctx->get_def_use_mgr()->GetDef(structId);
|
||||
if (structType == nullptr || member >= structType->NumInOperands()) return false;
|
||||
v4floatTypeId = structType->GetSingleWordInOperand(member);
|
||||
|
||||
for (Instruction& inst : ctx->module()->types_values()) {
|
||||
if (inst.opcode() == spv::Op::OpVariable &&
|
||||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) ==
|
||||
spv::StorageClass::Output &&
|
||||
VariablePointeeType(ctx, &inst) == structId) {
|
||||
blockVarId = inst.result_id();
|
||||
memberIndex = member;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Fragment stage: gl_FragCoord discovery/synthesis -----------------------------
|
||||
|
||||
Instruction* FindBuiltinInput(IRContext* ctx, spv::BuiltIn builtin) {
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() != spv::Op::OpDecorate || ann.NumInOperands() < 3) continue;
|
||||
if (static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) !=
|
||||
spv::Decoration::BuiltIn)
|
||||
continue;
|
||||
if (static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(2)) != builtin) continue;
|
||||
Instruction* var = ctx->get_def_use_mgr()->GetDef(ann.GetSingleWordInOperand(0));
|
||||
if (var != nullptr && var->opcode() == spv::Op::OpVariable &&
|
||||
static_cast<spv::StorageClass>(var->GetSingleWordInOperand(0)) ==
|
||||
spv::StorageClass::Input) {
|
||||
return var;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32_t SynthesizeFragCoord(IRContext* ctx, uint32_t v4floatTypeId) {
|
||||
const uint32_t ptrType = PointerTypeTo(ctx, v4floatTypeId, spv::StorageClass::Input);
|
||||
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::Input)}}}));
|
||||
ctx->AddAnnotationInst(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpDecorate, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {varId}},
|
||||
{SPV_OPERAND_TYPE_DECORATION,
|
||||
{static_cast<uint32_t>(spv::Decoration::BuiltIn)}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER,
|
||||
{static_cast<uint32_t>(spv::BuiltIn::FragCoord)}}}));
|
||||
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||
ep.AddOperand({SPV_OPERAND_TYPE_ID, {varId}});
|
||||
}
|
||||
return varId;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status EmulateNoPerspectivePass::Process() {
|
||||
auto* ctx = context();
|
||||
const spv::ExecutionModel model = EntryExecutionModel(ctx);
|
||||
const bool isVertex = model == spv::ExecutionModel::Vertex;
|
||||
const bool isFragment = model == spv::ExecutionModel::Fragment;
|
||||
|
||||
// Collect NoPerspective-decorated plain variables and every NoPerspective annotation.
|
||||
std::vector<uint32_t> plainVarIds;
|
||||
std::vector<Instruction*> decorationsToKill;
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 2 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) ==
|
||||
spv::Decoration::NoPerspective) {
|
||||
plainVarIds.push_back(ann.GetSingleWordInOperand(0));
|
||||
decorationsToKill.push_back(&ann);
|
||||
} else if (ann.opcode() == spv::Op::OpMemberDecorate && ann.NumInOperands() >= 3 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) ==
|
||||
spv::Decoration::NoPerspective) {
|
||||
// Block-member noperspective is not emulated here; the decoration is stripped
|
||||
// (smooth fallback) so SPIRV-Cross does not require the NV extension.
|
||||
decorationsToKill.push_back(&ann);
|
||||
}
|
||||
}
|
||||
|
||||
if (decorationsToKill.empty()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
const spv::StorageClass wantStorage =
|
||||
isVertex ? spv::StorageClass::Output : spv::StorageClass::Input;
|
||||
|
||||
// Emulatable = plain variable of the stage's interface direction, float or floatN.
|
||||
struct Target {
|
||||
Instruction* var;
|
||||
uint32_t typeId;
|
||||
uint32_t floatTypeId;
|
||||
bool isVector;
|
||||
};
|
||||
std::vector<Target> targets;
|
||||
if (isVertex || isFragment) {
|
||||
for (const uint32_t id : plainVarIds) {
|
||||
Instruction* var = ctx->get_def_use_mgr()->GetDef(id);
|
||||
if (var == nullptr || var->opcode() != spv::Op::OpVariable) continue;
|
||||
if (static_cast<spv::StorageClass>(var->GetSingleWordInOperand(0)) != wantStorage)
|
||||
continue;
|
||||
const uint32_t pointee = VariablePointeeType(ctx, var);
|
||||
uint32_t floatTypeId = 0;
|
||||
bool isVector = false;
|
||||
if (IsFloatScalarOrVector(ctx, pointee, floatTypeId, isVector)) {
|
||||
targets.push_back({var, pointee, floatTypeId, isVector});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force highp on the varyings we emulate: the a*w round-trip overflows a mediump (fp16)
|
||||
// varying at large clip-space w. Dropping RelaxedPrecision makes SPIRV-Cross emit them
|
||||
// highp on both stages, keeping the emulation exact. Only touches emulated variables.
|
||||
if (!targets.empty()) {
|
||||
std::vector<uint32_t> targetIds;
|
||||
targetIds.reserve(targets.size());
|
||||
for (const Target& t : targets) targetIds.push_back(t.var->result_id());
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 2 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) ==
|
||||
spv::Decoration::RelaxedPrecision &&
|
||||
std::find(targetIds.begin(), targetIds.end(),
|
||||
ann.GetSingleWordInOperand(0)) != targetIds.end()) {
|
||||
decorationsToKill.push_back(&ann);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isVertex && !targets.empty()) {
|
||||
uint32_t blockVarId = 0;
|
||||
uint32_t memberIndex = 0;
|
||||
uint32_t v4floatTypeId = 0;
|
||||
if (FindPositionBlock(ctx, blockVarId, memberIndex, v4floatTypeId)) {
|
||||
const uint32_t ptrOutV4 =
|
||||
PointerTypeTo(ctx, v4floatTypeId, spv::StorageClass::Output);
|
||||
const uint32_t memberConst = SignedIntConstant(ctx, static_cast<int32_t>(memberIndex));
|
||||
const uint32_t floatTy = FloatType(ctx);
|
||||
|
||||
uint32_t entryFuncId = 0;
|
||||
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||
// OpEntryPoint <model> <function> "name" <interface...>
|
||||
entryFuncId = ep.GetSingleWordInOperand(1);
|
||||
break;
|
||||
}
|
||||
|
||||
// Pre-multiply every target output by gl_Position.w before each return of the
|
||||
// ENTRY function only. glslang does not inline, so a called helper survives as
|
||||
// its own OpFunction; instrumenting its returns too would scale the varying
|
||||
// more than once (w^2), breaking the identity.
|
||||
for (auto funcIt = ctx->module()->begin(); funcIt != ctx->module()->end(); ++funcIt) {
|
||||
if (funcIt->result_id() != entryFuncId) continue;
|
||||
funcIt->ForEachInst([&](Instruction* inst) {
|
||||
if (inst->opcode() != spv::Op::OpReturn &&
|
||||
inst->opcode() != spv::Op::OpReturnValue) {
|
||||
return;
|
||||
}
|
||||
const uint32_t posPtrId = ctx->TakeNextId();
|
||||
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpAccessChain, ptrOutV4, posPtrId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {blockVarId}},
|
||||
{SPV_OPERAND_TYPE_ID, {memberConst}}}));
|
||||
const uint32_t posId = ctx->TakeNextId();
|
||||
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLoad, v4floatTypeId, posId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {posPtrId}}}));
|
||||
const uint32_t wId = ctx->TakeNextId();
|
||||
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpCompositeExtract, floatTy, wId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {posId}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {3u}}}));
|
||||
for (const Target& t : targets) {
|
||||
const uint32_t valId = ctx->TakeNextId();
|
||||
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLoad, t.typeId, valId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {t.var->result_id()}}}));
|
||||
const uint32_t scaledId =
|
||||
InsertScale(ctx, inst, t.typeId, valId, wId, t.isVector);
|
||||
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpStore, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {t.var->result_id()}},
|
||||
{SPV_OPERAND_TYPE_ID, {scaledId}}}));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isFragment && !targets.empty()) {
|
||||
Instruction* fragCoord = FindBuiltinInput(ctx, spv::BuiltIn::FragCoord);
|
||||
uint32_t fragCoordId = 0;
|
||||
uint32_t v4floatTypeId = 0;
|
||||
if (fragCoord != nullptr) {
|
||||
fragCoordId = fragCoord->result_id();
|
||||
v4floatTypeId = VariablePointeeType(ctx, fragCoord);
|
||||
} else {
|
||||
v4floatTypeId = V4FloatType(ctx);
|
||||
fragCoordId = SynthesizeFragCoord(ctx, v4floatTypeId);
|
||||
}
|
||||
const uint32_t floatTy = FloatType(ctx);
|
||||
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
for (const Target& t : targets) {
|
||||
// Collect every load that reads the varying. glslang lowers a whole-variable
|
||||
// read to OpLoad(var), but a single-component read (v.x) to
|
||||
// OpAccessChain(var) + OpLoad(chain). Both must be scaled; the identity is
|
||||
// per-component, so scaling one loaded component by gl_FragCoord.w is valid.
|
||||
std::vector<Instruction*> loads;
|
||||
defUse->ForEachUser(t.var, [&](Instruction* user) {
|
||||
if (user->opcode() == spv::Op::OpLoad &&
|
||||
user->GetSingleWordInOperand(0) == t.var->result_id()) {
|
||||
loads.push_back(user);
|
||||
} else if (user->opcode() == spv::Op::OpAccessChain &&
|
||||
user->GetSingleWordInOperand(0) == t.var->result_id()) {
|
||||
const uint32_t chainId = user->result_id();
|
||||
defUse->ForEachUser(user, [&](Instruction* chainUser) {
|
||||
if (chainUser->opcode() == spv::Op::OpLoad &&
|
||||
chainUser->GetSingleWordInOperand(0) == chainId) {
|
||||
loads.push_back(chainUser);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Rewrite `%r = OpLoad %ty %ptr` into
|
||||
// %orig = OpLoad %ty %ptr
|
||||
// %fc = OpLoad %v4float %fragCoord
|
||||
// %w = OpCompositeExtract %float %fc 3
|
||||
// %r = OpVectorTimesScalar/OpFMul %ty %orig %w (reuse %r: uses stay intact)
|
||||
// The op is chosen from the LOAD's own result type: a whole-vector load scales
|
||||
// with OpVectorTimesScalar, a scalar component load with OpFMul.
|
||||
for (Instruction* load : loads) {
|
||||
const uint32_t loadType = load->type_id();
|
||||
uint32_t componentFloat = 0;
|
||||
bool loadIsVector = false;
|
||||
if (!IsFloatScalarOrVector(ctx, loadType, componentFloat, loadIsVector)) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t ptrId = load->GetSingleWordInOperand(0);
|
||||
const uint32_t origId = ctx->TakeNextId();
|
||||
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLoad, loadType, origId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {ptrId}}}));
|
||||
const uint32_t fcId = ctx->TakeNextId();
|
||||
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLoad, v4floatTypeId, fcId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {fragCoordId}}}));
|
||||
const uint32_t wId = ctx->TakeNextId();
|
||||
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpCompositeExtract, floatTy, wId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {fcId}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {3u}}}));
|
||||
load->SetOpcode(loadIsVector ? spv::Op::OpVectorTimesScalar : spv::Op::OpFMul);
|
||||
load->SetInOperands(Instruction::OperandList{
|
||||
{SPV_OPERAND_TYPE_ID, {origId}}, {SPV_OPERAND_TYPE_ID, {wId}}});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strip every NoPerspective decoration: emulated varyings now transport smooth, and
|
||||
// non-emulatable ones fall back to smooth.
|
||||
for (Instruction* dec : decorationsToKill) {
|
||||
ctx->KillInst(dec);
|
||||
}
|
||||
|
||||
ctx->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken EmulateNoPerspectivePass::CreateEmulateNoPerspectivePass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<EmulateNoPerspectivePass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,41 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.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 {
|
||||
// Emulates 'noperspective' (screen-linear) interpolation on GLES devices that lack
|
||||
// GL_NV_shader_noperspective_interpolation, so no NV extension is required. The hardware
|
||||
// interpolates perspective-correct; screen-linear L(a) is recovered from the identity
|
||||
// L(a) = P(a * w) * gl_FragCoord.w
|
||||
// where P is perspective-correct interpolation and w is the vertex clip-space w. So each
|
||||
// NoPerspective-decorated output is pre-multiplied by gl_Position.w in the vertex stage
|
||||
// and each NoPerspective-decorated input is multiplied by gl_FragCoord.w in the fragment
|
||||
// stage; the decoration is then removed so the varying transports smooth. This is exact
|
||||
// (modulo float precision - the emulated varyings want highp).
|
||||
//
|
||||
// Scope: plain interface variables of float or floatN type. Anything it cannot emulate
|
||||
// (interface-block members, matrices, or a stage lacking the needed builtin) has its
|
||||
// NoPerspective decoration stripped instead, degrading to smooth - the same result the
|
||||
// extension-less fallback produced before, and never invalid SPIR-V. DirectGLES only.
|
||||
class EmulateNoPerspectivePass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "emulate-noperspective"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateEmulateNoPerspectivePass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,72 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.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 "StripNoPerspectivePass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
|
||||
// OpDecorate <target-id> <decoration> [literals...]
|
||||
// OpMemberDecorate <struct-id> <member> <decoration> [literals...]
|
||||
constexpr uint32_t kDecorateDecorationOperand = 1;
|
||||
constexpr uint32_t kMemberDecorateDecorationOperand = 2;
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status StripNoPerspectivePass::Process() {
|
||||
auto* irContext = context();
|
||||
|
||||
// Collect first: KillInst mutates the annotation list being walked.
|
||||
std::vector<Instruction*> toKill;
|
||||
for (Instruction& annotation : irContext->annotations()) {
|
||||
uint32_t decorationOperand = 0;
|
||||
if (annotation.opcode() == spv::Op::OpDecorate) {
|
||||
decorationOperand = kDecorateDecorationOperand;
|
||||
} else if (annotation.opcode() == spv::Op::OpMemberDecorate) {
|
||||
decorationOperand = kMemberDecorateDecorationOperand;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (annotation.NumInOperands() <= decorationOperand) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(decorationOperand)) ==
|
||||
spv::Decoration::NoPerspective) {
|
||||
toKill.push_back(&annotation);
|
||||
}
|
||||
}
|
||||
|
||||
if (toKill.empty()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
for (Instruction* inst : toKill) {
|
||||
irContext->KillInst(inst);
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken StripNoPerspectivePass::CreateStripNoPerspectivePass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<StripNoPerspectivePass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,35 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.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 {
|
||||
// Removes the NoPerspective decoration from every interface variable and block member.
|
||||
// DirectGLES fallback only, for devices that lack GL_NV_shader_noperspective_interpolation:
|
||||
// SPIRV-Cross renders a NoPerspective-decorated varying as ESSL `noperspective` plus
|
||||
// `#extension GL_NV_shader_noperspective_interpolation : require`, which such a driver
|
||||
// rejects. Dropping the decoration falls the varying back to smooth (perspective-correct)
|
||||
// interpolation - the same visible result the old text-level strip produced, but without
|
||||
// corrupting identifiers and without touching DirectVulkan, where NoPerspective is native.
|
||||
// (The exact screen-linear emulation via gl_Position.w / gl_FragCoord.w is a later step.)
|
||||
class StripNoPerspectivePass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "strip-noperspective"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateStripNoPerspectivePass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
Reference in New Issue
Block a user