์นดํ…Œ๊ณ ๋ฆฌ ์—†์Œ

Unity Compute Shader ๊ธฐ๋ฐ˜ ๋Œ€๊ทœ๋ชจ ์—ฐ์‚ฐ์˜ GPU-CPU ๋™๊ธฐํ™” ๋ณ‘๋ชฉ ํ•ด๊ฒฐ: AsyncGPUReadback ์ตœ์ ํ™” ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๊ฒŒ์ž„๊ต์ˆ˜ 2026. 9. 1. 22:01
๋ฐ˜์‘ํ˜•

๐ŸŒ English Abstract

This article provides an in-depth technical guide on resolving GPU-CPU synchronization stalls when handling large-scale parallel computations using Unity Compute Shaders. It focuses on replacing blocking ComputeBuffer.GetData calls with AsyncGPUReadback to eliminate CPU thread idling and frame drops. Key topics include zero-allocation data retrieval with NativeArray, handling multi-frame latency edge cases, and cross-platform graphics API optimization strategies.

์œ ๋‹ˆํ‹ฐ(Unity 3D) ๊ฐœ๋ฐœ ๊ณผ์ •์—์„œ ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌโ€”์˜ˆ๋ฅผ ๋“ค์–ด ์ˆ˜๋งŒ ๊ฐœ์˜ ํŒŒํ‹ฐํด ๋ฌผ๋ฆฌ ์—ฐ์‚ฐ, ๋ณต์žกํ•œ ์ง€ํ˜• ์ƒ์„ฑ(Terrain Generation), ํŒŒํŽธํ™” ๋“œ๋ก  AI(Boids Algorithm), ๋˜๋Š” GPU ๊ธฐ๋ฐ˜ ๋ž˜๊ทธ๋Œ ๋ฌผ๋ฆฌ ๋“ฑโ€”๋ฅผ ์ˆ˜ํ–‰ํ•  ๋•Œ ์ปดํ“จํŠธ ์…ฐ์ด๋”(Compute Shader)๋Š” ๊ฐ•๋ ฅํ•œ ์„ฑ๋Šฅ์„ ๋ฐœํœ˜ํ•ฉ๋‹ˆ๋‹ค. CPU๊ฐ€ ์ˆ˜์‹ญ ๋ฐ€๋ฆฌ์ดˆ ์ด์ƒ ์†Œ์š”ํ•  ์—ฐ์‚ฐ์„ GPU์˜ ์ˆ˜์ฒœ ๊ฐœ ์ฝ”์–ด๋กœ ์ด์‚ฐํ™”ํ•˜์—ฌ ๋‹จ ๋ช‡ ๋ฐ€๋ฆฌ์ดˆ ๋งŒ์— ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ปดํ“จํŠธ ์…ฐ์ด๋”๋กœ ์—ฐ์‚ฐ๋œ ๊ฒฐ๊ณผ๋ฅผ ๋‹ค์‹œ CPU ๋ฉ”์ธ ์Šค๋ ˆ๋“œ(Main Thread)๋กœ ๊ฐ€์ ธ์™€ ๊ฒŒ์ž„ ๋กœ์ง์— ๋ฐ˜์˜ํ•˜๋Š” ๊ณผ์ •์—์„œ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์‹ฌ๊ฐํ•œ ํ”„๋ ˆ์ž„ ๋“œ๋ž(Frame Drop)๊ณผ ๋ฉˆ์ถค ํ˜„์ƒ(Stall)์„ ๊ฒช๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋ณธ ์•„ํ‹ฐํด์—์„œ๋Š” ์ด๋Ÿฌํ•œ GPU-CPU ๋™๊ธฐํ™” ๋Œ€๊ธฐ ์‹œ๊ฐ„(Synchronization Latency)์˜ ์›์ธ์„ ์‹ฌ์ธต ๋ถ„์„ํ•˜๊ณ , ์œ ๋‹ˆํ‹ฐ์˜ AsyncGPUReadback API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์„ฑ๋Šฅ ๋ณ‘๋ชฉ์„ ์™„๋ฒฝํžˆ ํ•ด๊ฒฐํ•˜๋Š” ์ตœ์ ํ™” ๊ธฐ๋ฒ• ๋ฐ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๊ฒฝํ—˜์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.



1. GPU-CPU ๋™๊ธฐํ™” ๋ณ‘๋ชฉ์˜ ๊ทผ๋ณธ์  ์›์ธ (Pipeline Stall)

