book/02-srp-frame-loop-and-dataflow.md

02. SRP 프레임 루프와 데이터 흐름(CPU↔GPU)

SRP 프레임 루프, 컬링/드로우 구성, NativeArray 감각, CPU↔GPU 경계를 기반으로 RenderGraph/CommandBuffer 이해를 준비한다

02. SRP 프레임 루프와 데이터 흐름(CPU↔GPU)

이 챕터는 “SRP는 결국 프레임마다 어떤 데이터를 만들고, 어떤 명령을 GPU로 보내는가”를 정리합니다. 이후 RenderGraph/CommandBuffer/Shader 상호작용을 이해하기 위한 기반입니다.

2.1 프레임의 두 층: 논리(렌더 패스 구성) vs 실행(GPU 명령)

  • 논리 층(Record/Build): “이번 프레임에 어떤 패스를 돌릴 것인지”를 결정하고, 패스 간 의존성을 구성합니다.
  • 실행 층(Execute/Submit): 구성된 패스가 GPU 명령으로 변환되어 실행됩니다.

URP에서는 이 분리가 특히 RenderGraph에서 선명합니다. RecordRenderGraph()가 논리를 기록하고, 이후 URP가 실행을 담당합니다.

2.2 C# 렌더링 루프의 핵심 타입 3개

  1. RenderPipeline / RenderPipelineAsset
  2. ScriptableRenderContext (SRP의 “렌더링 제출” 문맥)
  3. CommandBuffer (GPU 명령을 쌓는 그릇)

ScriptableRenderContext는 결국 CommandBuffer에 쌓인 명령을 적절한 타이밍에 GPU로 제출합니다.

2.3 카메라 단위 렌더링과 FrameData

SRP/URP는 보통 카메라 단위로 렌더링을 수행합니다. 카메라마다:

  • 컬링 결과(무슨 렌더러를 그릴지)
  • 카메라 매트릭스/프로젝션
  • 렌더 타겟(화면 또는 텍스처)
  • 포스트 프로세싱/볼륨 파라미터

같은 데이터가 바뀌기 때문입니다.

URP RenderGraph 경로에서는 ContextContainer frameData를 통해 여러 서브시스템이 공유하는 “이번 프레임의 데이터”가 전달됩니다. (구체 예시는 04. RenderGraphRecordRenderGraph 예제 참고)

2.4 데이터 흐름 체크리스트(실전)

무엇을 만들 때마다 다음을 점검하세요.

  • 이 데이터는 어느 패스가 생산하는가? (write)
  • 어떤 패스가 소비하는가? (read)
  • 수명은 “이번 카메라만”인가, “프레임 전체”인가?
  • GPU에서 접근 형태는? (샘플링 텍스처 / 렌더타겟 / UAV / 상수버퍼)
  • 멀티 카메라(씬뷰/게임뷰/리플렉션)에서 안전한가?

2.5 컬링(Culling): “무엇을 그릴지” 결정하는 단계

렌더링은 결국 “드로우 콜을 얼마나/무엇을/어떤 순서로” 내느냐의 문제입니다. 그 출발점이 컬링입니다.

SRP에서 컬링은 대체로 다음 흐름을 가집니다.

  1. 카메라로부터 ScriptableCullingParameters를 얻는다
  2. context.Cull(ref parameters)CullingResults를 얻는다
  3. 이 결과를 바탕으로 “어떤 렌더러를 어떤 패스로 그릴지”를 구성한다

URP는 이 단계를 내부에서 수행하지만, Render Feature/Pass를 작성할 때도 “이 패스가 어떤 오브젝트를 대상으로 하는가”를 설계하려면 컬링과 필터링 개념을 알아야 합니다.

2.6 드로우 구성: DrawingSettings / FilteringSettings / RenderStateBlock

“무엇을 어떤 셰이더 패스로 그릴지”는 보통 다음으로 결정됩니다.

  • FilteringSettings: 렌더 큐 범위(opaque/transparent), 레이어 마스크 등 “대상” 필터
  • DrawingSettings: 어떤 ShaderTagId(=LightMode)를 사용할지, 정렬 기준, per-object data 등
  • RenderStateBlock: 블렌드/뎁스/스텐실 같은 렌더 상태 오버라이드

Render Feature로 “특정 레이어만” 오프스크린 RT에 그리거나, “특정 LightMode만” 강제 선택해 마스크를 만들고 싶다면 이 3종이 핵심 도구입니다.

2.7 NativeArray는 왜 여기에 등장하는가? (데이터 구조 감각)

Unity의 렌더링/컬링/잡 시스템에서는 NativeArray<T>가 자주 등장합니다.

핵심 이유:

  • GC 없는 메모리(관리형 힙이 아니라 네이티브 메모리)
  • 잡 시스템/버스트와 호환
  • 대용량 데이터를 프레임 단위로 다룰 때 비용을 예측 가능하게 만듦

“NativeArray는 뭔 역할이냐?”를 한 문장으로 요약하면:

렌더링에서 자주 다루는 “큰 배열 데이터(인스턴스/라이트/컬링 결과 등)”를
할당/GC/스레드 문제 없이 다루기 위한 컨테이너입니다.

2.7.1 실무에서 특히 많이 만나는 지점

  • 많은 오브젝트/인스턴스를 그릴 때(인스턴싱 데이터)
  • 라이트 리스트/클러스터 데이터(Forward+ 계열)
  • RenderGraph/파이프라인 내부에서 임시 데이터를 효율적으로 전달할 때

주의: NativeArray는 수명 관리(Dispose)가 중요합니다.
이 책에서는 URP 내부 구현까지 파고들 때 핵심 개념만 소개하고, 실제 API 사용법/Job 패턴은 별도 자료로 확장하는 것을 권장합니다.

2.8 CPU↔GPU 경계: Submit, 동기화, 그리고 “왜 한 프레임이 늦게 보이나”

렌더링 디버깅에서 흔히 나오는 질문:

  • “왜 값 바꿨는데 한 프레임 뒤에 적용되지?”
  • “왜 갑자기 프레임이 튀지?”

이 대부분은 CPU 명령 기록GPU 실행이 분리되어 있기 때문입니다.

  • CPU는 CommandBuffer에 명령을 기록하고
  • SRP가 적절한 타이밍에 제출(Submit)하고
  • GPU는 자신의 큐에서 실행합니다

따라서:

  • GPU가 바쁘면 CPU가 기다리는 구간이 생기고(스톨),
  • 반대로 CPU가 너무 많은 일을 하면 GPU가 놀게 됩니다.

이 경계를 이해하면 RenderGraph의 “리소스 수명/의존성”이 왜 중요한지도 더 명확해집니다.

추가 읽을거리(공식/권위 자료)