웹에서 최대 FPS : WebRender가 끊김(jank)을 없애는 방법


원문 : https://hacks.mozilla.org/2017/10/the-whole-web-at-maximum-fps-how-webrender-gets-rid-of-jank/

Firefox Quantum 릴리즈가 가까워지고 있다. 이번 릴리즈에서는 Servo에서 가져온 초고속 CSS 엔진을 포함하여 많은 성능 개선이 있다.

그리고 곧 출시 예정인 Firefox Quantum에 없는 또 다른 커다란 Servo 기술이 있다. 그것은 WebRender로, Quantum Render 프로젝트의 부분으로써 Firefox에 추가될 것이다.

WebRender는 매우 빠르다고 알려져 있다. 그러나 WebRender는 렌더링 속도를 높이는 것이 아니다. 더 부드럽게 만드는 것이다.

WebRender를 사용하면, 디스플레이가 얼마나 큰지 또는 프레임당 얼마나 많은 페이지가 변경되는지에 관계 없이 초당 60 프레임(60 FPS) 또는 그 이상으로 부드럽게 앱을 실행할 수 있다. Chrome 또는 현재 Firefox에서 15 FPS로 버벅거리던 페이지가 WebRender를 통해 60 FPS로 실행된다.

그래서 WebRender는 어떻게 동작하는가? WebRender는 렌더링 엔진 동작을 3D 게임 엔진처럼 만들도록 변경시킨다.

이것이 무엇을 의미하는지 살펴 보겠다. 하지만 먼저...

렌더러는 무슨 일을 하는가?

Stylo의 아티클에서, 브라우저가 HTML과 CSS를 화면 픽셀로 바꾸는 방식과 대부분의 브라우저가 5단계로 이것을 수행하는 방식에 대해 설명했었다.

우리는 이 5단계를 2단계로 나눌 수 있다. 1단계에서는 기본적으로 계획을 수립한다. 이 계획을 세우기 위해, HTML 및 뷰포트 크기와 같은 CSS 정보와 결합하여 너비(width), 높이(height), 색상 등 각 요소(element)의 모양을 정확하게 파악한다. 만들어진 최종 결과를 프레임 트리(frame tree) 또는 렌더 트리(render tree)라고 부른다.

렌더러 동작의 2단계는 페인팅(painting)과 합성(compositing)이다. 이 단계에서는 전 단계에서 수립한 계획을 가져와서 화면에 표시하기 위해 픽셀로 전환한다.

그러나 브라우저는 웹 페이지에서 이 작업을 한 번만 수행하면 안된다. 동일한 웹 페이지에 대해서 반복해서 수행해야 한다. div의 열림 상태가 토글(toggle)되는 것과 같이 페이지에서 무언가 바뀔 때마다 브라우저는 이 단계들을 반복적으로 거치게 된다.

스크롤을 하거나 텍스트를 강조할 때처럼 페이지에서 실제로 아무 것도 변경되지 않는 경우에도, 브라우저는 여전히 화면에 새로운 픽셀을 그리기 위해 2단계 일부를 다시 통과해야 한다.

스크롤링이나 애니메이션과 같은 것들을 부드럽게 보이게 하려면, 초당 60 프레임이 필요하다.

초당 프레임(FPS)에 대해서 들어보았을 것이다. 나는 이것을 플립 북이라고 생각한다. 정적인 그림책 같지만 엄지 손가락을 사용하여 넘기면 페이지의 그림이 움직이는 것처럼 보인다.

이 플립 북의 애니메이션을 매끄럽게 보이게 하려면, 애니메이션이 발생하는 매초마다 60 페이지가 있어야 한다.

이 플립 북의 페이지는 그래프 용지로 만들어져 있다. 작은 사각형들이 많이 있고, 각 사각형은 오직 하나의 색만 채울 수 있다.

렌더러의 역할은 이 그래프 용지의 상자를 채우는 것이다. 그래프 용지의 모든 상자가 채워지면, 프레임 렌더링이 끝난다.

컴퓨터 내부에는 실제 그래프 용지가 없다. 대신 프레임 버퍼라는 메모리 섹션이 있다. 프레임 버퍼의 각 메모리 주소는 그래프 용지의 상자와 같다. 이것은 화면 상의 1개 픽셀에 해당한다. 브라우저는 RGBA(빨강, 녹색, 파랑, 알파) 값의 색상을 나타내는 숫자로 각 칸(슬롯)을 채운다.

