---
title: "5,330만 삼각형이 118 드로우 콜로 접힌다"
description: "최적화가 잘 되면 프로파일에서 시뮬레이션이 사라집니다. 그리고 병목이 렌더로 옮겨갑니다. Entities Graphics 와 GPU Resident Drawer 는 같은 뿌리입니다."
author: "유니얼"
pubDate: 2026-09-09
category: "game-dev/unity/dots"
tags: ["Unity", "DOTS", "최적화", "렌더링"]
canonical: https://blog.unialgames.com/posts/dots-brg-and-grd/
---
<x-repo>
<script type="application/json">
{
  "repo": "SeoBYP/ecs-warriors",
  "text": "적 1만 마리를 돌리는 3D 무쌍 액션. 이 글의 프로파일 수치는 여기 벤치 하니스에서 나왔습니다.",
  "chips": ["Unity 6000.5.4f1", "Entities 6.5.0", "Burst 1.8.29", "Collections 6.5.0"],
  "links": [
    { "text": "week5-stages.csv (측정 원본)", "path": "docs/benchmarks/week5-stages.csv" }
  ],
  "note": "벤치 하니스가 ProfilerRecorder 로 CPU 메인 스레드, GPU, 시스템별 마커, 드로우 콜을 한꺼번에 누적합니다."
}
</script>
</x-repo>

프레임타임 전체만 재면 "빨라졌다" 는 알아도 **어디가 빨라졌는지**는 모릅니다.
그래서 벤치 하니스에 `ProfilerRecorder` 를 붙여서 CPU 메인 스레드, GPU,
시스템별 마커, 드로우 콜을 같이 누적하게 만들었습니다.

## 시뮬레이션이 프로파일에서 사라졌다

| 적 수 | avg_ms | main_ms | SpatialHash_ms | Movement_ms | brg_draws | 삼각형 |
| --- | --- | --- | --- | --- | --- | --- |
| 1,000 | 6.45 | 2.54 | 0.023 | 0.014 | 22 | 0.72M |
| 2,500 | 6.37 | 2.48 | 0.023 | 0.014 | 44 | 1.42M |
| 5,000 | 6.80 | 2.74 | 0.023 | 0.014 | 69 | 2.59M |
| 7,500 | 6.56 | 2.64 | 0.023 | 0.014 | 94 | 3.89M |
| **10,000** | **6.77** | **2.92** | **0.023** | **0.014** | **118** | **5.33M** |

읽을 것이 셋 있습니다.

**하나. 시스템 마커가 바닥에 붙어 N 에 안 비례합니다.**
공간 해시 0.023ms, 이동 0.014ms 로 적이 열 배가 돼도 고정입니다.
그리드와 Burst 를 쓰면 실연산이 나노초 수준이라, 이 마커는 사실상
**잡을 띄우는 고정 오버헤드**를 재고 있습니다.

지루한 표처럼 보이는데 그게 결과입니다.
**최적화가 너무 잘 돼서 시뮬레이션이 프로파일에서 사라졌습니다.**

**둘. `main_ms` 가 전체의 절반이 안 됩니다.** 2.9ms 대 6.77ms 입니다.
나머지 3.8ms 는 렌더와 GPU 대기입니다.
**이제 병목은 시뮬레이션이 아니라 렌더 파이프라인입니다.**

**셋. `brg_draws` 만 선형으로 늡니다.** 22에서 118 입니다.
그리고 그 118개가 **삼각형 5,330만 개**를 그립니다.

## 그 118개가 무엇인가

Entities Graphics 는 `BatchRendererGroup` 위에 얹혀 있습니다.
같은 메시와 머티리얼을 쓰는 엔티티를 배치로 묶고, 인스턴스별 데이터는
GPU 버퍼에 올려 두고, 드로우 콜 하나로 수천 개를 그립니다.

`MeshRenderer` 1만 개였다면 최소 1만 번의 렌더러 순회와 컬링이 CPU 에서 일어납니다.
BRG 는 그 목록을 GPU 쪽 버퍼로 들고 있으므로 CPU 가 할 일이 그만큼 없습니다.

