역자의 말.
제 기억이 맞다면 이 글의 저자는 제가 넷이즈 광저우 스튜디오에 근무 할 당시 메시아 엔진 개발실(넷이즈 자체엔진)에서 이름이 좀 알려진 엔지니어입니다.
이 글을 참고 하여 2021년 하반기에 바이트덴스에서 다시 새롭게 모바일용 FSR 을 개발했던 기억이 있습니다.
최근 AMD는 피델리티FX 슈퍼 해상도(FSR)라는 이름의 슈퍼 해상도 알고리즘을 출시했습니다. 경쟁사인 DLSS가 RTX 지원 카드에서만 활성화할 수 있는 것에 비해 FSR은 특별한 하드웨어 지원이 필요하지 않으므로 크로스 플랫폼 게임에 매우 매력적입니다. 하지만 FSR은 고도로 최적화되어 PC에서 효율적으로 실행되지만 모바일에서는 과부하가 걸립니다. 이 글에서는 이미지 품질을 크게 저하시키지 않으면서도 iPhone 12에서 FSR을 크게 최적화하는 방법을 설명합니다.
FSR 소개
다음은 FSR 알고리즘에 대한 간략한 소개입니다. 자세한 내용은 이 시그그래프 강연에서 확인할 수 있습니다.
FSR은 2패스 알고리즘입니다. 첫 번째 패스인 EASU(에지 적응형 공간 업샘플링)는 업스케일링을 위한 패스입니다. EASU는 다음과 같이 12탭 패턴을 사용합니다:
b c
e f g h
i j k l
n o
이 12개의 탭은 늘어난 란코스 커널을 사용하여 복잡하게 구성됩니다. 스트레치 계수는 주변 픽셀의 그라데이션에서 결정됩니다. 간단히 말해, 최종 모양의 필터는 현재 픽셀의 그라데이션에 수직인 타원형 커널을 주축으로 합니다.
두 번째 패스는 이미지 디테일을 향상시키는 데 사용되는 RCAS(Robust Contract Adaptive Sharpening)라고 합니다. RCAS는 크로스 패턴으로 5개의 탭 샤프닝 연산자를 사용합니다:
w
w 1 w
w
클리핑(0, 1] 범위 초과)을 방지하기 위해 픽셀당 음수 가중치 'w'를 신중하게 선택합니다.
FSR 최적화
저희는 AMD FSR을 엔진에 통합했고, PC에서 훌륭하게 작동했습니다. 좋은 성능과 좋은 이미지 품질, AMD YES!!!. 그런 다음 큰 기대를 가지고 iPhone 12 1에서 테스트했지만 결과는 우리를 많이 실망 시켰습니다. EASU 패스는 약 5.4ms, RCAS는 약 0.9ms가 걸렸습니다 2. 총 6.3ms의 비용은 절대 용납할 수 없는 수준입니다.
최적화할 수 있을까요? RCAS의 비용이 어느 정도 합리적이기 때문에 저희는 EASU 패스에 집중했습니다. 몇 번의 시도 끝에 EASU 비용을 5.4ms에서 1.8ms로 줄이는 데 성공했고, 이는 분명히 큰 성과입니다. 다음은 그 방법을 설명합니다.
1. 절반 정밀도 사용
처음에는 단순성을 위해 fp32 버전의 EASU를 사용했지만 모바일 디바이스에서는 기본적으로 fp16 연산의 처리량이 fp32 연산의 두 배에 달합니다. 셰이더 시스템이 uint16/int16을 지원하지 않기 때문에 컴파일을 위해 원본 FsrEasuH 코드를 약간 수정해야 했습니다. 수정은 간단하지만 이익이 높은 fp16 버전은 4.7ms밖에 걸리지 않습니다.
2. Deringing ( 물결 현상 제거 )?
EASU는 음의 로브가 있는 랜조스 커널을 사용하기 때문에 강한 가장자리를 따라 링잉 아티팩트가 나타날 수 있습니다. 링잉을 제거하기 위해 EASU는 필터링된 결과를 가장 가까운 2x2 쿼드의 최소-최대 값과 비교하여 클립합니다. 이 더링 과정에서 많은 산술 명령어가 생성됩니다. 클리핑 코드를 완전히 제거하려고 시도한 결과, EASU 비용이 4.3ms로 줄어들어 0.4ms가 더 절약되었습니다! 하지만 Ringing(물결현상)은 어때요? 일부 드문 경우를 제외하고는 아티팩트가 거의 눈에 띄지 않는다는 사실에 놀랐고, 저는 그것에 만족합니다.
3. 간소화된 EASU 분석
EASU 패스에는 랜코스 커널의 스트레치 및 회전을 결정하는 분석 단계가 있습니다. 이 단계에서는 분석 루틴 FsrEasuSetH를 4번 호출하고 출력을 2선 보간합니다. 4번 계산하고 출력을 보간하는 대신 입력을 보간하고 FsrEasuSetH를 한 번만 호출할 수 있을까요? 이론적으로는 비선형 함수에 대해 올바른 결과를 얻지 못하지만 시도해 봅시다. 분석 프로세스를 다음과 같이 수정했습니다:
// Compute bilinear weight.
// x y
// z w
AH4 ww = AH4_(0.0);
ww.x =(AH1_(1.0)-ppp.x)*(AH1_(1.0)-ppp.y);
ww.y = ppp.x *(AH1_(1.0)-ppp.y);
ww.z =(AF1_(1.0)-ppp.x)* ppp.y ;
ww.w = ppp.x * ppp.y ;
// Direction is the '+' diff.
// A
// B C D
// E
AH1 lA = dot(ww, AH4(bL, cL, fL, gL));
AH1 lB = dot(ww, AH4(eL, fL, iL, jL));
AH1 lC = dot(ww, AH4(fL, gL, jL, kL));
AH1 lD = dot(ww, AH4(gL, hL, kL, lL));
AH1 lE = dot(ww, AH4(jL, kL, nL, oL));
// Here FsrEasuSetH is inlined as following:
AH1 dc=lD-lC;
AH1 cb=lC-lB;
AH1 lenX=max(abs(dc),abs(cb));
lenX=ARcpH1(lenX);
AH1 dirX=lD-lB;
lenX=ASatH1(abs(dirX)*lenX);
lenX*=lenX;
// Repeat for the y axis.
AH1 ec=lE-lC;
AH1 ca=lC-lA;
AH1 lenY=max(abs(ec),abs(ca));
lenY=ARcpH1(lenY);
AH1 dirY=lE-lA;
lenY=ASatH1(abs(dirY)*lenY);
AH1 len = lenY * lenY + lenX;
AH2 dir = AH2(dirX, dirY);
이는 매우 무례한 단순화로, EASU 비용이 3.8ms에 이릅니다. 하지만 이렇게 하면 업스케일링 품질이 떨어질까요? 별로 그렇지 않습니다! 가장자리 기능은 여전히 매우 잘 보존되어 이전과 이후의 차이를 거의 구분할 수 없습니다.
4. 조기 종료
가장자리가 아닌 픽셀의 경우 원래 EASU는 여전히 12개의 탭을 모두 누적합니다. 이 경우 최적화를 위한 직관적인 아이디어는 간단한 2선형 보간을 사용하는 것입니다:
AH2 dir2=dir*dir;
AH1 dirR=dir2.x+dir2.y;
if (dirR<AH1_(1.0/64.0)) {
// Early quit for non-edge pixels
pix.r = dot(ww, AH4(fR, gR, jR, kR));
pix.g = dot(ww, AH4(fG, gG, jG, kG));
pix.b = dot(ww, AH4(fB, gB, jB, kB));
return;
}
대부분의 스레드 그룹이 이 조기 종료 브랜치에 들어가기 때문에 비용이 많이 절감되어야 합니다. 하지만 프로파일링 후 0.2ms 개선에 그쳤습니다. ALU 압력은 급격히 감소했지만 GPU 카운터는 이제 셰이더가 완전히 텍스처에 바인딩되었음을 보여줍니다.
5. 텍스처 가져오기 감소
따라서 TEX 작업을 최적화해야 합니다. 조기 종료 코드 경로에 집중해 보겠습니다: 이 경로에서는 이 픽셀이 비에지 케이스에 속하도록 하기 위해 5개의 선형 보간된 루마만 필요합니다. 이 5개의 탭을 바이리니어 샘플러를 사용하여 직접 샘플링하고 다른 탭은 실제로 필요할 때까지 연기하는 것은 어떨까요? 이렇게 하면 조기 종료 경로에 대한 텍스처 페치를 절약할 수 있을 뿐만 아니라 수동으로 이중 선형 보간을 수행하는 일부 ALU도 절약할 수 있습니다.
AF2 pp=(ip)*(con0.xy)+(con0.zw);
AF2 tc=(pp+AF2_(0.5))*con1.xy;
AH3 sA=FsrEasuSampleH(tc-AF2(0, con1.y));
AH3 sB=FsrEasuSampleH(tc-AF2(con1.x, 0));
AH3 sC=FsrEasuSampleH(tc);
AH3 sD=FsrEasuSampleH(tc+AF2(con1.x, 0));
AH3 sE=FsrEasuSampleH(tc+AF2(0, con1.y));
AH1 lA=sA.r*AH1_(0.5)+sA.g;
AH1 lB=sB.r*AH1_(0.5)+sB.g;
AH1 lC=sC.r*AH1_(0.5)+sC.g;
AH1 lD=sD.r*AH1_(0.5)+sD.g;
AH1 lE=sE.r*AH1_(0.5)+sE.g;
이 변경으로 성능이 크게 향상되어 EASU 비용이 3.6ms에서 1.8ms로 감소합니다. 이제 FSR의 총 비용은 1.8ms(EASU) + 0.9ms(RCAS) = 2.7ms입니다. FSR이 없어도 오프스크린 타겟을 백 버퍼로 복사하려면 약 0.7ms가 소요되는 패스가 필요하다는 점을 잊지 마세요. 따라서 최적화된 FSR의 순 비용은 2.7ms - 0.7ms = 2.0ms입니다. 매우 효율적이지는 않지만 만족할 만한 수준입니다.
소스 코드
자, 여기까지입니다. 전체 소스 코드는 아래(요점 링크)에 나와 있으니 유용하게 사용하시기 바랍니다. 또한 공식 FSR 데모를 기반으로 샘플을 만들었으니 직접 플레이해보고 최적화된 버전과 원본의 품질을 비교해 보실 수 있습니다. 궁금한 점이 있거나 추가 최적화를 위한 다른 아이디어가 있으시면 주저하지 마시고 의견을 남겨주시기 바랍니다.
//==============================================================================================================================
// An optimized AMD FSR's EASU implementation for Mobiles
// Based on https://github.com/GPUOpen-Effects/FidelityFX-FSR/blob/master/ffx-fsr/ffx_fsr1.h
// Details can be found: https://atyuwen.github.io/posts/optimizing-fsr/
// Distributed under the MIT License. Copyright (c) 2021 atyuwen.
// -- FsrEasuSampleH should be implemented by calling shader, like following:
// AH3 FsrEasuSampleH(AF2 p) { return MyTex.SampleLevel(LinearSampler, p, 0).xyz; }
//==============================================================================================================================
void FsrEasuL(
out AH3 pix,
AF2 ip,
AF4 con0,
AF4 con1,
AF4 con2,
AF4 con3){
//------------------------------------------------------------------------------------------------------------------------------
// Direction is the '+' diff.
// A
// B C D
// E
AF2 pp=(ip)*(con0.xy)+(con0.zw);
AF2 tc=(pp+AF2_(0.5))*con1.xy;
AH3 sA=FsrEasuSampleH(tc-AF2(0, con1.y));
AH3 sB=FsrEasuSampleH(tc-AF2(con1.x, 0));
AH3 sC=FsrEasuSampleH(tc);
AH3 sD=FsrEasuSampleH(tc+AF2(con1.x, 0));
AH3 sE=FsrEasuSampleH(tc+AF2(0, con1.y));
AH1 lA=sA.r*AH1_(0.5)+sA.g;
AH1 lB=sB.r*AH1_(0.5)+sB.g;
AH1 lC=sC.r*AH1_(0.5)+sC.g;
AH1 lD=sD.r*AH1_(0.5)+sD.g;
AH1 lE=sE.r*AH1_(0.5)+sE.g;
// Then takes magnitude from abs average of both sides of 'C'.
// Length converts gradient reversal to 0, smoothly to non-reversal at 1, shaped, then adding horz and vert terms.
AH1 dc=lD-lC;
AH1 cb=lC-lB;
AH1 lenX=max(abs(dc),abs(cb));
lenX=ARcpH1(lenX);
AH1 dirX=lD-lB;
lenX=ASatH1(abs(dirX)*lenX);
lenX*=lenX;
// Repeat for the y axis.
AH1 ec=lE-lC;
AH1 ca=lC-lA;
AH1 lenY=max(abs(ec),abs(ca));
lenY=ARcpH1(lenY);
AH1 dirY=lE-lA;
lenY=ASatH1(abs(dirY)*lenY);
AH1 len = lenY * lenY + lenX;
AH2 dir = AH2(dirX, dirY);
//------------------------------------------------------------------------------------------------------------------------------
AH2 dir2=dir*dir;
AH1 dirR=dir2.x+dir2.y;
if (dirR<AH1_(1.0/64.0)) {
pix = sC;
return;
}
dirR=ARsqH1(dirR);
dir*=AH2_(dirR);
len=len*AH1_(0.5);
len*=len;
AH1 stretch=(dir.x*dir.x+dir.y*dir.y)*ARcpH1(max(abs(dir.x),abs(dir.y)));
AH2 len2=AH2(AH1_(1.0)+(stretch-AH1_(1.0))*len,AH1_(1.0)+AH1_(-0.5)*len);
AH1 lob=AH1_(0.5)+AH1_((1.0/4.0-0.04)-0.5)*len;
AH1 clp=ARcpH1(lob);
//------------------------------------------------------------------------------------------------------------------------------
AF2 fp=floor(pp);
pp-=fp;
AH2 ppp=AH2(pp);
AF2 p0=fp*(con1.xy)+(con1.zw);
AF2 p1=p0+(con2.xy);
AF2 p2=p0+(con2.zw);
AF2 p3=p0+(con3.xy);
p0.y-=con1.w; p3.y+=con1.w;
AH4 fgcbR=FsrEasuRH(p0);
AH4 fgcbG=FsrEasuGH(p0);
AH4 fgcbB=FsrEasuBH(p0);
AH4 ijfeR=FsrEasuRH(p1);
AH4 ijfeG=FsrEasuGH(p1);
AH4 ijfeB=FsrEasuBH(p1);
AH4 klhgR=FsrEasuRH(p2);
AH4 klhgG=FsrEasuGH(p2);
AH4 klhgB=FsrEasuBH(p2);
AH4 nokjR=FsrEasuRH(p3);
AH4 nokjG=FsrEasuGH(p3);
AH4 nokjB=FsrEasuBH(p3);
//------------------------------------------------------------------------------------------------------------------------------
// This part is different for FP16, working pairs of taps at a time.
AH2 pR=AH2_(0.0);
AH2 pG=AH2_(0.0);
AH2 pB=AH2_(0.0);
AH2 pW=AH2_(0.0);
FsrEasuTapH(pR,pG,pB,pW,AH2( 1.0, 0.0)-ppp.xx,AH2(-1.0,-1.0)-ppp.yy,dir,len2,lob,clp,fgcbR.zw,fgcbG.zw,fgcbB.zw);
FsrEasuTapH(pR,pG,pB,pW,AH2(-1.0, 0.0)-ppp.xx,AH2( 1.0, 1.0)-ppp.yy,dir,len2,lob,clp,ijfeR.xy,ijfeG.xy,ijfeB.xy);
FsrEasuTapH(pR,pG,pB,pW,AH2( 0.0,-1.0)-ppp.xx,AH2( 0.0, 0.0)-ppp.yy,dir,len2,lob,clp,ijfeR.zw,ijfeG.zw,ijfeB.zw);
FsrEasuTapH(pR,pG,pB,pW,AH2( 1.0, 2.0)-ppp.xx,AH2( 1.0, 1.0)-ppp.yy,dir,len2,lob,clp,klhgR.xy,klhgG.xy,klhgB.xy);
FsrEasuTapH(pR,pG,pB,pW,AH2( 2.0, 1.0)-ppp.xx,AH2( 0.0, 0.0)-ppp.yy,dir,len2,lob,clp,klhgR.zw,klhgG.zw,klhgB.zw);
FsrEasuTapH(pR,pG,pB,pW,AH2( 0.0, 1.0)-ppp.xx,AH2( 2.0, 2.0)-ppp.yy,dir,len2,lob,clp,nokjR.xy,nokjG.xy,nokjB.xy);
AH3 aC=AH3(pR.x+pR.y,pG.x+pG.y,pB.x+pB.y);
AH1 aW=pW.x+pW.y;
//------------------------------------------------------------------------------------------------------------------------------
pix=aC*AH3_(ARcpH1(aW));}
iPhone 12의 기본 해상도는 2532 x 1170이며 여기서 사용한 렌더링 해상도는 0.7배, 즉 1772 x 819입니다. ︎
컴퓨팅 셰이더가 아닌 픽셀 셰이더에 EASU와 RCAS를 모두 통합했습니다.
원문.
https://atyuwen.github.io/posts/optimizing-fsr/
'TECH.ART.FLOW.IO' 카테고리의 다른 글
[소식][번역]URP 17로의 업그레이드와 Render Graph 활용 방법 (1) | 2024.10.06 |
---|---|
[소식] 유나이트 2024 하이라이트 게임 개발의 도구, 강연 및 변화 (17) | 2024.09.27 |
[소식] 유니티 2024 테크데모 공개. (2) | 2024.09.20 |
[번역] 언리얼 렌더링 시스템 해부하기(10-1)- RHI (0) | 2024.09.19 |
[번역] 언리얼 렌더링 시스템 해부하기(11)- RDG (1) | 2024.09.14 |