디스플레이가 새로 고침될 때, 이 메모리 섹션을 보아야할 것이다.

대부분의 컴퓨터 디스플레이는 초당 60번으로 새로 고침된다. 이것이 브라우저가 초당 60 프레임으로 페이지를 렌더링 하려는 이유이다. 브라우저는 CSS 스타일링, 레이아웃, 페인팅과 같은 모든 설정을 수행하고 픽셀 색상들로 프레임 버퍼의 슬롯을 채우기 위해 16.67ms를 유지해야 한다. 이 두 프레임 사이의 시간(16.67ms) 프레임을 프레임 예산(frame budget)이라고 한다.

때때로 프레임이 떨어진다고 말하는 것을 들어보았을 것이다. 드롭된 프레임(dropped frame)이라고 하며, 프레임 예산 내에서 프레임 작업이 완료되지 않는 경우이다. 디스플레이는 브라우저가 프레임 버퍼를 채우기 전에 프레임 버퍼에서 새 프레임을 가져오려고 시도한다. 드롭된 프레임이 발생하는 경우, 디스플레이에 이전 버전의 프레임이 다시 표시된다.

드롭된 프레임은 플립 북에서 페이지를 찢어놓은 것과 같다. 이전 페이지와 다음 페이지 사이의 전환이 누락되었기 때문에 애니메이션이 버벅거리거나 점프하는 것처럼 보일 수 있다.

따라서 디스플레이가 다시 확인하기 전에 이러한 모든 픽셀을 프레임 버퍼에 넣어야 한다. 브라우저가 역사적으로 이것을 어떻게 했는지, 그리고 시간이 지남에 따라 브라우저가 어떻게 변했는지 살펴보자.

페인팅과 합성에 대한 간략한 역사

참고 : 브라우저 렌더링 엔진간에 가장 차이점이 두드러지는 부분은 페인팅과 합성이다. 단일 플랫폼 브라우저(Edge 및 Safari)는 멀티 플랫폼 브라우저 (Firefox 및 Chrome)와 조금 다르게 동작한다.

초기 브라우저에서도 페이지 렌더링 속도를 높이기 위한 최적화가 있었다. 예를 들어 콘텐츠를 스크롤하는 경우, 브라우저는 보이는 부분을 유지하면서 콘텐츠를 이동시킨다. 그런 다음 빈 자리에 새 픽셀을 칠한다.

변경된 내용을 파악하고 변경된 요소 또는 픽셀만 업데이트하는 과정을 무효화(invalidation)라고 한다.

시간이 지남에 따라 브라우저는 사각형 무효화(rectangle invalidation)와 같은 더 많은 무효화 기법을 적용하기 시작했다. 사각형 무효화를 사용하면, 변경된 화면의 주위에서 가장 작은 사각형을 찾은 다음 그 사각형 안에 있는 것만 다시 그린다.

이렇게 하면 페이지에서 변경이 많지 않을 때 필요로 하는 작업량을 줄일 수 있다. 커서가 깜박이는 경우를 예로 들 수 있다.

하지만 페이지의 큰 부분이 변경될 때는 별로 도움이 되지 않는다. 그래서 브라우저는 이러한 경우를 처리할 수 있는 새로운 기술들과 함께 해왔다.

레이어와 합성 소개

레이어를 사용하면 페이지의 큰 부분이 변경될 때 굉장히 많은 도움이 된다.

브라우저의 레이어는 포토샵 또는 손으로 그린 ​​애니메이션에서 사용되는 어니언 스킨(onion skin) 레이어와 비슷하다. 기본적으로 레이어마다 페이지의 다른 요소를 그린 다음 이 레이어들을 서로 위에 올려놓는다.

이 레이어들은 오랫동안 브라우저의 일부였지만 속도를 높이는데는 사용되지 않았다. 처음에는 페이지를 올바르게 렌더링 하는데만 사용되었다. 레이어들을 스태킹 컨텍스트(stacking context)와 대응한다.

예를 들어 투명한 요소가 있는 경우, 자체 스태킹 컨텍스트 안에 포함된다. 즉, 자신의 레이어 색상을 그 아래 레이어의 색상과 혼합할 수 있다. 이 레이어들은 프레임이 완성되자마자 버려지고, 다음 프레임에서 모든 레이어를 다시 칠한다.

그러나 레이어들의 내용이 프레임마다 변경되지 않을 때도 있다. 예를 들어 전통적인 애니메이션을 생각해보자. 전경(foreground)의 문자가 변경되더라도 배경(background)은 변경되지 않는다. 배경 레이어를 유지하고 다시 사용하는 것이 훨씬 효율적이다.

