12

Part IV · Multi-pass Chapter 12 of 18 Build 2025.31550

GLSL Advanced POP

“한 노드 안에서 여러 pass를 돌리고 여러 buffer에 쓰는 것은 GPGPU의 표준 그 자체다.”

이 챕터에서 정복할 CG 개념

왜 이게 중요한가

GLSL POP은 의도적으로 단순화돼 있다. 한 클래스에 한 element-당 한 thread, 입출력 element 수 동일. GPGPU의 실제 워크로드는 이 가정을 자주 깬다 — particle은 매 frame 생기고 죽고, broad-phase collision은 한 패스가 bucket 수에 쓰고 다음 패스가 point 수에 쓰며, fluid solver는 velocity와 divergence를 같은 셰이더에서 갱신한다. "한 셰이더가 여러 buffer에 동시에 쓰고, 출력 element 수를 직접 결정"하는 능력이 표준이다. GLSL Advanced POP이 그 능력을 모두 노출한다. 이 챕터를 끝내면 LearnOpenGL의 multiple render target, Unity의 RWStructuredBuffer, WebGPU의 storage binding이 POP의 어디에 대응되는지 보인다.

POP에서의 노출 지점

Passesprevpassoutput은 튜토리얼 페이지(Write a GLSL POP)에서 "GLSL POP 전용"으로 적혀 있으나 Advanced POP 파라미터 페이지에 동일 파라미터가 노출된다. wiki 미확정 — 실험 필요. 본 챕터는 파라미터 페이지를 1차 근거로 따른다(튜토리얼이 stale인 것으로 추정).

이론

Single-shader vs Per-primitive-batch dispatch

shaderdispatchmode = single은 한 dispatch가 전체 element pool에 1D thread를 펼친다(TDIndex()는 0..N-1). 한 thread가 oTDPoint_*[id], oTDPrim_*[id], oTDVert_*[id] 세 출력에 동시 write 가능. GLSL POP과의 결정적 차이다.

perprimbatch는 입력의 primitive batch(동일 토폴로지 그룹) 단위로 셰이더를 한 번씩 호출. batch 내 thread 수는 numthreadsbatchmode로 결정.

Output 페이지의 element-count 변경

GLSL POP은 입출력 element 수가 같다. Advanced POP은 Output 페이지로 변경한다. maxpointsmode = fromparams는 parameter로, fromattrs는 GPU attribute로 상한 결정. attribute 경로는 wiki의 indirect dispatch를 발동시킨다 — "If the number of elements … is known on the GPU and not on the CPU … the compute shader is launched with an indirect dispatch." particle spawn/kill의 진입점이다. maxtrianglesmode 등도 동일 구조.

여기서 중요한 것: element 수가 변하면 "변경되지 않은 attribute의 reference pass-through"가 정지한다. 모든 attribute를 셰이더가 명시적으로 써야 한다.

Extra Output — 한 컴퓨트가 여러 buffer에 쓴다

LearnOpenGL의 "Multiple Render Targets"는 fragment shader 하나가 out vec4 gColor; out vec4 gNormal; 식으로 여러 texture에 동시 출력하는 패턴이다. Extra Output은 그 compute shader 버전이다. Extra Outputs 페이지에서 extraout0pop이 다른 POP을 가리키면, 그 POP의 attribute 버퍼가 셰이더의 출력으로 추가된다. 셰이더 안에서는 oTDPoint_P[id](메인)와 oTDPoint_trail_P[id](extra output "trail")가 동시에 쓰여진다. 다운스트림에서 GLSL Select POP의 extraout 파라미터에 이름 trail을 적으면 그 출력만 꺼낼 수 있다. 한 dispatch가 여러 SSBO에 동시에 쓰는 것이지 두 번 cook이 아니다.

각 extra output의 source POP은 자체 element 수를 가진다. thread 수는 메인 출력 기준이므로 element 수 mismatch가 있으면 bounds check가 필요하다. wiki는 TDInputNumPoints_OutputName() / TDInputNumPrims_OutputName() / TDInputNumVerts_OutputName()를 제공한다.

outputaccess = readwrite와 multi-pass

wiki 인용: "Read-Write is necessary to perform atomic operations." atomicAdd 등 atomic을 쓰려면 SSBO를 readwrite로. spatial hash bucket count, particle spawn counter, histogram bin count 모두 atomic이 필요하다. readwrite일 때는 첫 read 전에 값을 직접 0으로 초기화해야 한다.

Passes = N이면 같은 셰이더가 한 cook 안에서 N번 실행된다. Copy Previous Pass Output to Input ON이면 pass i의 출력이 pass i+1의 입력으로 복사된다. 이 copy가 GPU memcpy인지 buffer swap인지, barrier 의미는 무엇인지 wiki는 명시하지 않는다 — wiki 미확정 — 실험 필요. 안전한 동작 모델은 "각 pass는 완전한 dispatch이고 pass 사이에 memory barrier가 자동으로 들어간다"이다.

이 multi-pass는 Ch.11의 외부 Feedback POP과 시간 스케일이 다르다. multi-pass는 한 cook 내 반복, Feedback POP은 frame 간 누적. fluid Jacobi iteration 30회는 multi-pass로, 시간 진행은 외부 Feedback POP으로 분리하는 것이 표준 조합이다.

손작업 (Hands-on)

셋업 A — Sphere의 P와 N을 한 셰이더에서 동시 갱신

목적: Advanced POP의 multi-class write 능력 최단 확인.

시작 파일

연결: Sphere POP1 → Normal POP1 → GLSL Advanced POP1 → Null POP1.

Compute Shader DAT:

// GLSL Advanced POP, single shader dispatch mode.
// Point Output Attributes: P, N. 함수 시그니처가 클래스 정보를 갖는다.
void main() {
    const uint id = TDIndex();
    if (id >= TDNumElements()) return;

    vec3 p = TDInPoint_P();              // == TDInPoint_P(0u, TDIndex())
    // 원점 중심 단위 구의 점에서는 P 자체가 normal과 같다
    vec3 n = normalize(p);

    oTDPoint_P[id] = p;
    oTDPoint_N[id] = n;                  // 같은 셰이더에서 다른 point attribute에 쓴다
}

이 코드가 GPU에서 실제로 무엇을 하고 있는가: 한 dispatch가 sphere의 점 수만큼 thread를 띄운다. 각 thread는 자신의 P를 SSBO에서 읽어 그대로 다시 쓰고, 같은 인덱스의 N attribute SSBO에도 정규화 결과를 쓴다. 한 thread가 두 SSBO에 동시 출력. GLSL POP 경로라면 P와 N에 두 노드가 필요하다.

셋업 B — Extra Output으로 trail buffer 동시 출력

목적: 한 셰이더가 자기 자신 + 별도 POP에 동시 출력, GLSL Select POP으로 분기.

시작 파일

연결:

Sphere POP1 ──> GLSL Advanced POP1 ──┬──> Null_main ──> Geo COMP A
trail_src   ──> (Extra Outputs)       │
                                       └──> GLSL Select POP1 (trail) ──> Null_trail ──> Geo COMP B

Compute Shader DAT:

// 메인 출력: oTDPoint_P. Extra Output "trail": oTDPoint_trail_P.
void main() {
    const uint id = TDIndex();
    if (id >= TDNumElements()) return;

    vec3 p = TDInPoint_P();

    // Extra Output의 source POP element 수 (mismatch 대비 bounds check)
    uint nTrail = TDInputNumPoints_trail();

    oTDPoint_P[id] = p;
    if (id < nTrail) {
        oTDPoint_trail_P[id] = p + vec3(0.0, 0.5, 0.0);
    }
}

이 코드가 GPU에서 실제로 무엇을 하고 있는가: 한 dispatch에 sphere 점 수만큼 thread. 각 thread는 자기 P를 메인 출력에 쓰고 동시에 별도 SSBO인 trail의 P slot에 Y로 0.5 올린 값을 쓴다. cook 한 번에 두 POP의 attribute 버퍼가 채워진다. 함수 시그니처(oTDPoint_OutputName_AttribName[], TDInputNumPoints_OutputName())는 glsl_pop_deep.md §7 verbatim.

셋업 C — 출력 점 수를 셰이더가 결정 (개요)

maxpointsmode = fromparams, maxpoints = 100000, pointcountmode = fromattrs, pointcountattr = live로 두면 셰이더가 atomicAddlive를 증가시키며 동적으로 점을 spawn/kill한다. 자세한 atomic compaction은 Ch.16. 여기서는 진입점만: outputaccess = readwrite + Initialize Output Attributes = Off + multi-pass.

노드 ↔︎ GLSL 매핑

같은 결과 — "P와 N을 갱신하면서 위로 띄운 trail buffer도 생성" — 의 두 구성.

노드 방식: Sphere → Normal POP → Null_mainSphere → Transform (Y+0.5) → Null_trail 두 체인. P/N 갱신과 trail 생성이 각자 별도 POP cook. GPU dispatch 두 번.

GLSL 방식: 한 Advanced POP이 P, N, trail_P 세 출력을 한 dispatch로 동시 write. cook 한 번. 직관과 어긋나는 부분: 노드 그래프의 두 갈래가 두 번 일하는 게 아니라, 한 dispatch가 두 SSBO에 동시 write하는 일을 노드 그래프가 두 줄기처럼 그려준 것이다. Extra Output은 그 사실을 노드 모양으로 노출하는 인터페이스다.

확인 질문 (Self-check)

  1. Extra Output을 쓸 때 source POP의 element 수와 메인 출력의 element 수가 다르면 셰이더 안에서 어떤 bounds check를 추가해야 하는가? TDInputNumPoints_OutputName()을 어디서 써야 하는가?
  2. outputaccess = readwrite는 언제 필수인가? 다음 중 어떤 경우에 필요한지 답하라: (a) atomic counter를 증가시킬 때, (b) 같은 thread가 자신의 출력 slot을 두 번 쓸 때, (c) 다른 thread가 쓴 값을 자기 thread가 읽을 때.
  3. Number of Threads = Per Max Output Point로 두고 maxpointsmode = fromparams, maxpoints = 100000으로 두면 실제로 dispatch되는 thread 수는 몇 개인가? TDInputNumPoints()TDNumElements()의 값은 각각 얼마인가?
  4. Shader Dispatch Mode = perprimbatch로 두면 한 dispatch가 한 primitive batch만 처리한다. 그러면 메쉬에 triangle 100개 + quad 50개가 있을 때 셰이더는 몇 번 호출되는가?
  5. GLSL POP의 Passes와 Advanced POP의 Passes(파라미터 페이지 기준 — wiki tutorial은 Advanced POP에 없다고 적고 있음)는 실제로 같은 동작을 하는가? 실험으로 확인할 방법은? wiki 미확정 — 실험 필요.

연결 고리

이 챕터의 한 줄 명제

GLSL Advanced POP의 single-shader dispatch + Extra Output + 동적 element count는 GPGPU의 표준 — "한 컴퓨트가 여러 buffer에 동시에 쓰고, 출력 크기를 스스로 결정한다" — 를 POP 그래프 위에 그대로 옮긴 인터페이스다.