GPU์™€ CPU๋Š” ๋ณธ์งˆ์ ์œผ๋กœ ๋น„๋™๊ธฐ์ (Asynchronous)์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. CPU๊ฐ€ ์ปค๋งจ๋“œ ๋ฒ„ํผ(Command Buffer)์— ๊ทธ๋ž˜ํ”ฝ ๋ฐ ์—ฐ์‚ฐ ๋ช…๋ น์„ ์ƒ์„ฑํ•˜์—ฌ ์ „์†กํ•˜๋ฉด, GPU๋Š” ์ž์‹ ์˜ ํŒŒ์ดํ”„๋ผ์ธ ์Šค์ผ€์ค„๋Ÿฌ์— ๋”ฐ๋ผ ํ•ด๋‹น ๋ช…๋ น๋“ค์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ CPU๋Š” GPU๊ฐ€ ์ž‘์—…์„ ๋งˆ์น  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ๋‹ค์Œ ํ”„๋ ˆ์ž„์˜ ์—ฐ์‚ฐ์„ ๊ณ„์† ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ์กด ๋ฐฉ์‹: ComputeBuffer.GetData()์˜ ์น˜๋ช…์  ๋‹จ์ 

๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ปดํ“จํŠธ ์…ฐ์ด๋” ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์•„์˜ฌ ๋•Œ ์ง๊ด€์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด ComputeBuffer.GetData() ๋™๊ธฐ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.

// ๋™๊ธฐ์‹ ๋ฆฌ๋“œ๋ฐฑ (Blocking Call)
ComputeBuffer computeBuffer = new ComputeBuffer(count, sizeof(float) * 4);
myComputeShader.Dispatch(kernelIndex, threadGroupsX, 1, 1);

// ์—ฐ์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›๊ธฐ ์œ„ํ•ด ๋™๊ธฐ ์š”์ฒญ
Vector4[] data = new Vector4[count];
computeBuffer.GetData(data); // <-- ์—ฌ๊ธฐ์„œ CPU ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ์ฆ‰์‹œ ์ฐจ๋‹จ(Stall)๋จ!

ComputeBuffer.GetData()๊ฐ€ ํ˜ธ์ถœ๋˜๋Š” ์ˆœ๊ฐ„ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‚ด๋ถ€ ๋™์ž‘์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค:

  • ๋™๊ธฐ์‹ ํ”Œ๋Ÿฌ์‹œ(Synchronous Flush): CPU๋Š” GPU์—๊ฒŒ ํŒŒ์ดํ”„๋ผ์ธ์— ์Œ“์ธ ๋ชจ๋“  ์—ฐ์‚ฐ(Compute Kernel ํฌํ•จ)์„ ์ฆ‰์‹œ ์™„๋ฃŒํ•˜๋ผ๊ณ  ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
  • ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ์ฐจ๋‹จ(Main Thread Blocking): GPU๊ฐ€ ํ•ด๋‹น ComputeBuffer์— ๋Œ€ํ•œ ์“ฐ๊ธฐ ์ž‘์—…์„ ๋ชจ๋‘ ๋๋‚ด๊ณ  VRAM์—์„œ System RAM์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณต์‚ฌํ•  ๋•Œ๊นŒ์ง€ CPU ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋Š” ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€ ๋ชปํ•˜๊ณ  ๋Œ€๊ธฐ(Idle/Stall) ์ƒํƒœ์— ๋น ์ง‘๋‹ˆ๋‹ค.
  • ๊ฒฐ๊ณผ: GPU ์—ฐ์‚ฐ ์ž์ฒด๋Š” 1ms ๋งŒ์— ๋๋‚ฌ๋”๋ผ๋„, CPU-GPU ๊ฐ„ ๋™๊ธฐํ™” ์›Œํฌํ”Œ๋กœ์šฐ ๋•Œ๋ฌธ์— 15ms~30ms ์ด์ƒ์˜ ํ”„๋ ˆ์ž„ ์ง€์—ฐ(Jank)์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

2. AsyncGPUReadback: ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•