브라우저가 그렇다. 브라우저는 레이어들을 유지한다. 그러면 브라우저는 변경된 레이어들만 다시 칠할 수 있다. 레이어가 변경되지 않는 경우에는 단지 정렬만 변경하면 된다. 예를 들어 애니메이션이 화면을 가로질러 움직이거나 무언가가 스크롤되는 경우이다.

레이어를 함께 정렬하는 과정을 합성이라고 한다. 컴포지터(compositor)는 다음으로 구성된다.

  • 소스 비트맵(source bitmaps) : 배경(스크롤 가능한 콘텐츠가 있는 빈 상자 포함) 및 스크롤 가능한 콘텐츠 자체
  • 화면에 표시되는 대상 비트맵(a destination bitmap)

먼저, 컴포지터는 대상 비트맵에 배경을 복사한다.

그런 다음 스크롤 할 수 있는 콘텐츠의 어떤 부분을 표시해야 하는지 파악한다. 해당 부분을 대상 비트맵으로 복사한다.

이렇게 하면 메인 스레드가 수행해야 하는 페인트 작업 양이 줄어든다. 하지만 여전히 메인 스레드가 합성 작업에 많은 시간을 소비하고 있다. 그리고 메인 스레드에서 시간을 두고 경쟁하는 많은 것들이 있다.

이전에 이야기 했었지만, 메인 스레드는 풀 스택 개발자와 비슷하다. DOM, 레이아웃 및 자바스크립트 실행을 담당한다. 또한 페인팅과 합성도 담당한다.

메인 스레드가 페인트 및 합성 작업을 하는데 소비하는 시간에는 자바스크립트 또는 레이아웃을 실행될 수 없다.

이러한 문제를 해결할 수 있는 하드웨어가 있다. 이 하드웨어는 그래픽용으로 특별히 제작되었다. GPU라고 하며 90년대 후반부터 프레임을 빠르게 렌더링하기 위해 게임에서 사용되었다. 이후 GPU는 점점 커지고 더 강력해지고 있다.

GPU 가속 합성

그래서 브라우저 개발자들은 여러 작업들을 GPU로 옮기기 시작했다.

GPU로 옮길 수 있는 두 가지 작업이 있다.

  1. 레이어 페인팅
  2. 합성

페인팅을 GPU로 옮기는 것은 어려울 수 있다. 그래서 대부분의 멀티 플랫폼 브라우저는 CPU에서 페인팅을 유지하고 있다.

그러나 합성은 GPU가 매우 빠르게 할 수 있고 옮기기도 쉬웠다.

일부 브라우저는 이 병렬 처리를 더 많이 사용했고 CPU에 컴포지터 스레드를 추가했다. 이 컴포지터 스레드는 GPU에서 발생한 합성 작업의 관리자가 되었다. 자바스크립트 실행과 같이 메인 스레드가 무언가를 하고 있다면, 컴포지터 스레드는 사용자가 스크롤할 때 콘텐츠가 스크롤되는 것처럼 사용자를 위한 것들을 처리하게 된다.

모든 합성 작업을 메인 스레드로 옮겼다. 그래도 여전히 메인 스레드에는 많은 작업이 남아 있다. 레이어를 다시 칠할 필요가 있을 때마다, 메인 스레드가 작업을 수행한 다음 해당 레이어를 GPU로 전송한다.

일부 브라우저는 페인팅 작업을 다른 스레드(현재 Firefox에서 작업 중이다)로 옮겼다. 그러나 페인팅과 같은 일부 작업은 GPU로 옮기는 것이 훨씬 빠르다.

GPU 가속 페인팅

그래서 브라우저들은 페인팅 작업도 GPU로 옮기기 시작했다.

브라우저는 여전히 이러한 이동 과정을 겪고 있다. 일부 브라우저는 항상 GPU에서 페인트를 하기도 하며, Windows 또는 모바일 장치처럼 특정 플랫폼에서만 GPU에 페인트하기도 한다.

GPU에서 페인팅하는 것은 몇 가지 이유가 있다. 자바스크립트 및 레이아웃과 같은 일을 처리하는데 CPU를 낭비하지 않아도 된다. 또한 GPU는 CPU보다 픽셀을 그리는데 더 빠르기 때문에 페인팅 속도가 빠르다. 이것은 CPU에서 GPU로 복사해야 하는 데이터가 적다는 것을 의미한다.

