Mitigating the Blitter duplicate-initialization error

Traced a Blitter duplicate-initialization error firing 137,080 times/day on Android down to its root cause via two-stage hypothesis verification.
Problem
Overview
A large volume of URP-related error logs was collected on Android, prompting root-cause estimation and a defensive code change.
From the stack, this surfaces as Blitter.Initialize() being called repeatedly during UniversalRenderPipeline construction.
- Reports collected: AOS 137,080
- Environment: mostly Android
- Note: a similar issue under GLES is listed on the Unity Issue Tracker
Approach
Two hypothesis-verification cycles — log reduction measured at each step.
| Step | Hypothesis | Verification |
|---|---|---|
| 1st | Applying graphic options re-assigns URP Asset properties, triggering pipeline regeneration | Guard against re-assigning the same value → monitor Kibana |
| 2nd | A URP Asset setter is called on a per-frame code path | Audit every setter call site in the codebase |
Rather than nailing down the root cause in one shot, the approach was add a guard per hypothesis and measure log change — narrowing down incrementally. The most realistic strategy when reproduction isn't possible.
Implementation
- 01
1st fix — GraphicOptionAssets.ApplyURPSettings() guard
Blocks URP Asset properties from being re-assigned to the same value when options are applied.
Result: 137,080 → 2,978 (−98%) — effective, but residual errors remained.
- 02
2nd fix — tracing setters on per-frame call paths
To find the cause of the remaining 2,978, all URP Asset setter call sites in the codebase were audited. The culprit:
MaterialGlobalPropertiesFeature.AddRenderPasses()was calling theshadowDistancesetter every frame.Android likely shows a higher frequency because GraphicsDevice resets frequently (device resume / focus changes, etc.).
Validation
Side-effect verification matrix
| Item | Verification |
|---|---|
Where shadowDistance is used | MaterialGlobalPropertiesFeature.cs is the only place modified → no conflicts |
| ShadowVolume on/off equivalence | Applies on the first frame, then skips → behavior identical |
| When ShadowVolume value changes | Applies only on the frame the value changed → behavior identical |
| Volume on→off transition | Applies 100 on the next frame → behavior identical |
| External writes overwritten | Overwritten on the next frame → behavior identical |
Mathf.Approximately precision | At 100f the epsilon is 0.0001 → ample for shadow distance |
| NaN input | Approximately(NaN, NaN) == false → setter called (safe fallback) |
| Effect on deterministic simulation | Visual-only code → unrelated |
| Cost | 1 getter + 1 compare; negligible vs. setter cost |
Monitoring metric: tracked Kibana globalManagerException messages converging to 0/hour.
Device validation: Vivo Y11, Galaxy A12, S9, S21 — including graphic option changes → no side effects.
Note — 향후 가이드라인
Conclusion
A case study of tracing a production-only issue that couldn't be reproduced locally — using hypothesis-based incremental fixes plus log monitoring. The first fix quickly validated a visible hypothesis and yielded a 98% drop; the residual logs became the clue for the second-stage root-cause trace. The behavior "URP internal property setters trigger the dirty flag" became clear through this investigation. Because the same pattern can hide in other RendererFeatures / runtime code, a guideline was left behind to prevent recurrence.