์œ ๋‹ˆํ‹ฐ๋Š” ์ด๋Ÿฌํ•œ ๋™๊ธฐํ™” ์ง€์—ฐ์„ ๊ทน๋ณตํ•˜๊ธฐ ์œ„ํ•ด AsyncGPUReadback ํด๋ž˜์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด API๋Š” GPU VRAM์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฝ์–ด์™€ CPU ๋ฉ”๋ชจ๋ฆฌ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์ „์†กํ•ด ์ฃผ๋ฉฐ, ๊ทธ๋™์•ˆ CPU ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋Š” ๋ฉˆ์ถ”์ง€ ์•Š๊ณ  ๊ณ„์† ๋ Œ๋”๋ง ๋ฐ ๊ฒŒ์ž„ ๋กœ์ง์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

AsyncGPUReadback ์š”์ฒญ์˜ ๊ธฐ๋ณธ ๋ฉ”์ปค๋‹ˆ์ฆ˜

AsyncGPUReadback.Request()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด, ์œ ๋‹ˆํ‹ฐ ๊ทธ๋ž˜ํ”ฝ์Šค ๋ฐฑ์—”๋“œ๋Š” GPU ๋น„๋™๊ธฐ ๋ฆฌ๋“œ๋ฐฑ ํ(Queue)์— ์š”์ฒญ์„ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. GPU๊ฐ€ ์ปค๋งจ๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ์ค€๋น„ํ•˜๋ฉด ์ฝœ๋ฐฑ(Callback)์„ ํ˜ธ์ถœํ•˜๊ฑฐ๋‚˜, ํ”„๋ ˆ์ž„ ์—…๋ฐ์ดํŠธ ์‹œ์ ์— ์š”์ฒญ ์™„๋ฃŒ ์—ฌ๋ถ€๋ฅผ AsyncGPUReadbackRequest.done์œผ๋กœ ์•Œ๋ ค์ค๋‹ˆ๋‹ค.



3. ์‹ค๋ฌด ์ตœ์ ํ™” ๊ตฌํ˜„: Garbage Collection ์ œ๋กœ(Zero-Alloc) ์ฝ๊ธฐ

๋‹จ์ˆœํžˆ AsyncGPUReadback.Request๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋Š” ์™„๋ฒฝํ•œ ์„ฑ๋Šฅ์„ ํ™•๋ณดํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ฝœ๋ฐฑ ๋‚ด๋ถ€์—์„œ ๋งค ํ”„๋ ˆ์ž„ ๋ฐฐ์—ด์„ ์ƒˆ๋กญ๊ฒŒ ํ• ๋‹นํ•˜๊ฑฐ๋‚˜ managed memory๋ฅผ ์กฐ์ž‘ํ•  ๊ฒฝ์šฐ, ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰ํ„ฐ(Garbage Collector)๋กœ ์ธํ•œ ๋˜ ๋‹ค๋ฅธ ๋ณ‘๋ชฉ(GC Spike)์ด ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์•„๋ž˜๋Š” C# Job System ๋ฐ NativeArray<T>๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ GC ํ• ๋‹น์„ ์™„์ „ํžˆ ๋ฐฐ์ œํ•œ ๊ณ ์„ฑ๋Šฅ ๋น„๋™๊ธฐ Readback ํ…œํ”Œ๋ฆฟ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

using System.Collections.Generic;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Rendering;

public class HighPerformanceReadback : MonoBehaviour
{
    [SerializeField] private ComputeShader computeShader;
    
    private ComputeBuffer dataBuffer;
    private NativeArray<Vector4> resultNativeArray;
    private Queue<AsyncGPUReadbackRequest> requestQueue = new Queue<AsyncGPUReadbackRequest>();
    
    private const int DATA_COUNT = 100000; // 10๋งŒ ๊ฐœ ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ
    private const int MAX_PENDING_REQUESTS = 3; // ํŒŒ์ดํ”„๋ผ์ธ ํ ๊นŠ์ด ์ œ์–ด