그러나 페인트와 합성 사이에 이러한 구분을 유지하는 것은, 페인트와 합성이 둘 다 GPU에 있을 때처럼 여전히 비용이 든다. 또한 GPU의 작업 속도를 높이는데 사용할 수 있는 최적화의 종류도 제한된다.

이것이 WebRender가 나오게 된 이유이다. WebRender는 근본적으로 렌더링 방식을 바꾸고, 페인트와 합성 작업의 구분을 없앤다. WebRender는 렌더러의 성능을 조정하여 오늘날의 웹에서 최상의 사용자 경험을 제공하고, 미래의 웹에서 볼 수 있는 사용 사례를 가장 잘 지원할 수 있다.

프레임을 단지 더 빠르게 렌더링하려는 게 아니다... 즉, 끊김(jank) 없이 더 일관되게 렌더링하길 원한다. 그리고 4k 디스플레이나 WebVR 헤드셋처럼 많은 픽셀을 그릴 때 조차도 우리는 여전히 부드럽고 멋진 경험을 원한다.

브라우저에서 끊김이 언제 발생하는가?

위의 최적화를 통해 특정 경우에 페이지 렌더링 속도가 빨라졌다. 예를 들어 깜박이는 커서가 하나만 있을 때 처럼 페이지에서 변하는 것이 많지 않다면, 브라우저는 가능한 한 최소한의 작업량만 수행할 것이다.

페이지를 레이어로 분리하면 최적의 시나리오가 늘어난다. 몇 개의 레이어를 칠한 다음 서로 상대적으로 움직일 수 있다면, 페인팅+합성 아키텍처가 잘 동작할 것이다.

레이어들을 사용하는 것은 장점만 있는 것이 아니라 단점도 있다. 레이어들은 많은 메모리를 차지하고 실제로 작업을 더 느리게 만들 수 있다. 브라우저는 합당한 곳에서 레이어들의 결합을 필요로 하지만 그 장소를 알려주는 것은 어렵다.

즉, 페이지에서 서로 다른 것들이 많이 움직이면, 그냥 많은 레이어들로 끝날 수 있다. 이 레이어들은 메모리를 가득 채우며 컴포지터로 전송되는데 오랜 시간이 걸린다.

다른 경우, 여러 레이어가 있어야 할 때 하나의 레이어만 존재할 것이다. 이 단일 레이어는 계속해서 다시 칠해지고 컴포지터로 전송될 것이다. 그리고 이 컴포지터는 어떤 변화도 없이 단일 레이어를 합성한다.

이것은 당신이 그려야될 작업량을 두 배로 늘리고, 어떤 이점도 없이 각 픽셀을 두 번 건드리는 것을 의미한다. 합성 단계 없이 직접 페이지를 렌더링하는 것이 더 빠를 것이다.

레이어가 도움이 되지 않는 경우가 많이 있다. 예를 들어, 배경색을 애니메이션으로 만들면 어쨌든 전체 레이어를 다시 칠해야 한다. 이 레이어는 적은 양의 CSS 속성들만으로도 처리할 수 있다.

대부분의 프레임 예산을 약간만 차지하는 경우와 같이 프레임이 최상의 시나리오 일지라도 고르지 못한 동작을 얻을 수 있다. 단지 몇 개의 프레임이 최악의 시나리오로 떨어지더라도 인식할 수 있는 끊김이 발생할 수 있다.

이러한 시나리오를 성능 절벽(performance cliff)이라고 한다. 당신의 앱은 배경색 애니메이션 같은 최악의 시나리오에 부딪히기 전까지 잘 움직이는 것처럼 보이다가 앱의 프레임 속도가 급격하게 떨어질 것이다.

그러나 우리는 이 성능 절벽을 제거할 수 있다.

이것을 어떻게 하는가? 우리는 3D 게임 엔진을 따른다.

게임 엔진과 같은 GPU 사용

우리가 필요한 레이어들을 판별하는 것을 멈춘다면 어떻게 될까? 페인팅과 합성의 경계를 없애고 모든 프레임의 모든 픽셀을 페인팅하는 것으로 돌아간다면 어떨까?

어리석은 생각처럼 들리겠지만 실제로 몇 가지 전례가 있다. 현대 비디오 게임은 모든 픽셀을 다시 칠하고, 초당 60 프레임을 브라우저보다 더 안정적으로 유지한다. 그리고 예상하지 못한 방식... 즉, 무효화 사각형과 레이어를 생성하는 대신 페인트 작업을 최소화하여 전체 화면을 다시 칠한다.