**드로우 콜이 118로 접히는 것보다, 그 118을 만드는 데 CPU 를 거의 안 쓴다는 쪽이 큽니다.**

## GPU Resident Drawer 는 같은 뿌리다

Unity 6 의 URP 에는 GPU Resident Drawer 가 있습니다.
**일반 GameObject 를 같은 BRG 경로로 그려 주는 기능** 입니다.
ECS 를 안 써도 BRG 의 이득을 가져올 수 있게 만든 것입니다.

다만 그냥 켜면 되는 물건이 아닙니다.

- **CPU 를 벌고 GPU 를 조금 더 씁니다.** 이미 GPU 바운드면 프레임이 오히려 늘 수 있습니다.
  저사양 모바일과 VR 에서 이 경향이 강합니다
- **합쳐 그리는 만큼 오버드로우가 늘어납니다.** 문서가 Forward+ 에서
  Depth Priming Mode 를 Auto 나 Forced 로 두라고 권합니다
- 호환 조건이 있습니다. 정적 배칭을 끄고, Fixed Lightmap Size 를 켜고,
  라이팅의 Mipmap Limits 를 끄는 식입니다

그리고 **에디터보다 빌드에서 이득이 큽니다.** 씬 뷰와 게임 뷰는 조건이 달라서,
에디터에서 재고 "효과 없다" 고 판단하면 틀릴 수 있습니다.

## 프로파일러 카운터에서 두 번 데였다

이 표를 만들면서 걸린 것 둘입니다. 둘 다 **가짜 숫자가 나왔습니다.**

**하나. 카운터 이름은 문서가 아니라 런타임에서 확인해야 합니다.**
`ProfilerCategory` 상수만으로는 원하는 카운터를 못 잡았습니다.
`ProfilerRecorderHandle.GetAvailable()` 로 **8,062개를 전부 훑어서**
`BRG Draw Calls Count`, `CPU Main Thread Frame Time`,
그리고 각 시스템의 `Default World Simulation.Systems.<Name>` 마커를 찾았습니다.

같은 시스템 마커가 `Scripts` 와 `Burst Jobs` 두 카테고리에 걸쳐 있어서 둘 다 합산했습니다.
Burst 를 껐다 켜도 총비용이 일관되게 잡히게 하려는 것입니다.

**둘. 병렬 잡 마커는 `SumAllSamplesInFrame` 이 없으면 고정값이 나옵니다.**

처음에 `MovementSystem` 이 2,000마리든 10,000마리든 **똑같이 0.008ms** 였습니다.
당연히 이상합니다.

병렬 잡은 워커 스레드마다 마커가 여러 번 발생하는데,
`ProfilerRecorder` 기본값은 그중 **하나만** 잡습니다.
워커 한 명분을 보고 있었던 것입니다.

```csharp
ProfilerRecorder.StartNew(category, name, capacity,
    ProfilerRecorderOptions.SumAllSamplesInFrame);
```

이 옵션을 켜야 프레임 안의 전 샘플이 합산돼서 시스템 총비용이 나옵니다.

> **N 에 안 비례하는 숫자가 나오면 최적화를 의심하기 전에 계측을 의심해야 합니다.**
> 위의 표에서 마커가 고정인 것은 진짜였지만, 처음 본 고정값은 가짜였습니다.
> 둘을 구분하려면 계측 자체를 한 번 검증해야 했습니다.

## `gpu_ms` 는 에디터에서 0이다

표의 GPU 열이 전부 0에 가깝습니다. 정상입니다.
에디터에서는 GPU 프레임 타임 카운터가 제대로 안 잡힙니다.

**GPU 실측은 빌드에서 따로 잡아야 합니다.**
그래서 "병목이 렌더로 옮겨갔다" 까지는 `main_ms` 와 total 의 차이로 말할 수 있지만,
그 안에서 무엇이 비싼지는 이 데이터로 말할 수 없습니다.