https://learn.foundry.com/nuke/developers/13.0/ndkdevguide/2d/architecture.html
2D Architecture — NDK Developers Guide
2D Architecture NUKE’s 2D architecture is largely defined by the Iop class. This class inherits from Op, reimplementing _validate(), and extending functionality to add an image processing path. In turn, Iop is extended and specialized by PixelIop and Dra
learn.foundry.com
NUKE의 2D 아키텍처는 주로 Iop 클래스에 의해 정의됩니다. 이 클래스는 Op를 상속받아 _validate()를 재구현하고, 이미지 처리 경로를 추가하여 기능을 확장합니다. Iop는 다시 PixelIop와 DrawIop로 확장되어 특정 이미지 처리 연산자에 대해 단순화된 구현을 제공합니다.
Iop는 특히 NUKE의 채널 시스템이 어떻게 작동하는지, 관심 영역(ROI)과 정의 영역(ROD)이 노드 트리 상에서 어떻게 전달되는지, 2D 데이터가 어떻게 캐시되는지, 그리고 경계 상자와 이미지 포맷이 어떻게 처리되는지를 정의합니다.
Scanline-Based
NUKE는 비파괴적이고 노드 기반의 32비트 부동 소수점, 다중 채널 이미지 합성 시스템입니다. NUKE의 주요 목적은 입력 이미지를 받아 수정 작업을 수행한 후 출력 이미지를 생성하는 것입니다.
주요 특징:
- 스캔라인 기반 2D 이미지 시스템:
- NUKE는 2D 이미지를 스캔라인 방식으로 처리합니다.
- 다양한 하위 시스템:
- 3D 시스템, 딥 이미지 합성 시스템, 파티클 시스템 등도 지원됩니다.
- 이러한 하위 시스템들은 2D 이미지와는 다른 방식으로 작동하지만, 대부분의 출력은 2D 이미지로 변환되어 최종적으로 2D 출력 이미지가 만들어집니다.
- 3D 시스템 예시:
- 3D 시스템의 출력은 ScanlineRender 노드를 통해 2D 이미지로 변환됩니다.
핵심 개념:
- 이 섹션에서는 스캔라인 렌더 시스템의 기본 개념을 다루며, 다른 하위 시스템과의 공통 개념을 설명합니다.
A Basic Node Graph
NUKE의 이미지 처리 그래프는 주로 Node Graph 또는 DAG(Directed Acyclic Graph) 라고 불리며, 이 그래프의 기본적인 흐름은 다음과 같습니다. 예를 들어, 세 개의 노드로 구성된 단순한 이미지 처리 그래프를 생각할 수 있습니다:
- CheckerBoard (입력 노드 / 생성자 노드)
- ColorCorrect (필터 / 수정자 노드)
- Viewer (출력 노드)
NUKE의 기본적인 노드 유형:
- Generator/Input Node (CheckerBoard):
- 이미지를 생성하는 노드로, 입력이 없고 자체적으로 이미지를 반환합니다.
- Filter/Modifier Node (ColorCorrect):
- 이미지를 수정하는 노드로, 이전 노드로부터 이미지를 받아 수정하고 출력합니다.
- Output Node (Viewer):
- 최종적으로 이미지를 화면에 출력하는 노드입니다. Viewer는 연결된 노드로부터 이미지를 요청하여 화면에 표시합니다.
이미지 처리 과정:
- 처음에는 Viewer 노드가 연결되지 않아 이미지가 표시되지 않습니다. 이때 이미지 처리나 이미지 로딩이 일어나지 않습니다.
- Viewer 노드를 ColorCorrect 노드에 연결하면, Viewer는 이미지를 출력해야 하므로 ColorCorrect 노드에 이미지를 요청합니다.
- ColorCorrect 노드는 다시 CheckerBoard 노드에 이미지를 요청하고, CheckerBoard는 생성자 노드이므로 자체적으로 이미지를 반환합니다.
- ColorCorrect는 받은 이미지를 수정하여 Viewer에 전달하고, 이 과정을 통해 최종적으로 화면에 이미지가 표시됩니다.
‘Pull’ 시스템:
- NUKE는 Pull 시스템을 사용하여 이미지를 처리합니다. 출력 노드가 입력 노드에 이미지를 요청할 때만 이미지 처리가 발생합니다.
- 출력 노드는 Viewer와 같은 노드로, 이미지를 화면에 표시하거나 Write 노드를 통해 디스크에 기록합니다.
이 시스템의 핵심은 출력 노드가 입력 노드로부터 이미지를 요청하는 방식으로, 모든 이미지 처리는 이러한 "pull" 방식에 의해 이루어진다는 점입니다.
Fundamental Image Processing Unit - the Row
NUKE는 스캔라인 기반 시스템으로, 이미지 처리의 기본 단위가 스캔라인입니다. 스캔라인은 이미지의 수평 한 줄을 의미합니다. NDK에서는 스캔라인을 Row라고 부릅니다.
NUKE는 항상 Row 단위로 이미지를 처리합니다. 이 기본 개념을 바탕으로, 앞서 설명한 세 개의 노드가 포함된 간단한 처리 과정은 다음과 같습니다:
이미지 처리 과정:
- Viewer는 640x480 해상도의 이미지를 표시해야 합니다.
- 이 이미지는 480개의 Row로 나뉩니다.
- Viewer는 각 Row를 차례대로 요청합니다.
- ColorCorrect는 자신의 입력에서 한 번에 한 Row씩 요청합니다.
- CheckerBoard는 각 Row를 하나씩 생성하여 ColorCorrect에 반환합니다.
- ColorCorrect는 각 Row를 수정한 후 Viewer에 반환합니다.
NUKE의 메모리 효율성:
- NUKE의 이 아키텍처는 거의 무제한 크기의 이미지를 처리할 수 있게 해줍니다. 왜냐하면 이미지 처리가 Row 단위로 이루어지기 때문에, 전체 이미지가 한 번에 컴퓨터 메모리에 로드될 필요가 없습니다.
- 이렇게 하면 큰 이미지 파일도 메모리 효율적으로 처리할 수 있게 되며, 시스템 자원을 적게 사용하면서도 고해상도 이미지를 다룰 수 있습니다.
The Viewer and Large Image Sizes
NUKE는 큰 이미지를 처리할 때, 화면에 표시되는 Row의 수가 전체 이미지의 Row 수보다 적습니다. 예를 들어, 4K 해상도의 이미지는 800개의 Row로 화면에 표시될 수 있습니다. 이 경우, Viewer는 화면에 표시되지 않는 Row를 처리하지 않기 때문에 불필요한 Row를 건너뛰고, 필요한 Row만 요청하여 이미지 출력을 생성합니다.
이미지 처리 과정 (큰 이미지 처리 시):
- Viewer는 4K 해상도의 이미지를 800픽셀 높이의 박스에 표시해야 합니다.
- Viewer는 이미지를 Row 단위로 나누고, 표시할 Row만 요청합니다. 예를 들어, Row 0, 4, 8, 12, 16 등의 Row만 요청하여, 필요한 Row만 가져옵니다.
- ColorCorrect는 Viewer의 요청에 따라 필요한 Row만 요청합니다 (예: Row 0, 4, 8, 12, 16, ...).
- CheckerBoard는 ColorCorrect가 요청한 Row만 생성하여 반환합니다.
이 방식이 빠르고 상호작용이 원활한 이유:
- NUKE는 큰 이미지를 Row 단위로 나누어 처리함으로써, 전체 이미지를 메모리에 로드하지 않아도 되기 때문에 빠른 속도로 처리할 수 있습니다.
- 또한, 각 Row는 가로 해상도에서 항상 전체 해상도로 계산됩니다. 그 후, 해당 Row가 계산되면 줌 레벨에 관계없이 항상 정확하게 유지됩니다.
- 사용자가 이미지를 확대(Zoom)하면, Viewer는 아직 요청하지 않은 Row에 대해서만 새롭게 요청합니다. 즉, 이미 계산된 Row는 다시 요청하지 않기 때문에, 효율적인 메모리 사용과 빠른 속도를 유지할 수 있습니다.
이 방식은 NUKE가 큰 이미지나 고해상도 이미지 작업을 하면서도 빠르고 원활하게 상호작용할 수 있는 주요 이유 중 하나입니다.
Multi-Threading
NUKE는 이미지를 출력할 때, **출력 노드(Output Node)**가 다중 스레드를 사용하여 여러 Row를 동시에 처리합니다. 이를 통해 속도와 효율성을 높입니다.
다중 스레드 처리 과정:
- 4코어 시스템에서 예를 들어, Viewer는 4개의 스레드를 실행하여 각 스레드가 각각 다른 Row를 독립적으로 처리합니다.
- NUKE UI에서 두 개의 흰색 선을 통해 스레드의 진행 상태를 확인할 수 있습니다: 한 선은 가장 위의 스레드가 처리 중인 Row를, 다른 선은 가장 아래의 스레드가 처리 중인 Row를 나타냅니다.
- 스레드는 선착순 방식으로 작업을 분배받습니다. 예를 들어, 480 Row의 이미지를 처리할 때, 스레드 4개는 Row 0~3을 동시에 처리하고, 각 스레드가 완료되면 다음 Row를 할당받아 계속 작업합니다.
스레드 사용의 특징:
- 처리 작업은 일반적으로 별도로 스레드를 생성하지 않으며, 출력 노드가 스레드에서 이미 생성된 작업을 처리합니다.
- NUKE는 내부 동기화를 통해 하나의 스레드가 하나의 Row만 처리하도록 보장합니다. 이는 여러 스레드가 동시에 같은 Row를 처리하는 것을 방지하며, 스레드 간 충돌을 방지합니다.
이 방식으로 NUKE는 멀티스레드 환경에서 효율적이고 빠른 이미지 처리를 할 수 있으며, 이미지 처리 속도와 성능을 크게 향상시킬 수 있습니다.
The Row Cache
NUKE에서 이미지를 처리할 때, 일부 노드는 출력 Row를 생성하기 위해 여러 입력 Row를 필요로 합니다. 이는 주로 **블러(Blur)**나 커널 기반 연산에서 발생합니다. 예를 들어, Box Blur와 같은 노드는 여러 입력 Row를 필요로 하여 출력 Row를 계산합니다.
예시: Box Blur와 Row 캐시
- Blur 노드가 ColorCorrect 노드 뒤에 추가된 경우, Blur 노드는 각 출력 Row를 생성하기 위해 여러 입력 Row를 참조해야 합니다. 이 경우, ColorCorrect의 일부 Row가 여러 번 요청되므로, 같은 Row를 여러 번 계산하는 일이 발생할 수 있습니다.
- 해결책: NUKE는 캐시 시스템을 사용하여 이미 처리된 Row를 다시 계산하지 않도록 합니다. 첫 번째로 Row가 요청되면, 해당 Row는 캐시에 저장됩니다. 이후 Blur 노드가 같은 Row를 다시 요청하면, 캐시에서 해당 Row를 반환하여 불필요한 재계산을 피할 수 있습니다.
캐시와 관련된 사항:
- Row 캐시는 NUKE 사용자 인터페이스에서 **‘이미지 버퍼(Image Buffers)’**로 나타납니다.
- F12를 눌러 캐시를 지울 수 있으며, 캐시의 최대 메모리 크기는 Preferences에서 설정할 수 있습니다.
- 이 캐시는 메모리 버퍼로, 일반적으로 디스크에 저장되지 않습니다. 즉, NUKE가 종료된 후에는 캐시가 유지되지 않습니다.
DiskCache 노드:
- DiskCache 노드는 특수한 노드로, 캐시된 Row를 디스크에 저장하거나 디스크에서 읽어올 수 있습니다. 이는 메모리 캐시를 넘어서 큰 데이터를 처리할 때 유용하게 사용됩니다.
이 시스템 덕분에 NUKE는 효율적으로 메모리를 관리하고, 반복적인 계산을 피하여 속도와 성능을 최적화할 수 있습니다.
Tiles
NUKE에서는 이미지 계산을 수행할 때, 하나의 Row만으로는 충분하지 않을 경우가 많습니다. 이때 Tile 개념을 사용합니다. Tile은 한 번에 처리할 수 있는 2차원 배열로, 입력 노드에서 필요한 여러 Row를 포함하는 영역입니다.
Tile의 동작 방식:
- Tile의 정의: Tile은 2D 배열 형태로 픽셀을 처리할 수 있게 해주는 객체로, 여러 Row를 포함할 수 있습니다. Tile의 크기는 고정되어 있으며, 이를 통해 여러 행(Row)을 한 번에 처리할 수 있습니다.
- Tile 생성과 캐시:
- NUKE는 Tile을 생성할 때, 먼저 캐시를 확인하고, 필요하다면 해당 Tile에 필요한 Row들을 캐시합니다.
- 그 후, NUKE는 캐시에 있는 Row들을 잠그고 Tile 객체로 전달합니다. 이를 통해 효율적으로 데이터를 처리할 수 있습니다.
- 여러 스레드에서의 Tile 처리:
- 여러 스레드가 겹치는 Tile을 생성할 때, 이미 캐시에 있는 Row들이 많기 때문에 최소한의 추가 처리만으로 Tile이 생성됩니다.
- 이는 병렬 처리에서 효율성을 높여, NUKE가 여러 스레드를 사용할 때에도 성능을 최적화할 수 있도록 합니다.
요약:
- NUKE에서 기본적인 이미지 처리 단위는 여전히 Row이지만, Tile은 여러 Row를 동시에 처리할 수 있도록 도와주는 2D 배열 형태의 객체입니다.
- 캐시 시스템 덕분에 이미 처리된 Row들은 재계산 없이 Tile 안에 재사용되어, 성능을 높이는 데 기여합니다.
- 여러 스레드가 동시에 Tile을 처리할 수 있어, 병렬 처리로 더 빠르고 효율적인 이미지 처리가 가능합니다.
Channels
NUKE에서 이미지 색상은 **채널(channels)**로 처리됩니다. 채널은 이미지의 각 픽셀에서 하나의 부동 소수점 값을 나타내며, 각 채널은 32비트 부동 소수점 배열로 저장됩니다. 이 배열은 각 픽셀에 대해 해당 채널의 값을 설명합니다.
채널 및 채널 세트
- 채널: 각 채널은 이미지에서 특정 색상 컴포넌트를 표현합니다. 예를 들어, Red는 하나의 채널을 의미하며, 이 채널은 이미지의 모든 픽셀에 대해 빨간색 값을 표현합니다.
- 채널 세트 (Channel Set): 여러 채널이 모여 채널 세트를 형성합니다. 채널 세트는 해당 채널들의 의미를 정의합니다. 예를 들어, rgb 채널 세트에는 r (빨강), g (초록), b (파랑) 채널이 포함되어, 이를 통해 연산자는 각 채널이 무엇을 의미하는지 알 수 있습니다.
채널 접근
NUKE에서는 채널에 접근할 때 주로 foreach 매크로를 사용합니다. 이 매크로는 채널 세트를 순회하면서 각 채널에 대해 작업을 할 수 있게 해줍니다.
예시 코드:
foreach( channel, channelSet ) {
float *outputPixel = row.writable(channel);
}
이 코드는 주어진 채널 세트를 순회하면서 각 채널에 대한 출력 픽셀 포인터를 가져오는 예시입니다. 각 채널의 값을 처리할 수 있도록 해주며, 이미지 내 모든 픽셀에 대해 작업을 할 수 있습니다.
요약:
- 채널은 이미지의 각 색상 컴포넌트를 나타내며, 32비트 부동 소수점 배열로 저장됩니다.
- 여러 채널이 모여 채널 세트를 구성하며, 각 채널 세트는 그 안의 채널들의 의미를 정의합니다.
- NUKE에서 채널에 접근할 때 foreach 매크로를 사용하여 채널 세트를 순회하고, 각 채널을 처리할 수 있습니다.
The Viewer Cache
NUKE에는 Row 캐시 외에도 Viewer 캐시라는 또 다른 캐시가 존재합니다. 이 캐시는 디스크 캐시로도 불리며, 각 프레임이 Viewer에 표시될 때 디스크에 파일로 저장됩니다. 중요한 점은 Viewer 캐시가 이미지 처리에는 사용되지 않고, 빠른 이미지 표시를 목적으로 한다는 것입니다. 또한, Row 캐시와는 별개로 처리됩니다.
Viewer 캐시
- 목적: Viewer 캐시는 주로 재생 및 보기를 위한 빠른 이미지 표시를 위해 사용됩니다. 캐시된 이미지는 디스크에 저장되므로, NUKE를 종료하고 다시 시작해도 캐시된 데이터는 유지됩니다.
- 동작: Viewer가 출력 노드로서 이미지를 표시할 때, 먼저 표시해야 할 Rows가 이미 Viewer 캐시에 저장되어 있는지 확인합니다. 캐시에 해당 데이터가 있으면, 입력 노드에 요청하지 않고 캐시된 데이터를 바로 표시합니다.
- 캐시 미스 처리: 만약 요청된 Rows가 캐시에 없다면, Viewer는 여전히 입력 노드에 요청하여 이미지를 가져옵니다.
이 방식은 특히 빠른 이미지 재생 및 피드백을 제공하며, 여러 번 계산을 피할 수 있어 효율성을 높입니다.
Memory
NUKE는 Row 캐시 메모리 크기가 **환경 설정(Preferences)**에서 설정된 한도를 초과하면, 캐시에서 필요한 Row를 해제하여 메모리 제한을 맞추려고 합니다. 이때, 해제할 Row를 선택하는 알고리즘은 보통 요청 횟수, Row 접근 횟수, 그리고 **연산 비용(op ‘slowness’)**을 기준으로 결정됩니다. 잠금된 Rows는 Tile이나 다른 객체에 의해 사용 중이므로 해제되지 않습니다.
메모리 관리
- 메모리 한도 초과 시 처리: NUKE는 Row 캐시가 설정된 메모리 크기를 초과하면, 사용되지 않거나 덜 중요한 Row들을 해제하여 메모리 한도를 맞추기 위해 노력합니다. 일반적으로 약 10%의 여유 메모리를 두고 처리합니다.
- 잠금된 Rows: Tile이나 다른 객체에 의해 사용되는 Row는 잠금 상태로 해제되지 않으며, 이러한 Row들은 메모리 해제 대상으로 고려되지 않습니다.
- 사용자 플러그인 메모리 관리: 사용자 플러그인이 자체적으로 임시 버퍼를 할당할 경우, DDImage::Memory 함수로 메모리 부족 상황에 대비해 등록해야 합니다.
메모리 사용
- 3D 기하학이나 사용자 할당 메모리와 같은 요소도 메모리 사용에 영향을 미치며, 이들 역시 메모리 해제 이벤트를 트리거할 수 있습니다.
Iop Call Order
Iops의 생성, 노브 처리(knob handling), 파괴는 Op 클래스에서 직접 상속됩니다. 이는 Fundamental Concepts 장에서 소개되었고, Architecture 섹션에서 자세히 설명되었습니다.
_validate
void Iop::_validate(bool)
Nuke의 _validate()와 IopInfo 개요
_validate()는 Nuke에서 Op 노드의 상태를 설정하는 중요한 함수로, Iop 클래스와 자식 클래스들에서 IopInfo라는 정보를 설정하는 데 중요한 역할을 합니다. 여기서는 _validate()의 기능과 IopInfo 필드에 대해 이해하기 쉽게 요약하겠습니다.
1. IopInfo의 역할
- IopInfo: Box 클래스를 상속받은 단순한 바운딩 박스 클래스이며, Op가 픽셀을 보유한 영역을 나타냅니다. 뷰어(Viewer)에서 해당 영역이 bbox로 표시되며, overscan 영역을 포함할 수 있습니다.
2. IopInfo 주요 필드 설명
- fullSizeFormat과 format:
- fullSizeFormat은 원본 형식(비프록시 모드)이며, 프록시 모드에서도 스케일되지 않은 형식을 저장합니다.
- format은 프록시 모드에서 실제 사용되는 형식을 저장하며, Viewer에 표시되는 bbox가 이 형식으로 클리핑 됩니다.
- channels:
- 해당 Op에서 정의된 채널을 의미합니다. Iop에 없는 채널을 가져오려 할 경우, 0 값이 반환됩니다.
- first_frame 및 last_frame:
- Op의 프레임 범위를 지정하며, 예를 들어, Read 노드의 경우 소스 소재의 시작과 끝 범위가 됩니다.
- black_outside:
- Nuke는 기본적으로 bbox 외부를 연장하여 엣지 데이터를 제공합니다. 하지만 Op에서 black_outside를 설정하면 추가된 검정 픽셀 패딩을 사용하여 데이터의 엣지 확장을 방지할 수 있습니다.
- ydirection:
- 선호되는 y 방향의 접근 순서를 나타냅니다. 양수 값은 하단에서 상단, 음수 값은 상단에서 하단으로 접근하는 순서를 의미합니다.
3. _validate() 함수 개요
- 작업 흐름:
- _validate()는 Nuke의 메인 스레드에서 호출되며, UI가 블로킹 되지 않도록 지나치게 많은 계산을 피하는 것이 중요합니다.
- 특히, 입력 이미지의 계산은 시간이 오래 걸릴 수 있어 피해야 합니다.
- outchannels 설정:
- out_channels_ 필드는 Mask_All이 기본값이며, 이를 변경하면 Nuke는 최적화를 통해 _request와 engine() 호출을 건너뛸 수 있습니다. 이를 통해 input(0)에서 직접 입력 데이터를 가져올 수 있습니다.
- 그러나 이 최적화가 항상 보장되는 것은 아니므로, engine() 구현 시 out_channels_에 포함되지 않은 채널에 대한 호출에도 대응할 수 있도록 준비해야 합니다.
- 기타 주의사항:
- _validate()에서 호출될 때는 올바른 프레임 및 뷰에 대해 설정된 상태로, outputContext()와 knobs()가 유효하게 저장되어 있습니다.
이와 같은 내용으로 _validate() 함수와 IopInfo의 구성 요소를 이해하면, Nuke의 Op 노드와 Iop 객체의 검증 과정에서 IopInfo의 역할을 잘 파악할 수 있습니다.
_request
Iop::_request 함수 개요
_request()는 Nuke에서 Iop 노드에 대해 관심 있는 **영역(region)**과 **채널(channel)**을 요청할 때 호출됩니다. 이 함수의 역할과 사용 방법을 이해하면 효율적으로 Nuke에서 리소스를 관리하고 성능을 최적화할 수 있습니다. 아래는 _request()의 주요 포인트와 사용법입니다.
1. _request() 함수의 역할
- 주요 역할:
- Nuke는 Iop 노드에서 특정 영역과 채널에 대한 관심 정보를 전달하기 위해 _request()를 호출합니다.
- 이 함수는 Nuke의 캐시와 계산의 최적화를 돕기 위해 호출되며, 필요한 영역과 채널만 요청하도록 설계되어 있습니다.
- 입력 값 설명:
- x, y, r, t: 관심 영역의 좌상단(x, y)과 우하단(r, t)을 나타냅니다.
- ChannelMask: 요청된 채널을 나타냅니다.
- count: 캐시 관리 힌트로, 중첩된 요청이 얼마나 발생했는지 표시합니다.
2. _request() 구현 시 고려사항
- 최소 영역 요청:
- 최소한의 영역과 필요한 채널만 요청하는 것이 중요합니다. Nuke는 요청된 영역 전체를 계산하고 캐시할 수 있으므로, 불필요한 요청을 피하면 성능을 크게 향상시킬 수 있습니다.
- 다중 호출 관리:
- Nuke는 복잡한 트리 구조에서 _request()를 여러 번 호출할 수 있으며, 각 호출에서 누적된 영역과 채널을 전달합니다. 이를 적절히 관리하여 중복 계산을 피하고 최적화된 캐싱을 유지하는 것이 중요합니다.
- UI 스레드에서 실행:
- _request()는 주로 UI 스레드에서 실행되므로, 오랜 시간을 소모하지 않도록 해야 합니다. validate()와 마찬가지로 과도한 작업은 피해야 UI 응답성이 떨어지지 않습니다.
3. count 파라미터와 캐싱
- count 파라미터:
- count는 캐시의 힌트 역할을 하며, 중첩된 요청의 수를 나타냅니다.
- 입력에 대해 request()를 호출할 때, count 값을 1보다 크게 설정하면 해당 입력에서 데이터를 강제로 캐시하게 할 수 있습니다. 이는 복잡한 트리 구조에서 캐싱 최적화에 도움을 줄 수 있습니다.
4. 기본 _request() 구현
- 기본 제공 구현:
- Iop 클래스에서 제공하는 기본 _request() 구현은 입력이 Iop에서 파생된 것으로 가정합니다.
- 만약 GeoOp와 같이 Iop에서 파생되지 않은 입력을 사용할 계획이라면, 반드시 커스텀 _request() 구현이 필요합니다. 그렇지 않으면 Nuke에서 예상치 못한 동작이 발생할 수 있습니다.
요약
- _request()는 관심 영역과 채널을 요청하여 최소 영역만 계산하게 도와주는 중요한 함수입니다.
- UI 스레드에서 실행되므로 최적화된 구현이 필요하며, count 파라미터를 통해 캐싱을 제어할 수 있습니다.
- 입력이 Iop에서 파생되지 않을 경우, 반드시 커스텀 _request()를 구현해야 합니다.
open
Iop::_open 함수
_open 함수는 처음으로 engine() 함수가 호출되기 직전에 실행됩니다. request()가 완료된 이후에 호출되며, engine()과 마찬가지로 워커 스레드에서 실행되기 때문에 UI가 과도하게 지연되는 문제를 방지하면서 시간이 걸리는 작업을 처리할 수 있습니다.
주요 특징
- 첫 engine() 호출 직전에 호출: engine() 함수가 실행되기 직전, 오직 한 번 호출됩니다.
- 싱글 스레드 호출: _open() 함수는 스레드에서 락이 걸린 상태로 한 번만 실행됩니다. 따라서, 동시에 여러 번 실행되지 않으며 첫 호출이 완료된 후에야 다른 engine() 호출이 진행됩니다.
- 재귀적 호출 금지: validate, request, engine 함수와 달리, _open()은 입력 노드를 재귀적으로 호출해서는 안 됩니다.
이 함수는 보통 초기화 작업에 적합하며, 데이터를 준비하거나 연산을 캐싱하는 등의 작업을 수행할 수 있습니다.
사용 예시
일반적으로 _open() 함수는 계산에 필요한 자원이나 데이터를 준비하는 데 사용할 수 있습니다. 예를 들어, 외부 파일을 읽어들이거나 큰 데이터셋을 로드해야 할 때, _open()을 사용하면 작업 효율성을 높이면서도 UI의 응답성을 유지할 수 있습니다.
핵심 정리
- _open()은 engine()이 처음 호출되기 직전의 초기화 작업을 위한 함수입니다.
- UI가 지연되지 않도록 워커 스레드에서 실행되며, 스레드 락을 통해 한 번만 호출됩니다.
- 입력 노드에 대한 재귀 호출을 하지 않아야 합니다.
이 함수를 적절히 활용하면 복잡한 초기화 작업을 효율적으로 수행할 수 있으며, UI의 퍼포먼스 문제를 최소화할 수 있습니다.
engine (/pixel_engine/draw_engine)
Iop::engine 함수
engine 함수는 이미지 데이터를 실제로 처리하는 역할을 맡고 있으며, NUKE에서 워커 스레드를 통해 호출됩니다. 이는 Iop 타입에 따라 다르게 작동합니다. 예를 들어, Iop에는 engine() 함수, PixelIop에는 pixel_engine(), DrawIop에는 draw_engine()이 있습니다. 이 설명은 engine()을 중점으로 설명하지만, 여기서 다루는 내용은 모든 Iop 파생 클래스와 관련된 engine 메서드에 적용됩니다.
함수 동작
- 스레드 안전성: engine()은 워커 스레드에서 호출되므로, 스레드 안전해야 하며, 특히 함수 내부에서 많은 호출이 발생하지 않도록 주의해야 합니다. 또한, knob에 값을 설정하거나 가져오는 함수 호출을 자제해야 합니다. 이러한 호출은 스레드 안전하지 않으며 충돌을 일으킬 수 있습니다. 필요한 계산은 _validate에서 미리 수행하여, 결과를 Iop의 필드로 저장하고 engine()에서는 이를 참조하는 방식이 권장됩니다.
- 파라미터:
- y: 스캔라인의 y 좌표를 나타냅니다.
- l, r: x 좌표의 좌측(l)과 우측(r) 범위를 나타내며, engine()이 작업할 스캔라인의 범위를 지정합니다.
- ChannelMask channels: 처리할 채널 세트를 나타내며, 예를 들어 RGB 채널만을 대상으로 처리할 수 있습니다.
- Row &row: Row 객체는 engine()이 조작해야 하는 데이터의 대상이 되며, 주어진 범위(l에서 r)와 채널 세트를 설정하는 역할을 합니다.
간단한 engine 구현 예시
- 모든 채널을 초기화(블랭킹):
- row.erase(channels);를 사용하여 모든 채널을 지웁니다.
- Row의 초기 상태가 정의되지 않으므로, 명시적으로 블랭킹을 수행해야 합니다.
- void engine(int, int, int, ChannelMask channels, Row& row) { row.erase(channels); }
- 모든 픽셀 값을 1.0으로 채우기:
- writable(Channel)을 사용하여 Row의 내부 버퍼에 접근하고, 해당 채널에 값을 쓸 수 있습니다.
- void engine(int y, int l, int r, ChannelMask channels, Row& row) { foreach(z, channels) { float* out = row.writable(z); for (int x = l ; x < r ; x++) { out[x] = 1.0f; } } }
- 간단한 패스스루 연산:
- 입력으로부터 데이터를 가져와서 Row에 채웁니다. 이는 입력을 단순히 통과시키는 연산에 적합합니다.
- void engine(int y, int l, int r, ChannelMask channels, Row& row) { row.get(input0(), y, l, r, channels); }
- Gain 적용 예시:
- 각 채널에 _gain 값을 곱하여 적용하는 방식입니다. _gain 값은 knob에서 가져온 값이므로 _validate에서 미리 계산하여 저장하고 engine()에서는 이를 참조하는 것이 이상적입니다.
- void engine(int y, int l, int r, ChannelMask channels, Row& row) { row.get(input0(), y, l, r, channels); foreach (z, channels) { if (z == Chan_Red || z == Chan_Blue || z == Chan_Green) { float* out = row.writable(z); for (int x = l ; x < r ; x++) { out[x] *= _gain; // `_gain`은 gain knob의 값 } } } }
추가 팁
- 입력 데이터 처리: 간단한 in-place 연산 방식으로 구현할 수 있으면 메모리 사용을 줄이고 성능을 높일 수 있습니다. 예를 들어, 단순한 패스스루나 간단한 조정 연산의 경우 캐시 성능이 향상됩니다.
- 확장 가능성: Iop의 다양한 하위 클래스 및 헬퍼 클래스는 engine에서 불필요한 보일러플레이트 코드를 줄이고 추가 기능을 제공합니다.
close
_close 함수는 렌더링이 끝난 후 어느 시점에 호출되며, 렌더링이 완료되었을 때 항상 호출되는 것은 아닙니다. NUKE가 추가 작업이 필요하다고 판단할 경우, _close 호출 후에도 다시 open()을 호출할 수 있습니다.
validate, request, engine 함수와 달리 _close는 입력 노드를 재귀적으로 호출할 필요가 없으며, 오히려 입력 노드를 재귀적으로 호출하지 말아야 합니다.
이 함수의 역할은 작업이 끝난 후 리소스를 해제하거나 필요 없는 데이터를 정리하는 것으로 이해할 수 있습니다.
Call Safety
위의 설명에 따르면, engine과 open 호출을 제외한 모든 함수는 메인 또는 UI 스레드에서 호출됩니다. 특히, 메인 스레드에서 호출되는 함수(validate, request, invalidate, UI나 knob 관련 함수)는 가능한 한 빨리 실행되어야 하며, 그렇지 않으면 사용자 인터페이스에 지연이 발생할 수 있습니다.
다음과 같은 규칙들이 있습니다:
- 안전한 스레드 호출: engine 함수나 다른 워커 스레드에서 메인 스레드 함수(validate, request, invalidate, UI 및 knob 함수)를 호출해서는 안 됩니다. 이는 스레드 안전성이 없어서 프로그램의 불안정성을 초래할 수 있습니다.
- 엔진 호출 내에서 안전하게 사용할 수 있는 객체 및 함수:
- Row, Interest, Tile, Pixel 클래스
- ChannelSet 및 기타 채널 관련 함수
- 진행 상황을 알리는 progress 관련 함수들
- error와 warning 함수를 통한 경고 및 오류 메시지 호출
이 지침을 따르면, engine과 open 함수가 스레드 안전성을 유지하면서 효율적으로 이미지를 처리할 수 있습니다.
Formats & Bounding Boxes: RODs & ROIs
_validate() 함수는 Iop의 정의 영역(Region of Definition, ROD)을 픽셀 좌표로 설정하여 이를 IopInfo에 저장하는 역할을 수행합니다. 또한, 전체 이미지 크기 포맷과 다운샘플링/프록시를 고려한 포맷도 계산합니다. 이 작업은 기본적으로 copy_info()를 사용하여 간단히 구현할 수 있으며, 이는 입력의 IopInfo를 복사하는 방식으로 이루어집니다.
void _validate(bool forReal)
{
copy_info(0); // 입력(0)의 정보 복사
}
기본 구현은 여러 입력의 bbox와 채널을 병합하기 위해 merge_info()를 호출하기도 합니다.
NUKE는 _request()를 호출하여 필요한 관심 영역(Region of Interest, ROI)을 전달하는데, 이때 각 입력에 대해 필요한 영역을 요청하게 됩니다. 이를 통해 필요한 처리 범위만큼만 리소스를 사용하도록 합니다.
예를 들어, 블러 효과를 주는 경우 아래와 같이 _request() 함수를 정의할 수 있습니다:
`void _request(int x, int y, int r, int t, ChannelMask channels, int count)
{
// 블러 크기(_size)를 고려하여 입력의 영역을 확대 요청
input(0)->request(x - _size[0], y - _size[1], r + _size[0], t + _size[1], channels, count);
}
NUKE는 자체적으로 request()를 처리할 때 해당 함수가 반환하는 값과 비교하여 요청하는 영역이 Iop의 정의된 bbox를 넘지 않도록 클리핑합니다. 따라서 _validate()에서 설정한 bbox보다 큰 영역을 요청하더라도 안전하게 처리할 수 있습니다.
Coordinate System
NUKE의 좌표계
- 기본 좌표계: NUKE는 픽셀 기반 좌표계를 사용하여 좌측 하단을 (0,0)으로 간주합니다.
- 픽셀 종횡비: 이미지의 픽셀 종횡비는 데이터가 아니라 이미지 포맷의 속성에 의해 결정됩니다.
- 프록시 스케일링: 저해상도 프록시 처리 기능이 내장되어 있으며, 저해상도 모드에서도 올바른 좌표로 보여줍니다.
픽셀 종횡비와 프록시 스케일링
픽셀 종횡비 (Pixel Aspect Ratio)
- 픽셀의 가로와 세로 비율을 의미합니다.
- 예를 들어, 종횡비가 2:1인 경우, 블러 노드의 size 파라미터가 (10, 10)이라면 가로 블러 값은 절반인 (5, 10)으로 자동 조정됩니다.
프록시 스케일링 (Proxy Scaling)
- 프록시 모드는 저해상도에서 미리보기로 작업을 빠르게 할 수 있게 해줍니다.
- 프록시 스케일링이 설정되면, 예를 들어 2배로 축소된 프록시 모드일 경우, size가 (5, 5)로 변경됩니다.
- 픽셀 종횡비와 프록시 스케일링은 곱셈적으로 적용되어, 픽셀 종횡비가 2:1이고 프록시 스케일이 2배라면 최종 size는 (2.5, 5)가 됩니다.
자동 스케일링이 적용되는 Knob 타입
다음의 knob은 픽셀 종횡비와 프록시 스케일을 자동으로 적용하여 좌표나 크기를 조정합니다:
- BBox_knob
- Transform2d_knob
- WH_knob
- XY_knob
필요에 따라 NO_PROXYSCALE 플래그를 설정하여 프록시 스케일링을 끌 수도 있습니다.
직접 스케일링 제어
OutputContext::to_proxy와 OutputContext::from_proxy 함수를 사용하여 특정 값에 대해 수동으로 프록시 스케일링을 적용하거나 해제할 수 있습니다.
Top-Down Rendering
1. Bottom-Up vs Top-Down 렌더링 방식
Bottom-Up (클래식 방식)
- 작동 방식: 'On-Demand Pull' 접근 방식으로, 최종 노드에서 시작해 필요한 데이터를 입력 노드로부터 끌어오는 방식입니다.
- 문제점: 평가 순서가 사전에 명확하지 않아 스케줄링이 비효율적이며, 멀티스레드 사용 시 특정 노드의 캐시 데이터 접근에 대한 스레드 동기화 지연이 발생할 수 있습니다.
Top-Down
- 작동 방식: 'Push' 접근 방식으로, 입력 데이터가 필요하지 않은 노드부터 시작해 데이터를 출력 노드로 "푸시"하며 평가합니다.
- 장점: 캐시된 데이터 접근에 있어 스레드 동기화 오버헤드를 줄여 CPU 스레드의 효율적인 분배가 가능해 전체적인 렌더링 시간을 단축할 수 있습니다.
- 제한사항: 그래프 구조나 노드 연결에 따라 성능이 달라질 수 있습니다. 특히 긴 체인의 단일 입력 노드 연결이 많은 경우 병렬화 기회가 적어 성능이 떨어질 수 있습니다.
2. 렌더링 성능에 영향을 미치는 조건
- 브랜치가 많은 스크립트: 병렬 처리 기회가 많아 Top-Down 방식에서 성능이 향상됩니다.
- ScanlineRender 노드가 포함된 경우: ScanlineRender 노드 위에 위치한 노드들은 여전히 Bottom-Up 방식으로 처리되며, Top-Down 모드에서도 렌더링 성능의 차이가 크지 않을 수 있습니다.
- Pixel Op 후 Spatial Op (예: Grade 뒤에 Blur): Blur 같은 Spatial Op는 필요한 데이터를 캐시에서 미리 가져와 동기화 과정을 줄여 성능이 향상됩니다.
3. Top-Down 렌더링에서의 최적화 기법
- Pixel Op 체인의 버블 처리: Pixel Op (예: Grade, Premult 등) 체인은 Bottom-Up 방식으로 처리하여 CPU의 캐시 성능을 최적화하면서, 이 체인을 Top-Down 그래프에서 병렬로 스케줄링하는 '버블'로 간주하여 효율성을 높입니다.
- Op::OpHints() 메서드 사용: Custom Op를 사용하는 경우, OpHints() 메서드를 구현하여 Top-Down 스케줄링 효율을 높일 수 있습니다. 이는 노드 평가에 필요한 힌트를 제공하여 스케줄링 최적화를 도와줍니다.
4. 메모리 사용량에 대한 고려사항
Top-Down 방식은 빠른 렌더링 속도를 제공하지만, 출력 노드가 데이터를 읽기 시작하기 전 전체 이미지를 생성해 저장해야 하기 때문에 메모리 사용량이 증가할 수 있습니다.
이 요약을 바탕으로, 사용 중인 스크립트에 따라 Top-Down 방식이 더 나은 성능을 보일지 실험해보며 최적의 설정을 찾아볼 수 있습니다.
'Nuke > NDK' 카테고리의 다른 글
NDK - Versioning (0) | 2024.12.07 |
---|---|
NDK - Building & Installing Plug-ins (0) | 2024.12.07 |
NDK - 기본 개념 (0) | 2024.12.07 |
NDK - 용어 (1) | 2024.12.07 |