더 느린 웹 페이지를 렌더링하지는 않을까?

우리가 CPU에 칠하면 그렇게 될 것이다. 그러나 GPU는 이 작업을 수행하도록 설계되었다.

GPU는 극단적인 병렬 처리를 위해 제작되었다. Stylo에 대한 지난 아티클에서 병렬 처리에 대해 이야기 했었다. 병렬 처리를 사용하면, 기계가 동시에 여러 작업을 수행할 수 있다. 기계가 한 번에 할 수 있는 일의 수는 가지고 있는 코어(core)의 수에 의해 제한된다.

CPU의 보통 2~8개 코어를 가진다. GPU는 일반적으로 최소 수백 코어 이상, 종종 1,000 코어 이상을 가진다.

하지만 GPU의 코어는 약간 다르게 동작한다. CPU 코어처럼 완전히 독립적으로 동작할 수는 없다. 대신, 데이터의 다른 부분에서 같은 명령어를 실행하면서 함께 동작한다.

이것은 정확하게 픽셀을 채울 때 필요한 것이다. 각 픽셀은 다른 코어로 채울 수 있다. 한 번에 수백 개의 픽셀을 처리할 수 ​​있기 때문에, GPU는 CPU보다 더 빠르게 픽셀을 채운다. 그러나 모든 코어가 수행해야 할 작업이 있는지 확인된 경우에만 작업이 가능하다.

코어는 같은 시간에 동일한 작업을 해야하기 때문에, GPU는 매우 엄격한 단계를 거치며 API도 상당히 제한적이다. 어떻게 동작하는지 살펴보자.

먼저, GPU에 무엇을 그려야 하는지 알려주어야 한다. 이는 GPU에 도형을 제공하고 어떻게 채울지 방법을 알려주는 것을 의미한다.

이렇게 하려면 당신이 그린 그림을 간단한 도형(보통 삼각형)으로 나눈다. 이 도형은 3D 공간에 있으므로 일부 도형이 다른 도형 뒤에 있을 수 있다. 그런 다음 이 삼각형의 모서리를 모두 가져와서 배열에 x, y 및 z 좌표 정보를 넣는다.

그리고 드로우콜(draw call)을 발생한다. 드로우콜은 GPU에 위 도형들을 그리라고 말하는 것이다.

이 때 GPU가 받는다. 모든 코어는 같은 시간에 동시에 작동한다. 코어들은 이렇게 할 것이다.

  1. 도형의 모서리가 있는 곳을 모두 찾아낸다. 이를 버텍스 셰이딩(vertex shading)이라고 한다.

  1. 모서리를 연결하는 선을 그린다. 이렇게 하면, 픽셀이 어떤 도형으로 덮여있는지 알 수 있다. 이것을 레스터화(rasterization)라고 한다.

  1. 이제 도형으로 어떤 픽셀이 덮여있는지 알았으니, 도형 안의 각 픽셀을 통해 어떤 색상이 사용되었는지도 알 수 있다. 이것을 픽셀 셰이딩(pixel shading)이라고 한다.

마지막 단계는 다른 방법으로 수행될 수 있다. GPU에게 어떻게 하는지 알려주기 위해서, GPU에 픽셀 셰이더(pixel shader)라는 프로그램을 제공한다. 픽셀 셰이딩은 프로그래밍할 수 있는 GPU의 몇 안되는 부분 중 하나이다.

일부 픽셀 셰이더는 간단하다. 예를 들어 도형이 단일 색상인 경우, 셰이더 프로그램은 도형의 각 픽셀에 대한 색상을 반환하면 된다.

배경 이미지가 있을 때와 같은 경우에는 복잡하다. 이미지의 어느 부분이 각 픽셀에 해당하는지 파악해야 한다. 아티스트가 위, 아래로 이미지의 크기를 조정하는 것과 같이 이 작업을 수행할 수 있다. 각 픽셀에 해당하는 이미지 위에 격자를 놓는다. 그리고 어떤 상자가 픽셀에 해당하는지 알게 되었을 때, 해당 상자 안의 색상 샘플을 가져와서 색상을 알아내야 한다. 텍스처(texture)라고 불리는 이미지를 픽셀에 매핑하기 때문에, 이를 텍스처 매핑(texture mapping)이라고 한다.

