Unity AsyncGPUReadback으로 프레임 끊김을 줄이는 고해상도 스크린샷 구현하기
Unity에서 Texture2D.ReadPixels의 동기 대기와 메인 스레드 부하를 AsyncGPUReadback과 NativeArray로 줄이는 방법을 설명합니다. 백그라운드 인코딩, 버퍼 수명, 남아 있는 메모리 할당과 기기별 성능 점검도 다룹니다.

핵심 요약
기존 Texture2D.ReadPixels는 GPU 렌더 파이프라인의 동기화 대기를 강제해 메인 스레드에 큰 프레임 스파이크를 일으킬 수 있습니다. **AsyncGPUReadback.Request를 사용해 복사를 비동기로 요청하고 반환된NativeArray<byte>**를 백그라운드 인코딩과 결합하면 동기 대기와 메인 스레드 부하를 줄일 수 있습니다. 다만 이 예제처럼 ToArray()와 이미지 인코더가 새 배열을 만들기 때문에 GC 할당을 완전히 없애지는 않습니다.
4K() 이상의 고해상도 스크린샷 캡처나 포토 모드(Photo Mode)에서는 GPU와 CPU 간 동기 대기 때문에 화면이 끊길 수 있습니다. 이 글에서는 비동기로 GPU 프레임 버퍼를 읽고 이미지 인코딩과 파일 저장을 백그라운드로 옮겨 메인 스레드 지연을 줄이는 파이프라인을 단계별로 설명합니다.
기존 Texture2D.ReadPixels 방식은 왜 메인 스레드를 멈추게 할까?
전통적인 스크린샷 구현 방식은 RenderTexture.active를 지정한 후 Texture2D.ReadPixels를 호출하는 구조였습니다. 이 방식의 치명적인 단점은 동기식 GPU 파이프라인 플러시(Synchronous Pipeline Flush)를 유발한다는 점입니다.
// 전통적인 동기식 방식 (프레임 멈춤 유발)
RenderTexture.active = renderTarget;
Texture2D screenShot = new Texture2D(width, height, TextureFormat.RGB24, false);
screenShot.ReadPixels(new Rect(0, 0, width, height), 0, 0); // 여기서 CPU가 GPU 완료를 대기!
screenShot.Apply();
CPU가 ReadPixels 명령을 내리면 GPU가 해당 렌더 타깃에 대한 모든 드로우 콜을 완전히 끝마칠 때까지 CPU 메인 스레드가 완전히 대기(Stall) 상태에 빠집니다.
게다가 4K 해상도()의 32비트(RGBA32) 버퍼 크기는 다음 수식과 같습니다.
단 한 장의 사진을 찍기 위해 약 의 비관리 메모리와 텍스처 객체 복사, PNG 인코딩을 거치며 수백 밀리초 동안 게임 루프가 멈추게 됩니다.
| 비교 항목 | Texture2D.ReadPixels | AsyncGPUReadback |
|---|---|---|
| 동작 방식 | 동기식(Sync) GPU-CPU 복사 | 비동기식(Async) GPU-CPU 복사 |
| 메인 스레드 영향 | GPU 파이프라인 플러시로 멈춤 발생 | 스파이크 없음 (콜백 기반 수신) |
| 메모리 할당 | Texture2D 및 픽셀 배열 GC 할당 | NativeArray<byte>를 통한 제로 GC |
| 소요 프레임 | 동일 프레임(Blocking) | 1~3프레임 후 완료(Non-blocking) |
| 지원 플랫폼 | 전 플랫폼 | Direct3D 11+, Metal, Vulkan, OpenGL ES 3.0+ |

