npx skills add ...
npx skills add meta-quest/agentic-tools --skill hz-unity-code-review
Reviews Unity code targeting Meta VR and Horizon OS for performance issues, rendering best practices, and common VR pitfalls. Use during code review or when diagnosing Meta VR performance problems in Unity projects.
npx skills add meta-quest/agentic-tools --skill hz-unity-code-review
Use this skill when reviewing Unity C# code or project settings that target Meta VR headsets. This includes:
Meta VR applications must use the Universal Render Pipeline (URP) with specific settings optimized for mobile VR. The Built-in Render Pipeline is not recommended for new Meta VR projects.
Critical settings to verify:
Meta VR has draw call budgets that vary by workload complexity. Every draw call has CPU overhead that directly impacts frame timing.
| Metric | Quest 2 / Quest Pro | Quest 3 / Quest 3S | Meta VR Glasses |
|---|---|---|---|
| Draw calls (busy simulation) | 80-200 | 200-300 | 200-300 |
| Draw calls (medium simulation) | 200-300 | 400-600 | 400-600 |
| Draw calls (light simulation) | 400-600 | 700-1000 | 700-1000 |
| Triangles per frame | 750K-1M | 1M-2M | 1M-2M |
| SetPass calls | < 50 | < 80 | < 80 |
Enable and verify:
Mobile GPUs on Meta VR cannot handle desktop-class shaders. Review all materials for:
GC allocations cause frame hitches and must be eliminated from hot paths.
Meta VR supports multiple input modalities. Code should handle:
OVRInput is maintained for legacy supportOVRHand and OVRSkeleton for hand pose dataOVREyeGaze (Quest Pro / Meta VR Glasses, requires permission)Physics simulation is expensive on mobile. Review for:
Audio is often overlooked but can impact performance:
| Area | Target | Notes |
|---|---|---|
| Draw calls | 80-200 (busy) to 400-600 (light) | Use batching, instancing, atlasing |
| Triangles | 750K-1M/frame | Use LODs, occlusion culling |
| Texture resolution | Max 2K, 4K sparingly | ASTC compression required |
| Shader | URP mobile shaders | No Standard shader, no screen-space effects |
| Rendering mode | Single-pass multiview | Must be enabled in XR settings |
| FFR | Enabled (High or HighTop) | Fixed foveated rendering reduces edge fragment cost |
| MSAA | 4x quality / 2x perf | Free on tile-based GPU when configured correctly |
| Target frame rate | 72 Hz minimum | 90 Hz recommended, 120 Hz for smooth experiences |
| GC allocations | 0 B/frame in steady state | No allocations in Update/LateUpdate/FixedUpdate |
| Audio sources | < 16 simultaneous | Use pooling for audio sources |
You can use the metavr tool to validate builds and check device-side behavior. Invoke via metavr <args> (published as the npm package metavr; if metavr is not on PATH, run npx -y metavr <args>) — no install required.
Use device-side profiling to validate that code review findings translate to real performance improvements.
For detailed guidance on specific topics, see the following reference documents:
// Flag these patterns in code review:
Camera.main // Calls FindWithTag internally
GameObject.Find("name") // Linear search every call
GetComponent<T>() in Update // Cache the result
new List<T>() in Update // Allocates on heap
string + string in Update // Creates new string objects
foreach on non-List collections // Enumerator allocation
LINQ queries (.Where, .Select) // Multiple allocations
Boxing (int -> object) // Heap allocation
delegate/lambda in hot paths // Closure allocation// BAD: Empty Update still has overhead
void Update() { }
// BAD: Logic that doesn't need per-frame execution
void Update() {
SavePlayerPrefs(); // Should be event-driven
}
// GOOD: Use events, coroutines, or InvokeRepeating for non-per-frame logic
void OnScoreChanged(int newScore) {
UpdateScoreUI(newScore);
}// BAD: Camera.main uses FindWithTag internally
void Update() {
transform.LookAt(Camera.main.transform);
}
// GOOD: Cache the reference
private Transform _cameraTransform;
void Start() {
_cameraTransform = Camera.main.transform;
}
void Update() {
transform.LookAt(_cameraTransform);
}// BAD: Expensive search every frame
void Update() {
var player = GameObject.FindWithTag("Player");
var rb = player.GetComponent<Rigidbody>();
}
// GOOD: Cache in Awake/Start or use dependency injection
private Rigidbody _playerRb;
void Awake() {
_playerRb = GameObject.FindWithTag("Player").GetComponent<Rigidbody>();
}# Check connected Meta VR device
metavr device list
# Install and run a build
metavr app install path/to/build.apk
metavr app launch com.company.app
# Check device logs for errors
metavr adb logcat --tag Unity
# Monitor GPU performance
metavr perf capture