GPU는 각 픽셀에서 픽셀 셰이더 프로그램을 호출한다. 서로 다른 코어가 동시에 다른 픽셀에서 동기적으로 작동하지만, 동일한 픽셀 셰이더 프로그램을 사용해야 한다. GPU에 도형을 그리도록 지시할 때, 사용할 픽셀 셰이더를 지정해야 한다.

거의 모든 웹 페이지에서, 페이지의 다른 부분들은 서로 다른 픽셀 셰이더를 사용해야 한다.

셰이더는 드로우콜에서 모든 도형에 적용되기 때문에, 종종 여러 그룹의 드로우콜을 분리해야 한다. 이것을 일괄 처리(batches)라고 한다. 가능한 한 모든 코어를 사용 중인 상태로 유지하기 위해서 많은 도형을 가지는 일괄 처리들을 소량으로 생성해야 한다.

이것이 GPU가 수백 또는 수천 개의 코어를 통해서 작업을 나누는 방법이다. 우리가 각 프레임에서 모든 것을 렌더링한다고 생각할 수 있는 것은 극단적인 병렬 처리 때문이다. 극단적인 병렬 처리를 사용하더라도 여전히 많은 작업이 있기에, 어떻게 이것을 해야할지 대해 현명해져야 한다. 여기에 WebRender가 있다...

WebRender가 GPU와 함께 동작하는 방법

브라우저가 페이지를 렌더링하는 단계를 다시 살펴보겠다. 여기서 두 가지가 변할 것이다.

  1. 페인트와 합성 사이에 더 이상 구별이 없다. 즉, 둘 다 같은 단계의 일부이다. GPU는 전달된 그래픽스 API 명령을 기반으로 동시에 페인트와 합성을 수행한다.
  2. 레이아웃은 렌더링을 위해 다른 데이터 구조를 제공한다. 이전에 프레임 트리(또는 Chrome에서 렌더 트리)라는 것이 있었다. 이제 이것은 디스플레이 목록(display list)에서 사라진다.

디스플레이 목록은 고수준의 드로잉 명령어 세트다. 어떤 그래픽스 API에도 구애받지 않고 그릴 것들을 알려준다.

새로 그릴 것이 있을 때마다 메인 스레드는 CPU에서 실행되는 WebRender 코드인 RenderBackend에 디스플레이 리스트를 제공한다.

RenderBackend의 역할은 고수준의 드로잉 명령어 목록을 가져와 GPU에 필요한 드로우콜로 변환하는 것이다. 이 드로잉 명령어는 함께 일괄 처리되어 더 빠르게 실행된다.

그런 다음 RenderBackend는 해당 일괄 처리들을 컴포지터 스레드로 전달하여 GPU로 전달한다.

RenderBackend는 가능한 한 빠르게 실행되도록 GPU에 전달하는 드로우콜을 만들기를 원한다. 이 작업에는 몇 가지 다른 기술이 사용된다.

목록에서 불필요한 도형 제거 (초기 선별)

시간을 절약하는 가장 좋은 방법은 작업을 하지 않는 것이다.

먼저 RenderBackend가 디스플레이 항목을 축소한다. 실제로 어떤 디스플레이 항목이 화면에 나타날지 파악한다. 이를 위해 각 스크롤 상자에 대해서 스크롤이 얼마나 멀리 아래에 있는지 등을 확인한다.

도형의 일부가 상자 안에 있으면 디스플레이 항목에 도형이 포함된다. 만약 도형이 페이지에 표시되지 않으면 디스플레이 항목에서 제거된다. 이 프로세스를 초기 선별(early culling)이라고 한다.

중간 텍스처의 수 최소화 (렌더링 작업 트리)

이제 사용할 도형만 포함한 트리를 가지고 있다. 이 트리는 앞에서 말한 스태킹 컨텍스트으로 구성된다.

CSS 필터와 스태킹 컨텍스트 같은 효과는 조금 복잡하다. 예를 들어 불투명도가 0.5이면서 자식을 가진 요소가 있다고 가정해보자. 각 자식 요소에 투명도가 적용되었다고 생각할지도 모르지만 그것은 실제로 전체 그룹에 적용된 것이다.

이 때문에 그룹을 먼저 텍스처로 렌더링하고, 각 상자는 완전 불투명하게 렌더링해야 한다. 그런 다음 부모 요소 안에 배치할 때 전체 텍스처의 불투명도를 변경할 수 있다.

