mobile-shader-optimization — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited mobile-shader-optimization (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Use this skill when the user is writing, reviewing, or optimizing Unity shaders for mobile platforms (Android, iOS) or any GPU-constrained environment (Meta Quest VR, low-end devices). Triggers include: mentions of "mobile", "Android", "iOS", "Mali", "Adreno", "PowerVR", "performance", "optimization", "fillrate", "overdraw", "bandwidth", "frame budget", "half precision", "LOD", "baked lighting", or any shader work targeting sub-60fps scenarios. Also trigger when the user is profiling shaders, asking about GPU architecture, or comparing Simple Lit vs Complex Lit in URP.
These rules must always be followed when writing mobile shaders:
float for world-space positions, UV coordinates requiring high precision, and depth calculations. On Mali and Adreno GPUs, half triggers 16-bit fast paths that are 2x faster.for loops are allowed. Even on ES 3.0+, avoid dynamic loop bounds — they prevent the compiler from unrolling.step(), lerp(), saturate() for branchless alternatives.float3 result = scalar1 * scalar2 * vector is faster than float3 result = vector * scalar1 * scalar2 because the first scalar multiply is 1 ALU op instead of 3.texture-packing-variant-stripping skill).Understanding tile-based rendering is essential for mobile shader optimization.
Mobile GPUs split the screen into small tiles (16×16 for Mali, variable for Adreno) and process each tile entirely on-chip before writing to main memory. This means:
half hardware.half is genuinely 16-bit and 2x throughput. float is 32-bit. No fixed type.// HLSL/ShaderLab precision mapping to GLSL:
// float → highp (32-bit) — positions, UVs, depth
// half → mediump (16-bit) — colors, normals, most calculations
// fixed → lowp (11-bit) — simple color ops only (deprecated on many GPUs)
// USE half FOR:
half4 color = tex2D(_MainTex, uv); // Texture samples
half3 normal = normalize(i.worldNormal); // Normals
half NdotL = saturate(dot(normal, lightDir)); // Lighting
half fresnel = pow(1.0h - NdotL, 4.0h); // Fresnel
// USE float FOR:
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // World position
float2 uv = TRANSFORM_TEX(v.texcoord, _MainTex); // UV transforms
float depth = Linear01Depth(rawDepth); // DepthOn fillrate-bound mobile GPUs, vertex count is orders of magnitude lower than fragment count. Move calculations to the vertex shader whenever visually acceptable:
// BAD — per-pixel calculation (runs millions of times)
half4 frag(v2f i) : SV_Target {
half rim = 1.0h - saturate(dot(normalize(i.viewDir), normalize(i.worldNormal)));
half rimPower = pow(rim, _RimPower);
return _RimColor * rimPower;
}
// GOOD — per-vertex calculation (runs thousands of times)
v2f vert(appdata v) {
v2f o;
o.vertex = TransformObjectToHClip(v.vertex.xyz);
float3 viewDir = normalize(GetWorldSpaceViewDir(TransformObjectToWorld(v.vertex.xyz)));
float3 worldNormal = TransformObjectToWorldNormal(v.normal);
o.rimFactor = pow(1.0 - saturate(dot(viewDir, worldNormal)), _RimPower);
return o;
}
half4 frag(v2f i) : SV_Target {
return _RimColor * i.rimFactor; // Just interpolated value
}Use LOD tiers to serve different shader complexity per device capability:
Shader "Game/Character" {
// LOD 300: Full quality (desktop, high-end mobile)
SubShader {
LOD 300
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
Pass {
// Normal map + specular + rim + emission
}
}
// LOD 150: Medium quality (mid-range mobile)
SubShader {
LOD 150
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
Pass {
// Diffuse + simple lighting, no normal map
}
}
// LOD 100: Minimum quality (low-end mobile)
SubShader {
LOD 100
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
Pass {
// Vertex-lit only, single texture
}
}
Fallback "Universal Render Pipeline/Simple Lit"
}Set at runtime based on device:
// In a device detection script:
if (SystemInfo.graphicsMemorySize < 1024)
Shader.globalMaximumLOD = 100;
else if (SystemInfo.graphicsMemorySize < 2048)
Shader.globalMaximumLOD = 150;
else
Shader.globalMaximumLOD = 300;Built-in LOD values: Mobile/VertexLit = 100, Mobile/Diffuse = 150, Mobile/Bumped = 200.
Fully baked lighting eliminates real-time shadow maps and reduces fragment shaders to a single lightmap fetch. This is often the difference between 30 and 60 FPS on low-end devices.
Configure the URP Asset for mobile:
mobile-post-processing skill).When reviewing a mobile shader, verify:
half precisionfloatdiscard/clip on iOS/PowerVR targets (or isolated in separate pass)while/do-while loopsnoforwardadd used on surface shaders (or additional lights disabled in URP)~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.