diff --git a/CMakeLists.txt b/CMakeLists.txt
index dd5bfaaf8d6d..91426ac0c95b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -67,6 +67,9 @@ set(FILAMENT_METAL_HANDLE_ARENA_SIZE_IN_MB "8" CACHE STRING
     "Size of the Metal handle arena, default 8."
 )
 
+# Enable exceptions by default in spirv-cross.
+set(SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS OFF)
+
 # ==================================================================================================
 # CMake policies
 # ==================================================================================================
@@ -339,6 +342,7 @@ endif()
 
 if (CYGWIN)
     set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions -fno-rtti")
+    set(SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS ON)
 endif()
 
 if (MSVC)
@@ -375,6 +379,7 @@ endif()
 # saved by -fno-exception and 10 KiB saved by -fno-rtti).
 if (ANDROID OR IOS OR WEBGL)
     set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -fno-exceptions -fno-rtti")
+    set(SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS ON)
 
     if (ANDROID OR WEBGL)
         # Omitting unwind info prevents the generation of readable stack traces in crash reports on iOS
@@ -386,6 +391,7 @@ endif()
 # std::visit, which is not supported on iOS 11.0 when exceptions are enabled.
 if (IOS)
     set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fno-exceptions")
+    set(SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS ON)
 endif()
 
 # With WebGL, we disable RTTI even for debug builds because we pass emscripten::val back and forth
diff --git a/NEW_RELEASE_NOTES.md b/NEW_RELEASE_NOTES.md
index c3161a7700b9..b731a9ccf55c 100644
--- a/NEW_RELEASE_NOTES.md
+++ b/NEW_RELEASE_NOTES.md
@@ -8,3 +8,4 @@ appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md).
 
 ## Release notes for next branch cut
 
+- matc: Support optimizations for ESSL 1.0 code.
diff --git a/filament/src/materials/blitLow.mat b/filament/src/materials/blitLow.mat
index 4bebc8612e60..09e6c79920c2 100644
--- a/filament/src/materials/blitLow.mat
+++ b/filament/src/materials/blitLow.mat
@@ -35,7 +35,7 @@ vertex {
 
 fragment {
     void postProcess(inout PostProcessInputs postProcess) {
-#if __VERSION__ == 100
+#if FILAMENT_EFFECTIVE_VERSION == 100
         postProcess.color = texture2D(materialParams_color, variable_vertex.xy);
 #else
         postProcess.color = textureLod(materialParams_color, variable_vertex.xy, 0.0);
diff --git a/libs/filamat/src/GLSLPostProcessor.cpp b/libs/filamat/src/GLSLPostProcessor.cpp
index 871bc2980022..00bdde0eff6d 100644
--- a/libs/filamat/src/GLSLPostProcessor.cpp
+++ b/libs/filamat/src/GLSLPostProcessor.cpp
@@ -32,6 +32,7 @@
 
 #include "MetalArgumentBuffer.h"
 #include "SpirvFixup.h"
+#include "utils/ostream.h"
 
 #include <filament/MaterialEnums.h>
 
@@ -395,7 +396,9 @@ bool GLSLPostProcessor::process(const std::string& inputShader, Config const& co
             break;
         case MaterialBuilder::Optimization::SIZE:
         case MaterialBuilder::Optimization::PERFORMANCE:
-            fullOptimization(tShader, config, internalConfig);
+            if (!fullOptimization(tShader, config, internalConfig)) {
+                return false;
+            }
             break;
     }
 
@@ -478,7 +481,7 @@ void GLSLPostProcessor::preprocessOptimization(glslang::TShader& tShader,
     }
 }
 
-void GLSLPostProcessor::fullOptimization(const TShader& tShader,
+bool GLSLPostProcessor::fullOptimization(const TShader& tShader,
         GLSLPostProcessor::Config const& config, InternalConfig& internalConfig) const {
     SpirvBlob spirv;
 
@@ -546,7 +549,16 @@ void GLSLPostProcessor::fullOptimization(const TShader& tShader,
             }
         }
 
+#ifdef SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS
         *internalConfig.glslOutput = glslCompiler.compile();
+#else
+        try {
+            *internalConfig.glslOutput = glslCompiler.compile();
+        } catch (spirv_cross::CompilerError e) {
+            slog.e << "ERROR: " << e.what() << io::endl;
+            return false;
+        }
+#endif
 
         // spirv-cross automatically redeclares gl_ClipDistance if it's used. Some drivers don't
         // like this, so we simply remove it.
@@ -559,6 +571,7 @@ void GLSLPostProcessor::fullOptimization(const TShader& tShader,
             str.replace(found, clipDistanceDefinition.length(), "");
         }
     }
+    return true;
 }
 
 std::shared_ptr<spvtools::Optimizer> GLSLPostProcessor::createOptimizer(
diff --git a/libs/filamat/src/GLSLPostProcessor.h b/libs/filamat/src/GLSLPostProcessor.h
index 8e3c2e1e4ac8..c13dece6369f 100644
--- a/libs/filamat/src/GLSLPostProcessor.h
+++ b/libs/filamat/src/GLSLPostProcessor.h
@@ -93,7 +93,7 @@ class GLSLPostProcessor {
         ShaderMinifier minifier;
     };
 
-    void fullOptimization(const glslang::TShader& tShader,
+    bool fullOptimization(const glslang::TShader& tShader,
             GLSLPostProcessor::Config const& config, InternalConfig& internalConfig) const;
 
     void preprocessOptimization(glslang::TShader& tShader,
diff --git a/libs/filamat/src/MaterialBuilder.cpp b/libs/filamat/src/MaterialBuilder.cpp
index d175c4e1c638..d43fb997340e 100644
--- a/libs/filamat/src/MaterialBuilder.cpp
+++ b/libs/filamat/src/MaterialBuilder.cpp
@@ -141,11 +141,10 @@ void MaterialBuilderBase::prepare(bool vulkanSemantics,
             });
             if (featureLevel == filament::backend::FeatureLevel::FEATURE_LEVEL_0
                 && shaderModel == ShaderModel::MOBILE) {
-                // ESSL1 code may never be compiled to SPIR-V.
                 mCodeGenPermutations.push_back({
                     shaderModel,
                     TargetApi::OPENGL,
-                    TargetLanguage::GLSL,
+                    glTargetLanguage,
                     filament::backend::FeatureLevel::FEATURE_LEVEL_0
                 });
             }
@@ -1421,15 +1420,15 @@ void MaterialBuilder::writeCommonChunks(ChunkContainer& container, MaterialInfo&
         uniforms.push_back({
                 "objectUniforms.data[0].morphTargetCount",
                 offsetof(PerRenderableUib, data[0].morphTargetCount), 1,
-                UniformType::UINT });
+                UniformType::INT });
         uniforms.push_back({
                 "objectUniforms.data[0].flagsChannels",
                 offsetof(PerRenderableUib, data[0].flagsChannels), 1,
-                UniformType::UINT });
+                UniformType::INT });
         uniforms.push_back({
                 "objectUniforms.data[0].objectId",
                 offsetof(PerRenderableUib, data[0].objectId), 1,
-                UniformType::UINT });
+                UniformType::INT });
         uniforms.push_back({
                 "objectUniforms.data[0].userData",
                 offsetof(PerRenderableUib, data[0].userData), 1,
diff --git a/libs/filamat/src/shaders/CodeGenerator.cpp b/libs/filamat/src/shaders/CodeGenerator.cpp
index b4818a50fdeb..2c20307b7f4e 100644
--- a/libs/filamat/src/shaders/CodeGenerator.cpp
+++ b/libs/filamat/src/shaders/CodeGenerator.cpp
@@ -151,6 +151,36 @@ utils::io::sstream& CodeGenerator::generateProlog(utils::io::sstream& out, Shade
         out << "#define FILAMENT_HAS_FEATURE_INSTANCING\n";
     }
 
+    // During compilation and optimization, __VERSION__ reflects the shader language version of the
+    // intermediate code, not the version of the final code. spirv-cross automatically adapts
+    // certain language features (e.g. fragment output) but leaves others untouched (e.g. sampler
+    // functions, bit shift operations). Client code may have to make decisions based on this
+    // information, so define a FILAMENT_EFFECTIVE_VERSION constant.
+    const char *effective_version;
+    if (mTargetLanguage == TargetLanguage::GLSL) {
+        effective_version = "__VERSION__";
+    } else {
+        switch (mShaderModel) {
+            case ShaderModel::MOBILE:
+                if (mFeatureLevel >= FeatureLevel::FEATURE_LEVEL_1) {
+                    effective_version = "300";
+                } else {
+                    effective_version = "100";
+                }
+                break;
+            case ShaderModel::DESKTOP:
+                if (mFeatureLevel >= FeatureLevel::FEATURE_LEVEL_2) {
+                    effective_version = "450";
+                } else {
+                    effective_version = "410";
+                }
+                break;
+            default:
+                assert(false);
+        }
+    }
+    generateDefine(out, "FILAMENT_EFFECTIVE_VERSION", effective_version);
+
     if (stage == ShaderStage::VERTEX) {
         CodeGenerator::generateDefine(out, "FLIP_UV_ATTRIBUTE", material.flipUV);
         CodeGenerator::generateDefine(out, "LEGACY_MORPHING", material.useLegacyMorphing);
@@ -271,8 +301,9 @@ utils::io::sstream& CodeGenerator::generateProlog(utils::io::sstream& out, Shade
     out << '\n';
     out << SHADERS_COMMON_DEFINES_GLSL_DATA;
 
-    if (mFeatureLevel > FeatureLevel::FEATURE_LEVEL_0 &&
-            material.featureLevel == FeatureLevel::FEATURE_LEVEL_0) {
+    if (material.featureLevel == FeatureLevel::FEATURE_LEVEL_0 &&
+            (mFeatureLevel > FeatureLevel::FEATURE_LEVEL_0
+                    || mTargetLanguage == TargetLanguage::SPIRV)) {
         // Insert compatibility definitions for ESSL 1.0 functions which were removed in ESSL 3.0.
 
         // This is the minimum required value according to the OpenGL ES Shading Language Version
@@ -460,11 +491,14 @@ io::sstream& CodeGenerator::generateOutput(io::sstream& out, ShaderStage type,
     const char* materialTypeString = getOutputTypeName(materialOutputType);
     const char* typeString = getOutputTypeName(outputType);
 
+    bool generate_essl3_code = mTargetLanguage == TargetLanguage::SPIRV
+            || mFeatureLevel >= FeatureLevel::FEATURE_LEVEL_1;
+
     out << "\n#define FRAG_OUTPUT"               << index << " " << name.c_str();
-    if (mFeatureLevel == FeatureLevel::FEATURE_LEVEL_0) {
-        out << "\n#define FRAG_OUTPUT_AT"        << index << " gl_FragColor";
-    } else {
+    if (generate_essl3_code) {
         out << "\n#define FRAG_OUTPUT_AT"        << index << " output_" << name.c_str();
+    } else {
+        out << "\n#define FRAG_OUTPUT_AT"        << index << " gl_FragColor";
     }
     out << "\n#define FRAG_OUTPUT_MATERIAL_TYPE" << index << " " << materialTypeString;
     out << "\n#define FRAG_OUTPUT_PRECISION"     << index << " " << precisionString;
@@ -472,7 +506,7 @@ io::sstream& CodeGenerator::generateOutput(io::sstream& out, ShaderStage type,
     out << "\n#define FRAG_OUTPUT_SWIZZLE"       << index << " " << swizzleString;
     out << "\n";
 
-    if (mFeatureLevel >= FeatureLevel::FEATURE_LEVEL_1) {
+    if (generate_essl3_code) {
         out << "\nlayout(location=" << index << ") out " << precisionString << " "
             << typeString << " output_" << name.c_str() << ";\n";
     }
diff --git a/shaders/src/depth_main.fs b/shaders/src/depth_main.fs
index 77608f3e15c4..e50d7620f171 100644
--- a/shaders/src/depth_main.fs
+++ b/shaders/src/depth_main.fs
@@ -57,7 +57,7 @@ void main() {
     fragColor.xy = computeDepthMomentsVSM(depth);
     fragColor.zw = computeDepthMomentsVSM(-1.0 / depth); // requires at least RGBA16F
 #elif defined(VARIANT_HAS_PICKING)
-#if MATERIAL_FEATURE_LEVEL == 0
+#if FILAMENT_EFFECTIVE_VERSION == 100
     outPicking.a = mod(float(object_uniforms_objectId / 65536), 256.0) / 255.0;
     outPicking.b = mod(float(object_uniforms_objectId /   256), 256.0) / 255.0;
     outPicking.g = mod(float(object_uniforms_objectId)        , 256.0) / 255.0;
diff --git a/third_party/spirv-cross/spirv_glsl.cpp b/third_party/spirv-cross/spirv_glsl.cpp
index 0d63d35f8f2f..f57f702c620b 100644
--- a/third_party/spirv-cross/spirv_glsl.cpp
+++ b/third_party/spirv-cross/spirv_glsl.cpp
@@ -14977,7 +14977,11 @@ string CompilerGLSL::flags_to_qualifiers_glsl(const SPIRType &type, const Bitset
 	{
 		auto &execution = get_entry_point();
 
-		if (flags.get(DecorationRelaxedPrecision))
+		if (type.basetype == SPIRType::UInt && is_legacy()) {
+			// HACK: This is a bool. See comment in type_to_glsl().
+			qual += "lowp ";
+		}
+		else if (flags.get(DecorationRelaxedPrecision))
 		{
 			bool implied_fmediump = type.basetype == SPIRType::Float &&
 			                        options.fragment.default_float_precision == Options::Mediump &&
@@ -15503,7 +15507,11 @@ string CompilerGLSL::type_to_glsl(const SPIRType &type, uint32_t id)
 	if (type.basetype == SPIRType::UInt && is_legacy())
 	{
 		if (options.es)
-			SPIRV_CROSS_THROW("Unsigned integers are not supported on legacy ESSL.");
+			// HACK: spirv-cross changes bools into uints and generates code which compares them to
+			// zero. Input code will have already been validated as not to have contained any uints,
+			// so any remaining uints must in fact be bools. However, simply returning "bool" here
+			// will result in invalid code. Instead, return an int.
+			return backend.basic_int_type;
 		else
 			require_extension_internal("GL_EXT_gpu_shader4");
 	}