이러한 스태킹 컨텍스트는 중첩될 수 있다... 즉, 부모 요소는 또 다른 스태킹 컨텍스트의 일부일 수 있다. 이것은 다른 중간 텍스처로 렌더링되어야 한다는 것을 의미한다.

이러한 텍스처를 위한 공간을 만드는 것은 비용이 많이 든다. 가능한 한 많이 동일한 중간 텍스처를 그룹화하려고 한다.

GPU가 이것을 돕기 위해 렌더 작업 트리(render task tree)를 만든다. 렌더 작업 트리로 우리는 어떤 텍스쳐가 다른 텍스쳐보다 먼저 만들어져야 하는지 알 수 있다. 첫 번째 패스에서는 다른 것과 관계 없는 텍스처를 만들 수 있다. 동일한 중간 텍스처로 그룹화 할 수 있다.

그래서 위 예제에서, 첫 번째로 박스 그림자의 한 모서리를 출력한다. (이보다 약간 더 복잡하지만 이것이 요지다)

두 번째는 상자 그림자를 일괄 처리하기 위해 모서리를 모든 상자 주위로 비출 수 있다. 그런 다음 그룹을 완전히 불투명하게 렌더링할 수 있다.

다음으로 해야 할 일은 이 텍스처의 불투명도를 변경하고, 화면에 출력될 최종 텍스처를 이동해야 하는 위치에 배치하는 것이다.

이 렌더 작업 트리를 구성하는 것으로, 우리가 사용할 수 있는 오프스크린(offscreen) 렌더 대상의 최소 개수를 파악한다. 앞서 말했듯이 이러한 렌더 대상 텍스처의 공간을 만드는 것은 비용이 많이 들기 때문에 렌더 작업 트리를 사용하는 것이 좋다.

또한 함께 일괄 처리하는데 도움이 된다.

드로우콜 그룹화 (일괄 처리)

이전에 말했듯이, 많은 도형을 가진 적은 수의 일괄 처리를 생성해야 한다.

일괄 처리를 만드는 방법에 관심을 가지면 실제로 속도를 높일 수 있다. 할 수 있는 한 같은 일괄 처리 안에 많은 도형을 가져야 한다. 이것은 몇 가지 이유 때문이다.

첫 번째, CPU가 GPU에 드로우콜을 지시할 때마다 CPU는 많은 작업을 해야 한다. 작업에는 GPU 설정, 셰이더 프로그램 업로드, 다른 하드웨어 버그 테스트 등이 있다. CPU가 이 작업을 수행하는 동안 작업이 합쳐지면서 GPU가 아이들 상태(프로세스가 실행되고 있지 않음)가 될 수 있다.

두 번째, 상태를 변경하는데 비용이 든다. 일괄 처리 사이에서 셰이더 프로그램을 변경해야 한다고 가정해보자. 일반적인 GPU에서 모든 코어들은 현재 셰이더가 완료될 때까지 기다려야 한다. 이것을 파이프라인 배출(draining the pipeline)이라고 부른다. 파이프라인에서 빠져나올 때까지, 다른 코어는 아이들 상태가 된다.

이 때문에 가능한 한 많이 일괄 처리해야 한다. 일반적인 데스크탑 PC의 경우 프레임 당 100회 또는 그 이하의 드로우콜이 필요하며 각 콜이 수천 개의 정점을 필요로 한다. 이 방법으로 병렬 처리를 활용할 수 있다.

렌더 작업 트리에서 각 패스들을 보고, 함께 일괄 처리 할 수 있는 항목을 찾는다.

그 시점에 각기 다른 종류의 프리미티브(primitive)는 다른 셰이더를 요구한다. 예를 들어 보더 셰이더와 텍스트 셰이더, 이미지 셰이더가 있다.

우리는 더 큰 일괄 처리를 할 수 있게 해주는 많은 셰이더를 결합할 수 있다고 믿지만 이미 충분히 일괄 처리되어 있다.

GPU로 전송할 준비가 거의 끝났다. 그러나 우리가 제거할 작업이 조금 더 있다.

불투명 및 알파 패스로 픽셀 셰이딩 줄이기 (Z-컬링)

대부분의 웹 페이지에는 겹치는 도형들이 많이 있다. 예를 들어, 텍스트 필드는 body(또 다른 배경 포함) 상단에 있는 div(배경 포함) 상단에 위치한다.

