티스토리 뷰
모바일 프로그래밍
Android Graphics Architecture - (2) SurfaceFlinger와 Hardware Composer
두덕리온라인 2017. 12. 1. 17:44728x90
반응형
SurfaceFlinger와 Hardware Composer
그래픽 데이터의 버퍼들을 생성한 것도 훌륭하지만, 이 데이터들을 여러분 기기의 화면에서 보게 된다면 더욱 멋질 것입니다. 그것이 SurfaceFlinger와 Hardware Composer HAL이 관여하는 부분이다.
SurfaceFlinger의 역할은 여러 소스로부터 그래픽 데이터 버퍼들을 받고, 그것들을 합성해서 display로 보내는 것이다. 예전에는 이런 작업들은 하드웨어 프레임버퍼 (예를 들면, /dev/graphics/fb0)로 software blitting 하는 것으로 처리했지만, 이런 시기가 지난지 이미 오래됐다.
(역자주) 여기서 blitting은 컴퓨터의 그래픽 메모리를 다른 곳으로 복사하는 것을 의미하는 그래픽 용어다.
앱이 포그라운드로 나올 때, Window Manager 서비스는 SurfaceFlinger에 드로잉 Surface를 요청한다. SurfaceFlinger는 “레이어” - 이것의 기본 컴포넌트는 BufferQueue다 - 를 생성하며, BufferQueue의 소비자(Consumer)처럼 동작한다. 생산자(Producer) 쪽의 바인더 객체는 Window Manager를 통해 앱으로 전달되며, 이후에 앱은 바인더 객체를 통해 SurfaceFlinger 쪽으로 직접 프레임 전송을 시작할수 있다.
Note: Window Manager는 위에서 설명한 동작을 위해 “레이어”라는 용어 대신 “윈도우”라는 용어를 사용하며, “레이어”라는 용어를 다른 용도로 사용한다. 우리는 SurfaceFlinger 용어를 사용할 예정이다. 이것은 SurfaceFlinger가 정말로 LayerFlinger라고 불러져야 하는 논쟁을 불러 일으킬 수 있다.
대부분의 앱이 스크린에서 동작할 때는 항상 다음과 같은 3개의 레이어로 구성이 될 것이다. 스크린 상단의 상태바, 스크린 옆이나 하단 부에 네비게이션 바 그리고 애플리케이션의 UI가 바로 그것이다. 몇몇 앱은 더 많거나 적은 레이어를 가지고 있을 수도 있다. 예를 들어 기본 Home 앱의 경우, 바탕화면을 표시하기 위한 별도의 레이어를 가지고 있으며, 반면에 풀스크린 게임은 상태바를 숨길 수도 있다. 각 레이어는 독립적으로 업데이트 될 수 있다. 상태바와 네비게이션바는 시스템 프로세스에 의해 렌더링되는 반면에, 앱 레이어들은 앱 자신에 의해 렌더링이되며, 이들을 관리하는 별도의 모듈은 없다.
디바이스 디스플레이는 특정 주기로 리프레시되며, 보통 폰이나 테블릿에서는 초당 60프레임의 속도로 리프레시된다. 만약 디스플레이 컨텐츠가 리프레시 도중에 업데이트가 된다면, 티어링(tearing) 현상이 나타날 것이다. 그래서 각 리프레시 주기 동안에 컨텐츠 업데이트를 마무리 하는 것이 중요하다. 시스템은 컨텐츠를 업데이트하는데 안전한 시점을 디스플레이로부터 시그널을 통해 전달받는다. 역사적인 이유로 우리는 이 시그널을 VSYNC라고 부른다.
리프레시 주기는 동작 중에 변경될 수도 있다. 예를 들어 몇몇 모바일 장치들은 현재 상황에 따라 58에서 62fps의 범위를 가지고 동작한다. HDMI TV에 경우, 이론적으로 비디오 처리를 위해 24 또는 48Hz로 낮출 수도 있다. 리프레시 사이클에 오직 한번만 스크린을 업데이트 할 수 있기 때문에, 200fps로 디스플레이를 하기 위해 버퍼를 전송하면 대부분의 프레임을 보지 못하기 때문에 리소스를 낭비한다고 볼 수 있다. SurfaceFlinger는 앱이 버퍼를 전송할 때마다 처리를 하는 것 대신에, 디스플레이가 업데이트가 가능하다고 준비될 때 깨어난다.
SurfaceFlinger가 VSYNC 시그널을 받았을 때, 새로운 버퍼들을 찾기위해 자신의 레이어 리스트를 뒤진다. 그래서 새로운 버퍼가 발견이 됐다면, SurfaceFlinger는 그것을 받는다. 만약 새로운 버퍼가 발견되지 않았다면, SurfaceFlinger는 이전에 가져왔던 버퍼들을 계속 사용한다. SurfaceFlinger는 항상 뭔가를 디스플레이 하려고 하기 때문에, 버퍼 한개도 처리할 것이다. 레이어 상에 어떤 버퍼도 전송되지 않았다면, 그 레이어는 무시된다.
일단 SurfaceFlinger는 Visible 레이어들의 모든 버퍼들을 모은 다음, Hardware Composer에게 어떻게 각 레이어의 합성(position)을 수행할 건지에 대해 요청한다.
Hardware Composer
Hardware Composer HAL (“HWC”)는 안드로이드 3.0 (“허니콤”)에서 최초로 소개됐고, 몇년에 걸쳐 점점 발전했다. HWC의 주요 목적은 사용 가능한 하드웨어를 가지고 여러 버퍼들을 가장 효율적으로 합성하는 방법을 결정하는 것이다. HAL로서 HWC의 구현은 디바이스에 특화되며, 대개는 디스플레이 하드웨어 OEM 업체들에 의해 구현된다.
이러한 접근 방식의 장점은 언제 오버레이 평면(overlay planes)을 고려해야 하는지 인식하기 쉽다는 것이다. 오버레이 평면의 목적은 GPU 보다는 디스플레이 하드웨어에서 여러 버퍼들을 함께 합성하게 하는 것이다. 예를 들면, 안드로이드 폰이 세로 방향 상태에서, 상단 부에 상태바가, 하단 부에는 네비게이션 바가 있으며, 그 밖의 영역에는 애플리케이션이 있다고 가정해보자. 각 레이어의 컨텐츠들은 별도의 버퍼들로 구성된다. App 컨텐츠는 스크래치 버퍼에 렌더링을 하고, 그런 다음 그 위에 상태바를 렌더링하고 그 위에 네비게이션을 렌더링함으로써, 이렇게 합성된 스크래치 버퍼의 내용을 디스플레이 하드웨어로 넘긴다.(역자주 - 여기서는 합성이 GPU에 의해서 일어남) 또는 세 개의 버퍼 모두를 디스플레이 하드웨어로 넘기고. 디스플레이 하드웨어에게 각각 버퍼 데이터를 스크린의 각 영역에 렌더링 하라고 얘기할 수 있다 (역자주 - 이것은 HWC를 이용한 합성 방법이다). 후자의 접근 방식이 훨씬 더 효율적일 수 있다.
여러분이 예상한대로, 디스플레이 프로세서들마다 그 기능들은 상당히 다르다. 오버레이 개수, 어떤 레이어들이 회전이나 블렌딩 될 수 있는지, 레이어의 포지셔닝 및 오버랩에 대한 제한들에 대해서 API를 통해 표현하기 어려울 수 있다. 그래서 HWC는 아래와 같이 동작한다.
- SurfaceFlinger는 HWC에게 전체 레이어 리스트를 제공하고, “당신(HWC)은 이것을 어떻게 처리하고 싶어?”라고 묻는다.
- HWC는 이러한 질문에 대해 각 레이어에 “오버레이(overlay)” 또는 “GLES 합성(composition)”이라고 표시해서 응답한다.
- SurfaceFlinger는 GLES 합성을 처리하거나, 출력 버퍼를 HWC 전달하고, 나머지 작업을 HWC에 맡긴다.
로직을 결정하는 코드는 각 하드웨어 벤더들에 의해 커스터마이징 될 수 있기 때문에, 모든 디바이스들로부터 최적의 성능을 얻을 수 있다.
type | source crop | frame name
------------+-----------------------------------+--------------------------------
HWC | [ 0.0, 0.0, 320.0, 240.0] | [ 48, 411, 1032, 1149] SurfaceView
HWC | [ 0.0, 75.0, 1080.0, 1776.0] | [ 0, 75, 1080, 1776] com.android.grafika/com.android.grafika.PlayMovieSurfaceActivity
HWC | [ 0.0, 0.0, 1080.0, 75.0] | [ 0, 0, 1080, 75] StatusBar
HWC | [ 0.0, 0.0, 1080.0, 144.0] | [ 0, 1776, 1080, 1920] NavigationBar
FB TARGET | [ 0.0, 0.0, 1080.0, 1920.0] | [ 0, 0, 1080, 1920] HWC_FRAMEBUFFER_TARGET
위 표는 스크린 상에 무슨 레이어가 있는지를 여러분에게 말해준다. (역자주 - 위 표의 행이 '레이어'를 나타낸다) 또한 각 레이어들이 오버레이(“HWC”)로 처리되는지 또는 OpenGL ES 합성(“GLES”) 방식으로 처리되는 지를 보여주고, 여러분이 아마 관심 없어할 일련의 정보들(위 표에서 제거된 “핸들(handler)”과 “힌트(hints)”와 “플래그(flags)” 및 다른 정보들)도 제공해준다. 위 표의 source crop와 frame 컬럼에 대해서는 이후에 좀 더 자세히 다뤄볼 것이다.
FB_TARGET 레이어는 GLES 합성의 출력 결과가 나오는 부분이다. 위 표에서 오버레이(HWC)를 이용한 모든 레이어가 스크린에 보여졌기 때문에, FB_TARGET은 이번 프레임에서 이용되지는 않는다. 위 표의 레이어 이름(name)컬럼은 그것의 원래 역할을 보여준다. /dev/graphics/fb0만 있고, 오버레이를 가지지 않은 디바이스의 경우, 모든 합성은 GLES로 처리되고, 합성 결과가 프레임버퍼에 쓰여질 것이다. 최신 디바이스의 경우, 일반적으로 단순한 프레임버퍼가 있는 것이 아니며, FB_TARGET 레이어는 스크래치 버퍼다.
Note : 이것이 안드로이드 예전 버전에서 작성된 screen grabber가 더 이상 동작하지 않는지에 대한 이유다. 그것은 프레임버퍼에서만 데이터를 읽어오지, 다른 곳에서는 그렇게 하지 못하기 때문이다.
오버레이 평면은 또 다른 중요한 역할이 있다. 그것은 DRM 컨텐츠를 보여주는 유일한 방식이다. DRM이 걸린 버퍼들은 SurfaceFligner나 GLES 드라이버를 통해 접근할 수 없으며, 이것은 HWC를 GLES 합성 방식으로 바꾸면, 여러분의 비디오가 사라질 수 있다는 것을 의미한다.
트리플 버퍼링의 필요성
디스플레이 할때 티어링(Tearing)을 방지하기 위해, 시스템은 더블 버퍼링 처리가 필요하다. 더블 버퍼링에서는 후면 버퍼(back buffer)가 준비되는 동안, 전면 버퍼(front buffer)가 디스플레이된다. VSYNC 시그널이 발생할 때 후면 버퍼가 준비되면, 전면 버퍼와 후면 버퍼를 빠르게 교환한다. 이러한 더블 버퍼링은 프레임 버퍼에 바로 그려야 하는 시스템에서 꽤 잘 동작한다. 그러나 합성 과정이 추가됐을 때, 디스플레이 과정에서 문제가 발생한다. SurfaceFlinger가 트리거 되는 방식 때문에, 더블 버퍼된 파이프 라인이 날아갈 수 있다.
N번째 프레임이 디스플레이 되고 있고, 다음 VSYNC에서 디스플레이하기 위해 SurfaceFlinger에 N+1번째 프레임이 저장돼 있다고 가정하다. (N번째 프레임이 overlay로 합성되는 것을 가정하면, 우리는 이 프레임이 디스플레이될 때 까지 버퍼 컨텐츠를 바꿀 수 없다) VSYNC 시그널을 받았을 때, HWC는 버퍼들을 플립(flip)한다.(역자주-N번째 프레임 버퍼는 이미 디스플레이가 완료됨) App이 N번째 프레임을 가지고 있었던 버퍼에 N+2번째 프레임을 렌더링하려고 시작하는 동안, SurfaceFlinger는 레이어 리스트를 스캔하고 업데이트된 레이어를 조사한다. SurfaceFlinger는 어떤 새로운 버퍼가 없으면(즉, App이 렌더링을 N+2번째 프레임에 렌더링을 마무리하지 않았았다면), 다음 VSYNC 시그널에 N+1번째 프레임을 다시 보여줄 준비를 한다. 잠시 후에, App은 N+2번째 프레임 렌더링을 마치고 그것을 SurfaceFlinger로 넘기지만, 이미 늦어버린 것이다.(역자주-즉, 프레임 Drop이 발생한 것이다.) 이러한 현상은 최대 프레임 주기를 반으로 줄여버린다.
우리는 이러한 문제를 트리플 버퍼링으로 고칠 수 있다. VSYNC가 발생하기 직전, N번째 프레임이 디스플레이 되고 있고, N+1번째 프레임은 SurfaceFlinger에서 합성이 된 후 (또는 overlay를 위한 스케쥴링이 된 후) 디스플레이가 될 준비가 된 상황이고, N+2번재 프레임은 BufferQueue에 인큐 되고, SurfaceFlinger가 프레임을 수신할 준비가 된다. 스크린이 플립되면, 버퍼들은 데이터가 없는 상태로 돌아간다. 앱은 전체 VSYNC 주기(16.7ms at 60fps)보다 작은 시간 동안 렌더링 하고 버퍼를 큐에 넣어야 한다. 그리고 SurfaceFlinger/HWC는 다음 플립 전에 합성을 처리하기 위한 전체 VSYCN 주기를 가진다. 이러한 방식의 약점은 앱이 스크린에 뭔가를 출력하기 위해서는 적어도 2개의 VSYNC 주기 만큼 걸린다는 것이다. 이러한 지연 시간이 증가하는 만큼, 디바이스 터치 입력 반응성이 더 떨어진다.
위 다이어그램은 프레임이 그려지는 동안 SurfaceFliger와 BufferQueue의 흐름을 설명해주고 있다.
- 붉은 색 버퍼가 차면, BufferQueue로 밀어넣는다.
- 붉은 색 버퍼가 앱에서 BufferQueue로 전송이 완료된 후, 푸른색 버퍼가 그것을 대체해서 앱으로 들어온다. (역자주- 이미 푸른색 버퍼는 렌더링이 완료된 상황이다.)
- 초록색 버퍼와 시스템 UI(각주1 참조)가 생성한 그림자 표시 부분은 HWC로 이동한다.(SurfaceFlinger가 아직 버퍼를 가지고 있는 것 처럼 보이지만, 지금 HWC는 다음 VSYNC 주기에 오버레이를 통해 버퍼를 디스플레이하기 위해 준비하고 있다)
푸른색 버퍼는 Display와 BufferQueue 두 군데에서 참조되고 있다. 앱은 관련된 sync fence 시그널이 발생할 때까지 푸른색 버퍼에 렌더링을 할 수 없다.
VSYNC 주기에, 다음과 같은 동작들이 한꺼번에 일어난다.
- 붉은색 버퍼는 초록색 버퍼를 대치하며 SurfaceFlinger로 보내진다.
- 초록색 버퍼는 푸른색 버퍼를 대치하며 Display로 보내지고, 위 그림의 점선이 가리키는 BufferQueue 에도 동일한 버퍼가 존재한다.
- 푸른색 버퍼의 펜스가 시그널을 보내면, 푸른색 버퍼는 앱에서 비워진다.
- 디스플레이 되는 사각형 영역은 <푸른색 버퍼+System UI>에서 <초록색 버퍼 + System UI> 내용으로 변경된다.
(각주1) System UI 프로세스는 상태바와 네비게이션바를 제공하며, 여기서 이 두 아이템들은 변경되지 않았기 때문에 SurfaceFlinger는 이전에 얻었던 버퍼를 그대로 사용한다. 살제로 두 개의 별도 버퍼로 나눠져 있을 것이며, 하나는 맨위 상태바를, 다른 하나는 바닥에 네비게이션바일 것이다. 그리고 이것들은 자신의 컨텐츠에 적합하게 크기를 조절할 것이다. 각자 자신의 BufferQueue에서 저장될 것이다.(각주2) 버퍼가 실제로 ‘비워’지지는 않는다. 만약 앱에서 푸른색 버퍼에 그리는 작업을 하지 않고 버퍼를 전송한다면, 동일한 푸른색이 전송된다. 버퍼를 비운다는 것은 버퍼 컨텐츠의 내용을 초기화 한다는 것을 의미하며, 앱은 그리기를 실행하기 전에 이러한 작업을 처리해야 한다.
레이어 합성 과정이 전체 VSYNC 주기가 필요하지 않다는 것을 주목해보면 지연을 줄일 수 있다. 합성이 Overlay에서 발생하면, 이론적으로는 CPU와 GPU 소모 시간이 거의 0에 가깝다. 그러나 우리는 정확한 소모 시간을 확신할 수 없으므로, 약간의 허용 시간이 더 필요하다. 만약 App이 VSYNC 시그널 중간에 렌더링을 시작했고, SurfaceFlinger가 다음 VSYNC 시그널이 발생할 몇 밀리초까지 HWC 셋업을 연기했다면, 2프레임에서 1.5프레임으로 지연을 줄일 수 있다. 이론적으로 더블 버퍼를을 활용하면 단일 주기 동안 렌더링과 합성을 처리할 수 있다. 그러나 이런 작업들은 최근 장치에서는 처리하기 어렵다. 렌더링과 합성 과정의 소모 시간 약간의 변동과 Overaly를 GLES 합성으로 변경하는 작업은 Swap Deadline을 넘기는 원인이 될 수 있고, 이 경우 이전 프레임이 반복된다.
SurfaceFlinger의 버퍼 핸들링은 앞서 언급한 것처럼 펜스 기반 버퍼 관리를 이용한다. 최고 속도로 애니메이션을 처리한다면, 전면 디스플레이를 위한 버퍼와 다음 Flip을 위한 후면 버퍼를 얻는 것이 필요하다. Overlay 상에 버퍼 내용을 보여 주고 있다면, 버퍼 컨텐츠는 display에 의해 직접 액세스 되고 있는 것이다. 그러나 여러분이 dumpsys SurfaceFlinger 명령의 출력으로 각 액티브 레이어의 BufferQueue 상태를 본다면, BufferQueue로부터 얻은 버퍼, BufferQueue에 존재하는 버퍼, 프리 버퍼를 각각 보게 될 것이다. 그것은 SurfaceFlinger가 새 후면(back)버퍼를 얻을 때, 전면(front)버퍼를 큐에서 릴리즈 하기 때문이다. 전면버퍼는 여전히 Display에 의해 사용중이므로, 그것을 dequeue 하려는 작업은 그것을 그리기 직전에 발생하는 펜스 시그널을 기다려야 한다. 모든 사람들이 펜스 규치를 따르는 한, 모든 큐 관리를 위한 IPC 요청은 Display와 병렬적으로 발생할 수 있다.
가상 디스플레이 (Virtual Display)
SurfaceFlinger는 Phone이나 태블릿에 장착된 것과 같은 메인(primary) 디스플레이와 HMDI를 통해 연결된 텔레비전과 같은 외부(external) 디스플레이를 지원한다. 그것은 또한 여러개의 가상(virtual) 디스플레이를 지원하는 데, 그것은 시스템 내부에서의 합성된 출력을 가능하게 해준다. 가상 디스플레이는 스크린을 레코딩 하거나 네트워크로 보내는데 활용할 수 있다.
가상 디스플레이는 메인 디스플레이(“레이어 스택’)처럼 동일한 종류의 레이어를 공유하거나 별도의 레이어를 가질 수 있다. 가상 디스플레이를 위한 VSYNC는 없으며, 메인플레이를 위한 VSYNC를 모든 디스플레이에서 합성을 트리거 하는데 활용할 수 있다.
예전에 가상 디스플레이는 항상 GLES로 합성을 처리했다. 하드웨어 컴포저는 메인 디스플레이용 합성만을 관리했다. 그런데 안드로이드 4.4 버전에서는 하드웨어 컴포저가 가상 디스플레이를 포함시키는 기능을 갖게됐다.
여러분의 예상대로, 가상 디스플레이를 위해 생성된 프레임은 BufferQueue에 저장된다.
케이스 스터디 : screenrecord 명령어
이제 우리는 BufferQueue와 SurfaceFlinger에 대한 어느 정도의 배경 지식을 쌓았고, 이러한 지식들은 실용적인 유스 케이스를 살펴보는데 유용하다.
안드로이드 4.4에서 소개된 screenrecord 명령어는 여러분들이 화면에 나타나는 모든 것을 레코딩하고 .mp4 파일로 디스크에 저장할 수 있게 해준다. 이것을 구현하기 위해서, 우리는 SurfaceFlinger로부터 합성된 프레임을 받고, 그것을 비디오 인코더에 넘긴 다음, 인코딩된 비디오 데이터를 파일에 기록해야 한다. 비디오 코덱은 미디어 서버(mediaserver)라는 별도의 프로세스에 의해 관리되므로, 우리는 많은 양의 그래픽 버퍼들을 시스템으로 이동시켜야 한다. 이것을 좀더 도전적으로 처리하기 위해서, 우리는 최대 해상도로 60fps 비디오 레코딩을 하려고 노력했다. 이러한 작업을 효율적으로 하기 위한 핵심이 BufferQueue다.
앱은 MediaCodec 클래스를 통해 raw 데이터를 버퍼나 Surface로 제공한다. 우리는 Surface에 대해서 뒤에서 좀더 자세히 살펴볼 것이지만, 이것을 BufferQueue 생산자 끝에 달린 래퍼로 생각하자. screenrecord 명령이 비디오 인코더에 접근을 요청할 때, mediaserver는 BufferQueue를 생성하고 자기 자신을 생성된 BufferQueue의 소비자로 연결한다. 그런 다음 screenrecord로 BufferQueue의 생산자 역할을 하게되는 Surface를 넘긴다.
screenrecord 명령어는 SurfaceFlinger에게 메인 디스플레이를 미러링하는 가상 디스플레이 생성을 요청한다. (즉, 가상 디스플레이는 모든 종류의 레이어를 가진다) 그리고 가상 디스플레이의 출력 결과를 mediaserver로부터 생성된 Surface로 보낸다. 이 경우, SurfaceFlinger는 버퍼의 소비자 보다는 생산자로서의 역할을 하는 것이다.
일단 스크린 레코딩을 위한 구성이 완료되면, screenrecord 명령어는 인코딩된 데이터가 생성되기를 기다린다. 앱들이 그리면, 그것들의 버퍼는 SurfaceFlinger로 보내지고, 그곳에서 하나의 버퍼로 합성이 되서 mediaserver의 비디오 인코더로 전송된다. 이때 모든 프레임이 screenrecord 프로세스를 거치지는 않는다. 내부적으로 mediaserver는 오버헤드를 최소화 하기위해 핸들을 통해 버퍼 데이터를 전송하는 자신만의 방법을 사용한다.
케이스 스터디 : 보조 화면 시뮬레이션
WindowManager는 SurfaceFlinger에게 SurfaceFlinger가 BufferQueue의 소비자로 동작하는 visible 레이어의 생성을 요청한다. 그것은 SurfaceFlinger에게 SurfaceFlinger를 BufferQueue의 생산자로서 동작하는 가상 디스플레이의 생성을 요청하는 것 또한 가능하다. 이 둘을 연결하고 visible 레이어를 렌더링하는 가상 디스플레이를 구성하면 어떻게 될까?
당신은 합성된 스크린이 윈도우 형태로 나타나는 루프를 생성한다. 물론 이 윈도우는 합성된 출력 결과의 부분이며, 다음 리프레시 동안 윈도우 내에 포함된 합성된 이미지는 전체 화면 컨텐츠와 동일한 내용이 보일 것이다.그것은 절대 끝나지 않는다. 안드로이드 폰의 ‘개발자 옵션’에서 ‘보조 화면 시뮬레이션’ 항목을 설정하면 이러한 동작을 직접 볼 수 있다. 추가로 screenrecord를 이용하면 이것들을 캡처하고 재생시킬 수 있다.
반응형
'모바일 프로그래밍' 카테고리의 다른 글
Android Thread에서 Dialog 띄우기 (wait, notify 사용) (0) | 2017.12.31 |
---|---|
Android Firebase, Google Analytics 디버그시 수집 중지 (0) | 2017.12.13 |
Android Graphics Architecture - (1) BufferQueue와 gralloc (0) | 2017.11.24 |
Android Studio 3.0 Unable to resolve dependency for ':app@debug/compileClasspath' (8) | 2017.10.31 |
Android 추석 기간에 만화/코믹 뷰어 앱 출시(코믹뷰어) (0) | 2017.10.05 |
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday