--- 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 추가 |