    private void Awake()
    {
        // ComputeBuffer ์ดˆ๊ธฐํ™” (Stride: 16 byte)
        dataBuffer = new ComputeBuffer(DATA_COUNT, sizeof(float) * 4);
        
        // C# Unmanaged NativeArray ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น (Persistent ํž™ ์˜์—ญ)
        resultNativeArray = new NativeArray<Vector4>(DATA_COUNT, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
    }

    private void Update()
    {
        // 1. Compute Shader ์‹คํ–‰ Dispatch
        int kernel = computeShader.FindKernel("CSMain");
        computeShader.SetBuffer(kernel, "ResultBuffer", dataBuffer);
        computeShader.Dispatch(kernel, DATA_COUNT / 64, 1, 1);

        // 2. ๋น„๋™๊ธฐ Readback ์š”์ฒญ (ํ ๊นŠ์ด ์ œํ•œ์œผ๋กœ ๋ฐฑํ”„๋ ˆ์…” ๋ฐฉ์ง€)
        if (requestQueue.Count < MAX_PENDING_REQUESTS)
        {
            var request = AsyncGPUReadback.Request(dataBuffer);
            requestQueue.Enqueue(request);
        }

        // 3. ์™„๋ฃŒ๋œ Readback ์ฒ˜๋ฆฌ
        ProcessCompletedRequests();
    }

    private void ProcessCompletedRequests()
    {
        while (requestQueue.Count > 0)
        {
            var req = requestQueue.Peek();

            if (req.hasError)
            {
                Debug.LogError("AsyncGPUReadback ์—๋Ÿฌ ๋ฐœ์ƒ");
                requestQueue.Dequeue();
            }
            else if (req.done)
            {
                requestQueue.Dequeue();
                
                // GC ํ• ๋‹น ์—†์ด NativeArray ๋ฉ”๋ชจ๋ฆฌ๋กœ ์–•์€ ๋ณต์‚ฌ (Direct Copy)
                NativeArray<Vector4> data = req.GetData<Vector4>();
                NativeArray<Vector4>.Copy(data, resultNativeArray, DATA_COUNT);

                // CPU ์˜์—ญ์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ ํ™œ์šฉ ๊ฐ€๋Šฅ (Job System์œผ๋กœ ๋„˜๊ธธ ์ˆ˜ ์žˆ์Œ)
                UseDataOnCPU(resultNativeArray);
            }
            else
            {
                // ์•„์ง ์™„๋ฃŒ๋˜์ง€ ์•Š์€ ๊ฐ€์žฅ ์˜ค๋ž˜๋œ ์š”์ฒญ์ด ์กด์žฌํ•˜๋ฉด ํƒˆ์ถœ
                break;
            }
        }
    }

    private void UseDataOnCPU(NativeArray<Vector4> data)
    {
        // ์ถ”๊ฐ€์ ์ธ CPU ๋กœ์ง ์ˆ˜ํ–‰ (์˜ˆ: Job System ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ)
    }

    private void OnDestroy()
    {
        // ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด Unmanaged ์ž์› ํ•ด์ œ ํ•„์ˆ˜
        if (dataBuffer != null) dataBuffer.Release();
        if (resultNativeArray.IsCreated) resultNativeArray.Dispose();
    }
}

4. ์‹ค๋ฌด ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๋ฐ ์—ฃ์ง€ ์ผ€์ด์Šค (Edge Cases) ๋Œ€์‘

AsyncGPUReadback์„ ์‹ค์ œ ๋ผ์ด๋ธŒ ํ”„๋กœ์ ํŠธ์— ๋„์ž…ํ•  ๋•Œ๋Š” ์ด๋ก ์  ๋™์ž‘๊ณผ ๋‹ฌ๋ฆฌ ์—ฌ๋Ÿฌ ์—ฃ์ง€ ์ผ€์ด์Šค์™€ ํ”Œ๋žซํผ๋ณ„ ๋™์ž‘ ์ฐจ์ด๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ํ•ต์‹ฌ ์ตœ์ ํ™” ๊ธฐ๋ฒ•๊ณผ ๋ฌธ์ œ ํ•ด๊ฒฐ ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

A. N-ํ”„๋ ˆ์ž„ ๋ ˆ์ดํ„ด์‹œ(Latency)์™€ ๋™๊ธฐํ™” ๊ฒฉ์ฐจ ํ•ด์†Œ

AsyncGPUReadback์€ ๋น„๋™๊ธฐ๋กœ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— Request()๋ฅผ ๋ณด๋‚ธ ์ฆ‰์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„๋ณผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋ณดํ†ต 1~3ํ”„๋ ˆ์ž„ ๋’ค(Frame Latency)์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋„์ฐฉํ•ฉ๋‹ˆ๋‹ค.

