본문으로 건너뛰기
CHOI HONGSU
1 min read

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.

StepHypothesisVerification
1stApplying graphic options re-assigns URP Asset properties, triggering pipeline regenerationGuard against re-assigning the same value → monitor Kibana
2ndA URP Asset setter is called on a per-frame code pathAudit 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

  1. 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.

  2. 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 the shadowDistance setter every frame.

    Android likely shows a higher frequency because GraphicsDevice resets frequently (device resume / focus changes, etc.).

Validation

Side-effect verification matrix

ItemVerification
Where shadowDistance is usedMaterialGlobalPropertiesFeature.cs is the only place modified → no conflicts
ShadowVolume on/off equivalenceApplies on the first frame, then skips → behavior identical
When ShadowVolume value changesApplies only on the frame the value changed → behavior identical
Volume on→off transitionApplies 100 on the next frame → behavior identical
External writes overwrittenOverwritten on the next frame → behavior identical
Mathf.Approximately precisionAt 100f the epsilon is 0.0001 → ample for shadow distance
NaN inputApproximately(NaN, NaN) == false → setter called (safe fallback)
Effect on deterministic simulationVisual-only code → unrelated
Cost1 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.

Blitter 중복 초기화 에러 대응 · Cookie Run: Oven Smash · Choi Hongsu · Hongsu