AsyncGPUReadback의 내부 동작 원리와 비동기 데이터 흐름
Unity 2018.2부터 도입된 AsyncGPUReadback은 GPU의 그래픽스 드라이버와 DMA(Direct Memory Access) 하위 시스템을 활용합니다. 렌더 큐에 복사 요청 명령을 등록해 두고 CPU는 멈춤 없이 다음 프레임 연산을 계속 진행합니다.
GPU 렌더링이 완료되어 스테이징 버퍼(Staging Buffer)에 데이터가 기록되면 Unity 서브시스템이 완료 이벤트를 수신하여 메인 스레드 콜백으로 AsyncGPUReadbackRequest 객체를 넘겨줍니다.
flowchart TD
A["Camera.Render() to RenderTexture"] --> B["AsyncGPUReadback.Request(RenderTexture)"]
B --> C{"GPU Rendering & Async Transfer"}
C -->|Frame N+1 ~ N+3| D["OnComplete Callback"]
D --> E["GetData<byte>() to NativeArray"]
E --> F["Background Thread / Job System"]
F --> G["Image Compression (JPG/PNG)"]
G --> H["Async File I/O to Disk"]
이 구조를 채택하면 메인 렌더 루프는 60fps 또는 120fps를 유지하면서도 백그라운드 스레드에서 압축 및 파일 저장 작업을 온전히 소화할 수 있습니다.
메인 스레드 부하를 줄이는 고해상도 캡처 파이프라인 구현 단계
전체 캡처 파이프라인은 3단계로 구성됩니다.
- Step 1: 오프스크린
RenderTexture생성 및 렌더링 - Step 2:
AsyncGPUReadback.Request비동기 요청 발행 - Step 3:
NativeArray<byte>데이터 추출 및 백그라운드 인코딩/저장