  • ๋ฌธ์ œ: ์‹ค์‹œ๊ฐ„ ์ถฉ๋Œ ํŒ์ •(Collision)์ด๋‚˜ ๋ฌผ๋ฆฌ ์—ฐ์‚ฐ์ฒ˜๋Ÿผ ์ฆ‰๊ฐ์ ์ธ ๋ฐ˜ํ™˜ ๊ฐ’์ด ํ•„์š”ํ•œ ๊ณณ์— ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ํ”„๋ ˆ์ž„ ๊ฐ„ ๋ฐ์ดํ„ฐ ์‹œ์ฐจ๊ฐ€ ์ง€์—ฐ(Lag)์œผ๋กœ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ•ด๊ฒฐ์ฑ… (Double/Triple Buffering): ํ”„๋ ˆ์ž„ ์ง€์—ฐ์„ ์ธ์ง€ํ•˜๊ณ  ์˜ˆ์ธก(Prediction) ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์ ์šฉํ•˜๊ฑฐ๋‚˜, CPU ์ธก์—์„œ๋Š” 1-2 ํ”„๋ ˆ์ž„ ์ „์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณด๊ฐ„(Interpolation) ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋”๋ธ” ๋ฒ„ํผ๋ง(Double Buffering) ์•„ํ‚คํ…์ฒ˜๋ฅผ ์ ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

B. ๋ฐฑํ”„๋ ˆ์…”(Backpressure) ํ˜„์ƒ๊ณผ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€

๋งค ํ”„๋ ˆ์ž„ ๋งˆ๋‹ค AsyncGPUReadback.Request()๋ฅผ ๋ฌด๋ถ„๋ณ„ํ•˜๊ฒŒ ํ˜ธ์ถœํ•˜๊ณ , ํ”„๋ ˆ์ž„ ๋ ˆ์ดํŠธ๊ฐ€ ์ˆœ๊ฐ„์ ์œผ๋กœ ๋–จ์–ด์ง€๋ฉด GPU ์ฒ˜๋ฆฌ ์†๋„๊ฐ€ ์š”์ฒญ ์†๋„๋ฅผ ๋”ฐ๋ผ๊ฐ€์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

  • ์ด ๊ฒฝ์šฐ ๋น„๋™๊ธฐ ์š”์ฒญ ํ(Queue)๊ฐ€ ๋น„๋Œ€ํ•ด์ ธ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์ด ํญ์ฆํ•˜๊ณ , GPU์˜ ๋“œ๋ผ์ด๋ฒ„ ๋ง ๋ฒ„ํผ(Driver Ring Buffer)๊ฐ€ ๊ณ ๊ฐˆ๋˜์–ด ์‹œ์Šคํ…œ ์ „์ฒด๊ฐ€ ๋ฉˆ์ถ”๋Š” ๋ฐฑํ”„๋ ˆ์…” ํ˜„์ƒ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.
  • ํ•ด๊ฒฐ์ฑ…: ์ฝ”๋“œ ํ…œํ”Œ๋ฆฟ์— ๋ช…์‹œ๋œ MAX_PENDING_REQUESTS์™€ ๊ฐ™์ด ํ์— ๋Œ€๊ธฐ ์ค‘์ธ ๋ฏธ์™„๋ฃŒ ์š”์ฒญ ์ˆ˜๋ฅผ ์ œํ•œ(Throttling)ํ•˜์—ฌ ๋™์‹œ ๋Œ€๊ธฐ ์š”์ฒญ ์ˆ˜๋ฅผ ํ•ญ์ƒ ์ผ์ • ์ดํ•˜(๋ณดํ†ต 2~3๊ฐœ)๋กœ ๊ด€๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

C. ๊ทธ๋ž˜ํ”ฝ์Šค API ๋ณ„ ๋ฐฑ์—”๋“œ ๋™์ž‘ ์ฐจ์ด (Vulkan / Metal vs Direct3D)

ํ”Œ๋žซํผ์— ๋”ฐ๋ผ AsyncGPUReadback์˜ ๋“œ๋ผ์ด๋ฒ„ ๋ ˆ๋ฒจ ์ง€์› ๋ฐฉ์‹์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

  • Direct3D 11 / Direct3D 12: ์•„์ฃผ ์•ˆ์ •์ ์œผ๋กœ ๋น„๋™๊ธฐ ์Šคํ…Œ์ด์ง• ๋ฒ„ํผ(Staging Buffer)๋ฅผ ํ™œ์šฉํ•˜์—ฌ Readback์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
  • Vulkan / Metal: ๋ชจ๋ฐ”์ผ ๋ฐ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ํ™˜๊ฒฝ์—์„œ๋Š” ๊ทธ๋ž˜ํ”ฝ์Šค ๋“œ๋ผ์ด๋ฒ„ ๋™๊ธฐํ™” ํŽœ์Šค(Fence) ํƒ€์ด๋ฐ ์ฐจ์ด๋กœ ์ธํ•ด req.hasError๊ฐ€ ๊ฐ„ํ—์ ์œผ๋กœ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ํ•ด๋‹น ํ”„๋ ˆ์ž„ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฒ„๋ฆฌ๊ณ  ์ด์ „ ํ”„๋ ˆ์ž„ ๋ฐ์ดํ„ฐ๋ฅผ ์žฌํ™œ์šฉํ•˜๋Š” ํด๋ฐฑ(Fallback) ์•ˆ์ „ ๋Œ€์ฑ…์ด ์ฝ”๋“œ ์ˆ˜์ค€์—์„œ ์ž‘์„ฑ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

D. ๊ฐ์ฒด ํŒŒ๊ดด(Destroy) ์‹œ ๋ผ์ดํ”„์‚ฌ์ดํด ๋Œ•๊ธ€๋ง ์ฐธ์กฐ

AsyncGPUReadback.Request๊ฐ€ ์™„๋ฃŒ๋˜๊ธฐ ์ „์— ์”ฌ(Scene)์ด ์ „ํ™˜๋˜๊ฑฐ๋‚˜ ComputeBuffer๋ฅผ ์†Œ๋ฉธ(Release)์‹œํ‚ค๋ฉด, ์ฝœ๋ฐฑ ํ•จ์ˆ˜๊ฐ€ ์ด๋ฏธ ํŒŒ๊ดด๋œ C# ๊ฐ์ฒด๋‚˜ ํ•ด์ œ๋œ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์ฐธ์กฐํ•˜๋ ค๋Š” NullReferenceException ๋ฐ Access Violation ๋ฉ”๋ชจ๋ฆฌ ํฌ๋ž˜์‹œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

  • ํ•ด๊ฒฐ์ฑ…: ๊ฐ์ฒด๊ฐ€ ํŒŒ๊ดด๋˜๋Š” OnDestroy() ๋˜๋Š” OnDisable() ์‹œ์ ์— AsyncGPUReadback.WaitAllRequests();๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋Œ€๊ธฐ ์ค‘์ธ ๋น„๋™๊ธฐ ์š”์ฒญ์„ ๊ฐ•์ œ๋กœ ์™„๋ฃŒ์‹œํ‚จ ํ›„ ๋ฒ„ํผ ์ž์›์„ ํ•ด์ œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


5. ์„ฑ๋Šฅ ๋น„๊ต ๋ฒค์น˜๋งˆํฌ (Performance Comparison)

๋‹ค์Œ์€ 100,000๊ฐœ์˜ Vector4 ๋ฐ์ดํ„ฐ๋ฅผ ์ปดํ“จํŠธ ์…ฐ์ด๋”์—์„œ ๊ณ„์‚ฐ ํ›„ CPU๋กœ ์ „๋‹ฌํ•  ๋•Œ ๊ธฐ์กด ๋ฐฉ์‹๊ณผ ์ตœ์ ํ™” ๋ฐฉ์‹์„ ๋น„๊ตํ•œ ์ธก์ • ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค.

์ธก์ • ํ•ญ๋ชฉ (Metrics) ComputeBuffer.GetData (๋™๊ธฐ) AsyncGPUReadback + NativeArray (๋น„๋™๊ธฐ)
CPU ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ์ง€์—ฐ์‹œ๊ฐ„ 16.8 ms (Stall ๋ฐœ์ƒ) 0.12 ms (๋ณ‘๋ชฉ ์ œ๊ฑฐ)
GC Alloc (๋ฉ”์ธ ์Šค๋ ˆ๋“œ) 1.6 MB / frame (๋ฐฐ์—ด ํ• ๋‹น ์‹œ) 0 Byte (Zero-Alloc)
๋ฐ์ดํ„ฐ ์ˆ˜์‹  ์ง€์—ฐ(Latency) 0 ํ”„๋ ˆ์ž„ (์ฆ‰์‹œ) 1 ~ 2 ํ”„๋ ˆ์ž„ (๋น„๋™๊ธฐ)
์ฒด๊ฐ ํ”„๋ ˆ์ž„๋ ˆ์ดํŠธ (FPS) 42 FPS (์ง€์†์  ๋ž™ ๋ฐœ์ƒ) 144+ FPS (๋งค์šฐ ๋ถ€๋“œ๋Ÿฌ์›€)

6. ๊ฒฐ๋ก  ๋ฐ ์š”์•ฝ CheckList

์œ ๋‹ˆํ‹ฐ 3D ํ™˜๊ฒฝ์—์„œ ์ปดํ“จํŠธ ์…ฐ์ด๋”๋ฅผ ํ™œ์šฉํ•ด ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•  ๋•Œ, GPU-CPU ๋™๊ธฐํ™” ๋Œ€๊ธฐ ์‹œ๊ฐ„(Stall)์€ ํ”„๋กœ์ ํŠธ ์„ฑํŒจ๋ฅผ ๊ฐ€๋ฅด๋Š” ์น˜๋ช…์ ์ธ ์„ฑ๋Šฅ ๋ณ‘๋ชฉ์ž…๋‹ˆ๋‹ค. ๋™๊ธฐ์‹ GetData() ๋Œ€์‹  AsyncGPUReadback API๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์šฉํ•˜๋ฉด CPU ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋ฅผ ๋™๊ธฐํ™” ๋Œ€๊ธฐ๋กœ๋ถ€ํ„ฐ ์™„๋ฒฝํ•˜๊ฒŒ ํ•ด๋ฐฉ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ตœ์ ํ™” ์ ์šฉ ์ฒดํฌ๋ฆฌ์ŠคํŠธ:

  1. ComputeBuffer.GetData() ๋™๊ธฐ ํ•จ์ˆ˜๊ฐ€ ๋ฃจํ”„๋‚˜ Update() ๋‚ด๋ถ€์— ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์ œ๊ฑฐํ–ˆ๋Š”๊ฐ€?
  2. AsyncGPUReadback.Request()์™€ NativeArray<T>๋ฅผ ์กฐํ•ฉํ•˜์—ฌ GC ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น์„ 0 Bytes๋กœ ๋งŒ๋“ค์—ˆ๋Š”๊ฐ€?
  3. ๋น„๋™๊ธฐ ์ฝ๊ธฐ ํŠน์„ฑ์ƒ ๋ฐœ์ƒํ•˜๋Š” 1~2 ํ”„๋ ˆ์ž„ ๋ฐ์ดํ„ฐ ์ง€์—ฐ์„ ๊ณ ๋ คํ•œ ๊ฒŒ์ž„ ๋กœ์ง(๋ณด๊ฐ„ ๋˜๋Š” ์˜ˆ์ธก)์ด ์„ค๊ณ„๋˜์—ˆ๋Š”๊ฐ€?
  4. ๋Œ€๊ธฐ ์ค‘์ธ ์š”์ฒญ ์ˆ˜(Pending Queue Count)๋ฅผ ์ œํ•œํ•˜์—ฌ ๋น„๋™๊ธฐ ๋ฐฑํ”„๋ ˆ์…” ํ˜„์ƒ์„ ๋ฐฉ์ง€ํ–ˆ๋Š”๊ฐ€?
  5. ๊ฐ์ฒด ํŒŒ๊ดด ์‹œ AsyncGPUReadback.WaitAllRequests()๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๋Œ•๊ธ€๋ง ๋ฉ”๋ชจ๋ฆฌ ํฌ๋ž˜์‹œ๋ฅผ ๋ฐฉ์ง€ํ–ˆ๋Š”๊ฐ€?

์œ„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ์™€ ํ…œํ”Œ๋ฆฟ ์ฝ”๋“œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ตœ์ ํ™”๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค๋ฉด ๊ณ ์„ฑ๋Šฅ ํŒŒํ‹ฐํด, ๋Œ€๊ทœ๋ชจ AI, ์ง€ํ˜• ๋ฉ”์‰ฌ ๋ณ€ํ˜• ์‹œ์Šคํ…œ ๋“ฑ ์–ด๋– ํ•œ ๋Œ€์šฉ๋Ÿ‰ GPU ๋ณ‘๋ ฌ ์—ฐ์‚ฐ ๊ธฐ๋Šฅ๋„ ํ”„๋ ˆ์ž„ ๋“œ๋ž ์—†์ด ์•ˆ์ •์ ์œผ๋กœ ๊ตฌํ˜„ํ•ด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•