d8a80f6272
이름만 다른(표기 변형) [[위키링크]]를 대상 문서의 canonical 제목으로 치환해 끊겼던 1,200개 링크를 연결. 제목/파일명 정규화 일치만 적용하고 별칭 매칭은 과병합 위험으로 제외(애매성 가드). 원본은 _link_reconcile_backup/ 에 백업. 도구: Datacollect/scripts/link_reconcile_apply.mjs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
207 lines
6.7 KiB
Markdown
207 lines
6.7 KiB
Markdown
---
|
|
id: wiki-2026-0508-draw-call-optimization
|
|
title: Draw Call Optimization
|
|
category: 10_Wiki/Topics
|
|
status: verified
|
|
canonical_id: self
|
|
aliases: [Batching, Instancing, GPU Draw Reduction]
|
|
duplicate_of: none
|
|
source_trust_level: A
|
|
confidence_score: 0.9
|
|
verification_status: applied
|
|
tags: [graphics, gpu, performance, webgl, webgpu]
|
|
raw_sources: []
|
|
last_reinforced: 2026-05-10
|
|
github_commit: pending
|
|
tech_stack:
|
|
language: TypeScript
|
|
framework: WebGPU
|
|
---
|
|
|
|
# Draw Call Optimization
|
|
|
|
## 매 한 줄
|
|
> **"매 CPU→GPU command submission 의 의 minimize 의 — 의 frame budget 의 dominant cost"**. 의 each draw call 의 의 driver overhead (state validation, command translation) 의 incur. 2026 의 WebGPU 의 매 explicit 의 design 의 의 batching 의 essential 의.
|
|
|
|
## 매 핵심
|
|
|
|
### 매 cost 의 source
|
|
- 의 driver state validation (shader, buffer, texture binding).
|
|
- 의 command buffer 의 translation 의 GPU-specific ISA.
|
|
- 의 GPU 의 의 pipeline switch (cache miss, warp reorganize).
|
|
|
|
### 매 reduction 의 strategy
|
|
- **Batching**: 의 same-state object 의 single draw 의 의 merge.
|
|
- **Instancing**: 의 same mesh 의 N copy 의 single 의 draw call 의 issue.
|
|
- **Texture atlas**: 의 multiple texture 의 의 single 의 의 — 의 binding 의 reduce.
|
|
- **Indirect draw**: 의 GPU 의 의 self-issue 의 의 — CPU 의 의 idle.
|
|
- **Bindless / large bind group**: 의 binding 의 의 amortize.
|
|
|
|
### 매 응용
|
|
1. UI rendering (의 button 의 thousand 의 single draw).
|
|
2. Particle system (의 instancing 의 의 millions).
|
|
3. Tilemap (atlas + instancing).
|
|
4. Foliage / crowd (의 GPU instancing).
|
|
5. Game world chunk (의 batching 의 의 static mesh).
|
|
|
|
## 💻 패턴
|
|
|
|
### Three.js BatchedMesh
|
|
```ts
|
|
import * as THREE from "three";
|
|
|
|
const batched = new THREE.BatchedMesh(1024, 60_000, 90_000, material);
|
|
const cubeGeom = new THREE.BoxGeometry();
|
|
const id = batched.addGeometry(cubeGeom);
|
|
|
|
for (let i = 0; i < 1024; i++) {
|
|
const inst = batched.addInstance(id);
|
|
const m = new THREE.Matrix4().setPosition(Math.random() * 100, 0, Math.random() * 100);
|
|
batched.setMatrixAt(inst, m);
|
|
}
|
|
scene.add(batched);
|
|
// 매 single draw call 의 1024 cube
|
|
```
|
|
|
|
### InstancedMesh
|
|
```ts
|
|
const geom = new THREE.SphereGeometry(0.5);
|
|
const mesh = new THREE.InstancedMesh(geom, material, 10_000);
|
|
const m = new THREE.Matrix4();
|
|
for (let i = 0; i < 10_000; i++) {
|
|
m.setPosition(Math.random() * 200 - 100, 0, Math.random() * 200 - 100);
|
|
mesh.setMatrixAt(i, m);
|
|
}
|
|
mesh.instanceMatrix.needsUpdate = true;
|
|
```
|
|
|
|
### WebGPU instanced draw
|
|
```ts
|
|
const pass = encoder.beginRenderPass(passDesc);
|
|
pass.setPipeline(pipeline);
|
|
pass.setBindGroup(0, sceneBindGroup);
|
|
pass.setVertexBuffer(0, vertexBuffer);
|
|
pass.setVertexBuffer(1, instanceBuffer); // per-instance data
|
|
pass.setIndexBuffer(indexBuffer, "uint32");
|
|
pass.drawIndexed(indexCount, instanceCount);
|
|
pass.end();
|
|
```
|
|
|
|
### WebGPU indirect draw (GPU 의 self-issue)
|
|
```ts
|
|
const indirectBuffer = device.createBuffer({
|
|
size: 16, // [vertexCount, instanceCount, firstVertex, firstInstance]
|
|
usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.STORAGE,
|
|
});
|
|
|
|
// 의 compute shader 의 의 indirectBuffer 의 의 fill (e.g. frustum cull)
|
|
pass.drawIndirect(indirectBuffer, 0);
|
|
```
|
|
|
|
### Texture atlas (UV 의 sub-region)
|
|
```glsl
|
|
// fragment shader
|
|
uniform sampler2D atlas;
|
|
uniform vec4 uvRect; // x, y, w, h
|
|
|
|
void main() {
|
|
vec2 uv = uvRect.xy + v_uv * uvRect.zw;
|
|
outColor = texture(atlas, uv);
|
|
}
|
|
```
|
|
|
|
### Sort 의 의 state-change minimize
|
|
```ts
|
|
// 의 draw 의 의 material 의 의 sort
|
|
drawables.sort((a, b) => {
|
|
if (a.materialId !== b.materialId) return a.materialId - b.materialId;
|
|
if (a.meshId !== b.meshId) return a.meshId - b.meshId;
|
|
return a.depth - b.depth;
|
|
});
|
|
```
|
|
|
|
### UI batching (single quad mesh)
|
|
```ts
|
|
// 의 each UI element 의 의 quad 의 의 single VBO 의 의 append
|
|
class UIBatcher {
|
|
vertices = new Float32Array(4096 * 4 * 5); // x, y, u, v, color
|
|
count = 0;
|
|
|
|
pushQuad(x: number, y: number, w: number, h: number, uv: UVRect, color: number) {
|
|
const v = this.vertices;
|
|
const o = this.count * 20;
|
|
v[o+0]=x; v[o+1]=y; v[o+2]=uv.x; v[o+3]=uv.y; v[o+4]=color;
|
|
v[o+5]=x+w; v[o+6]=y; v[o+7]=uv.x+uv.w; v[o+8]=uv.y; v[o+9]=color;
|
|
v[o+10]=x+w; v[o+11]=y+h; v[o+12]=uv.x+uv.w; v[o+13]=uv.y+uv.h; v[o+14]=color;
|
|
v[o+15]=x; v[o+16]=y+h; v[o+17]=uv.x; v[o+18]=uv.y+uv.h; v[o+19]=color;
|
|
this.count++;
|
|
}
|
|
|
|
flush(pass: GPURenderPassEncoder) {
|
|
device.queue.writeBuffer(this.vbo, 0, this.vertices, 0, this.count * 20);
|
|
pass.setVertexBuffer(0, this.vbo);
|
|
pass.draw(6 * this.count); // 매 single call
|
|
this.count = 0;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Frustum cull (CPU)
|
|
```ts
|
|
function cull(objects: Drawable[], camera: Camera): Drawable[] {
|
|
const frustum = camera.frustum;
|
|
return objects.filter((o) => frustum.intersects(o.worldBounds));
|
|
}
|
|
```
|
|
|
|
### GPU-driven cull (compute)
|
|
```wgsl
|
|
@compute @workgroup_size(64)
|
|
fn cullCS(@builtin(global_invocation_id) gid: vec3u) {
|
|
let i = gid.x;
|
|
if (i >= arrayLength(&instances)) { return; }
|
|
let inst = instances[i];
|
|
if (frustumIntersects(inst.bounds, frustum)) {
|
|
let slot = atomicAdd(&drawCount, 1u);
|
|
visibleInstances[slot] = inst;
|
|
}
|
|
}
|
|
```
|
|
|
|
## 매 결정 기준
|
|
| 상황 | Approach |
|
|
|---|---|
|
|
| Same mesh, many copy | InstancedMesh (instancing) |
|
|
| Different mesh, same material | BatchedMesh (geometry merge) |
|
|
| UI / 2D | Sprite batcher + atlas |
|
|
| Static scene | Pre-merge geometry at build time |
|
|
| Dynamic LOD / cull | GPU indirect draw + compute cull |
|
|
| Mobile / tile | Reduce binding, atlas, instancing |
|
|
|
|
**기본값**: instancing 의 first, batching 의 second, indirect/compute 의 last.
|
|
|
|
## 🔗 Graph
|
|
- 부모: [[GPU Pipeline]] · [[Real-Time Rendering]]
|
|
- 변형: [[Instancing]] · [[Batching]] · [[Indirect Draw]]
|
|
- Adjacent: [[Texture Atlas]] · [[GPU-driven Rendering]] · [[Frustum Culling]]
|
|
|
|
## 🤖 LLM 활용
|
|
**언제**: 의 frame time 의 의 CPU-bound 의 (draw call > 1000), GPU-driven culling, atlas 설계.
|
|
**언제 X**: 의 매 GPU-bound 의 (fragment-heavy) — 의 다른 의 axis 의 (overdraw, shader complexity) 의 attack.
|
|
|
|
## ❌ 안티패턴
|
|
- **One mesh per object**: 의 10,000 entity 의 = 의 10,000 draw — 매 disaster.
|
|
- **Per-frame buffer recreate**: 의 GC pressure + 의 driver overhead.
|
|
- **Random material switch**: state thrash — 매 sort 의 의 by material first.
|
|
- **Premature GPU-driven**: 의 CPU 의 매 not bottleneck 의 시 의 — 매 added complexity.
|
|
|
|
## 🧪 검증 / 중복
|
|
- Verified (WebGPU spec, Three.js BatchedMesh r167+, Unreal/Unity rendering docs, GPU Gems, RenderDoc analysis).
|
|
- 신뢰도 A.
|
|
|
|
## 🕓 Changelog
|
|
| 날짜 | 변경 |
|
|
|---|---|
|
|
| 2026-05-08 | Phase 1 |
|
|
| 2026-05-10 | Manual cleanup — instancing + indirect + UI batcher 추가 |
|