1. 고해상도 렌더링 및 비동기 복사 요청 (C# 구현)
다음 코드는 카메라 뷰를 지정된 해상도의 임시 RenderTexture로 렌더링한 후 비동기로 데이터를 추출하는 완결된 스크립트입니다.
using System;
using System.IO;
using System.Threading.Tasks;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;
public class HighResAsyncScreenshot : MonoBehaviour
{
[SerializeField] private Camera targetCamera;
[SerializeField] private int captureWidth = 3840;
[SerializeField] private int captureHeight = 2160;
public void TakeScreenshot(string filePath)
{
if (targetCamera == null)
targetCamera = Camera.main;
// Step 1: 임시 렌더 텍스처 할당 (메모리 절약을 위해 임시 버퍼 사용)
RenderTexture rt = RenderTexture.GetTemporary(captureWidth, captureHeight, 24, RenderTextureFormat.ARGB32);
RenderTexture prevTarget = targetCamera.targetTexture;
targetCamera.targetTexture = rt;
targetCamera.Render();
targetCamera.targetTexture = prevTarget;
// Step 2: 비동기 GPU 읽기 요청
AsyncGPUReadback.Request(rt, 0, TextureFormat.RGBA32, (AsyncGPUReadbackRequest request) =>
{
// 임시 RenderTexture는 즉시 반환
RenderTexture.ReleaseTemporary(rt);
if (request.hasError)
{
Debug.LogError("[Screenshot] AsyncGPUReadback 요청 중 오류가 발생했습니다.");
return;
}
// Step 3: CPU 백그라운드 스레드로 인코딩 위임
ProcessAndSaveScreenshot(request, captureWidth, captureHeight, filePath);
});
}
private static void ProcessAndSaveScreenshot(AsyncGPUReadbackRequest request, int width, int height, string filePath)
{
// NativeArray 형태로 픽셀 버퍼 획득 (가비지 컬렉션 할당 없음)
NativeArray<byte> rawData = request.GetData<byte>();
byte[] byteCopy = rawData.ToArray(); // 백그라운드 스레드 전달용 복사본
Task.Run(() =>
{
try
{
// Unity ImageConversion 유틸 대신 경량화된 Raw to TGA/PNG 또는 인코더 사용 가능
// 본 예제에서는 기본 포맷 인코딩을 백그라운드에서 처리
byte[] encodedBytes = ImageConversion.EncodeArrayToJPG(byteCopy, UnityEngine.Experimental.Rendering.GraphicsFormat.R8G8B8A8_UNorm, (uint)width, (uint)height, 0, 90);
File.WriteAllBytes(filePath, encodedBytes);
Debug.Log($"[Screenshot] 성공적으로 저장되었습니다: {filePath}");
}
catch (Exception ex)
{
Debug.LogError($"[Screenshot] 파일 저장 실패: {ex.Message}");
}
});
}
}
고해상도 캡처 시 성능과 메모리를 최적화하는 핵심 팁
1. 텍스처 포맷과 색 공간(Color Space) 일치
Linear 색 공간을 사용할 경우 AsyncGPUReadback.Request에서 텍스처 포맷을 올바르게 지정하지 않으면 캡처된 스크린샷이 어둡거나 채도가 왜곡될 수 있습니다. TextureFormat.RGBA32를 표준으로 사용하고 필요에 따라 감마 보정을 셰이더 단계에서 적용해야 합니다.
2. Y축 반전(Flip) 문제 처리
DirectX, Vulkan, Metal 등 그래픽스 API에 따라 버퍼 텍스처의 Y축 좌표계가 반전되어 캡처될 수 있습니다. 이를 CPU에서 회전 루프로 뒤집으면 메모리 연산 비용이 증가합니다. 가능한 경우 Graphics.Blit 단계에서 수직 플립 셰이더를 거치거나 인코더 단계에서 Y 스트라이드를 역방향으로 읽어 인코딩해야 합니다.
3. NativeArray 수명 주기 관리
request.GetData<byte>()로 얻어온 NativeArray는 AsyncGPUReadbackRequest의 내부 네이티브 메모리를 가리키고 있습니다. 콜백 메서드가 종료되면 해당 네이티브 버퍼가 해제되므로 비동기 태스크로 넘기기 전에 ToArray() 또는 NativeArray.Copy로 작업 버퍼를 확보해야 댕글링 포인터 크래시를 방지할 수 있습니다.
자주 묻는 질문 (FAQ)
Q1. AsyncGPUReadback은 모바일(iOS, Android)에서도 안전하게 동작하나요?
A: 네. Android(Vulkan 또는 OpenGL ES 3.0 이상)와 iOS(Metal) 최신 기기에서 완전하게 동작합니다. 다만 구형 OpenGL ES 2.0 기기에서는 지원되지 않으므로 런타임에 SystemInfo.supportsAsyncGPUReadback 프로퍼티를 확인하여 미지원 기기에서는 기존 동기식 fallback 로직을 타도록 분기해야 합니다.
Q2. 캡처 요청 후 결과가 들어올 때까지 몇 프레임이나 지연되나요?
A: 플랫폼과 GPU 로드에 따라 다르지만 일반적으로 1~3프레임 뒤에 콜백이 호출됩니다. 렌더 파이프라인의 스테이징 버퍼 플러시 주기에 따라 비동기로 전달되므로 특정 프레임의 완벽한 픽셀 보존을 위해서는 캡처 대상 RenderTexture의 레퍼런스를 콜백 완료 시점까지 유지해야 합니다.
Q3. PNG 인코딩 시 발생하는 메인 스레드 부하는 어떻게 해결하나요?
A: Unity의 ImageConversion.EncodeToPNG()는 메인 스레드에서만 호출 가능한 네이티브 함수가 포함되어 있어 스파이크를 유발할 수 있습니다. 메인 스레드 독립적인 C# 전용 경량 인코더(예: QOI 포맷, libpng C# 포팅 또는 EncodeArrayToJPG)를 Task.Run 내부에서 실행하면 CPU 병목을 완전히 격리할 수 있습니다.
요약 및 결론
AsyncGPUReadback은 GPU와 CPU 사이의 동기 대기를 줄여 고화질 스크린샷 캡처가 메인 스레드에 주는 부담을 낮춥니다. 캡처용 추가 렌더링, 메모리 복사, 이미지 인코딩과 디스크 쓰기 비용은 남으므로 대상 기기에서 프레임 시간을 측정해야 합니다.
- 동기식
Texture2D.ReadPixels대신AsyncGPUReadback.Request를 사용하십시오. SystemInfo.supportsAsyncGPUReadback으로 기기 호환성을 검증하십시오.- 추출된 버퍼는
Task.Run및 네이티브 구조체를 활용해 백그라운드에서 인코딩 및 디스크 저장을 수행하십시오.