겹쳐진 도형들의 픽셀 색상을 알면 GPU는 각 도형의 픽셀 색상을 파악할 수 있다. 그러나 보여지는 것은 최상위 레이어뿐이다. 이를 오버드로우(overdraw)라고 하며, GPU 시간을 낭비한다.

이것을 구현하는 방법은 맨 위의 도형을 먼저 렌더링하는 것이다. 다음 도형에서는 같은 픽셀로 가져올 때 이미 값이 있는지 여부를 확인한다. 있다면 렌더링을 하지 않는다.

그러나 이것은 조금 문제가 있다. 도형이 반투명일 때마다 두 도형의 색상을 혼합해야 한다. 그리고 올바르게 보이기 위해서는 정면에서 발생해야 한다.

그래서 두 가지 패스로 나누어서 작업을 한다. 먼저 불투명한 패스를 수행한다. 불투명한 도형의 앞뒤로 가서 모든 도형들을 렌더링한다. 다른 도형들 뒤에 있는 픽셀들은 건너 뛴다.

그런 다음 반투명한 도형을 만든다. 도형들은 정면으로 렌더링된다. 반투명 픽셀이 불투명한 픽셀 위에 떨어지면 불투명한 픽셀로 혼합된다. 불투명한 도형 뒤에 있는 경우 계산되지 않는다.

불투명 및 알파 패스로 작업을 구분하는 과정과 필요없는 픽셀 계산을 건너 뛰는 과정을 Z-컬링(Z-culling)이라고 한다.

이것은 간단한 최적화처럼 보일 수 있지만, 우리에게 매우 큰 승리를 가져왔다. 일반적인 웹 페이지에서 건드려야 될 픽셀 수를 크게 줄였다. 그리고 현재 우리는 불투명 패스로 더 많은 작업을 할 수 있는 방법을 찾고 있다.

이 시점에서 프레임을 준비했다. 우리는 작업을 없애기 위해 최대한 노력했다.

... 그리고 우리는 그릴 준비가 되었다!

이제 GPU를 설정하고 일괄 처리들을 렌더링할 준비가 되었다.

주의 사항 : 모든 것이 아직 GPU에 있는 것은 아니다.

CPU는 여전히 페인팅 작업을 해야 한다. 예를 들어 글리프(glyph)라고 부르며 텍스트 블록에 사용되는 문자를 CPU에서 렌더링한다. GPU에서 이 작업을 수행 할 수도 있지만, 컴퓨터가 다른 응용 프로그램에서 렌더링하는 글리프로 픽셀 단위의 매칭 값을 가져오는 것이 어렵기 때문이다. 따라서 사람들은 GPU 렌더링 폰트를 보지 못하게 될 수도 있다. 우리는 패스파인더 프로젝트를 사용하여 글리프와 같은 것들을 GPU로 옮기는 작업을 실험하고 있다.

현재는 이러한 것들이 CPU의 비트맵에 그려진다. 그런 다음 GPU의 텍스처 캐시에 업로드된다. 이 캐시는 대개 변경되지 않기 때문에 프레임 사이에서 유지된다.

이 페인팅 작업은 CPU에 머무르고 있지만, 지금보다 더 빠르게 만들 수 있다. 예를 들어 폰트로 문자를 그릴 때, 모든 코어에서 다른 문자들을 분리한다. Stylo가 스타일 계산을 병렬 처리하기 위해 사용한 것과 동일한 기술을 사용해 작업을 수행한다.

WebRender 다음 단계는 무엇인가?

최초의 Firefox Quantum 릴리즈 이후 몇 번의 릴리스가 있는데, 우리는 2018년에 Quantum Render의 일부로 Firefox에 WebRender를 추가할 것으로 기대한다. WebRender를 사용하면 웹 페이지가 보다 원활하게 실행될 것이다. 또한 화면의 픽셀 수를 늘리면 렌더링 성능이 더욱 중요해지기 때문에, 이를 사용하면 고해상도 4K 디스플레이 혁신에 맞춰 Firefox를 사용할 수도 있다.

WebRender는 Firefox에만 유용한 것은 아니다. WebVR을 사용하여 작업할 때도 중요하다. WebVR에서는 각 눈마다 4K 해상도에서 90 FPS로 다른 프레임을 렌더링해야 한다.

WebRender는 아직 통합 작업이 진행 중이므로, 완성될 때까지 성능이 좋지 않을 수 있다. WebRender 개발 내용을 따라잡고 싶다면 GitHub 또는 Firefox Nightly on Twitter를 팔로우 하라.


류선임, FE Development Lab2017.10.16Back to list