한 배치에 텍스처 여덟 장까지다
Esc

    한 배치에 텍스처 여덟 장까지다

    –UnityUIUI Toolkit최적화

    무엇이 다시 계산되는지가 네 가지로 나뉜다는 무엇을 바꾸면 무엇이 다시 도는지, CPU 쪽 이야기였습니다. 이번은 그렇게 만든 것을 GPU 로 보내는 쪽입니다.

    문서 첫 줄이 이렇습니다. “Every visual element requires instructions to be sent to the GPU. UI Toolkit optimizes these draw calls through batching.” 여기까지는 uGUI 와 같은 말인데, 묶이는 조건이 다릅니다.

    텍스처가 달라도 한 배치에 들어갑니다

    uGUI 에서는 재질 하나가 배치 하나였습니다. 한 칸이 바뀌면 그 캔버스는 통째로 다시 계산된다에서 아틀라스를 맞추라고 한 이유가 그것이었습니다. 다른 아틀라스를 쓰는 그림이 사이에 끼면 거기서 갈렸습니다.

    UI Toolkit 은 우버 셰이더 하나로 그립니다. 그래서 텍스처가 서로 달라도 같은 배치에 들어갈 수 있습니다. 다만 한도가 있습니다.

    문서 표현으로 “up to eight textures within the same batch” 입니다. 여덟 장까지는 한 번에 나가고, 넘기면 “forces the batching system to split into separate batches, increasing overhead” 입니다.

    그래서 아틀라스의 값이 uGUI 와 조금 달라집니다. 반드시 한 장으로 묶어야 하는 것이 아니라, 여덟 장 안으로만 들어오면 됩니다. 아이콘 스무 개를 아틀라스 두 장으로 묶었다면 그걸로 충분합니다. 문서도 “consolidating textures into atlases helps keep the number of textures within the supported limit” 라고, 한도 안으로 들이는 것이 목적이라고 적어 뒀습니다.

    런타임에 모이는 것들을 위해 Dynamic Atlas 도 있습니다. 해상도 대응은 씬이 아니라 에셋 한 장에 적힌다에서 Panel Settings 항목으로 이름만 냈던 것입니다. 작은 텍스처들을 런타임에 한 장으로 모아서 여덟 장 한도에 여유를 만듭니다. Min/Max Atlas Size 와 Max Sub Texture Size 로 어디까지 넣을지 정합니다.

    그래도 사이에 끼면 깨집니다

    한도 안이어도 안 묶이는 경우가 있습니다. 문서가 드는 예가 이겁니다. 글자들 사이에 이미지를 하나 끼워 넣으면 “requires different GPU settings. That forces a new batch” 입니다.

    텍스처 장수가 아니라 GPU 상태가 문제입니다. 글자는 글자대로, 이미지는 이미지대로 다른 설정으로 그려집니다. 그 사이를 오가면 배치가 끊깁니다.

    그리는 순서는 하이어라키가 아니라 숫자가 정한다에서 NGUI 의 FillAllDrawCalls 가 재질이 바뀌는 자리에서 드로우 콜을 끊는 것을 봤습니다. 구조가 같습니다. 그리는 순서대로 훑다가 설정이 바뀌면 끊는다 는 것이 세 체계에 다 있습니다.

    그래서 실무 규칙도 같습니다. 글자는 글자끼리, 그림은 그림끼리 모아 둡니다. 목록 한 줄 안에 아이콘과 글자가 번갈아 들어가는 구조라면, 그 한 줄이 통째로 배치를 여러 번 끊습니다.

    정점 버퍼를 미리 잡아 둡니다

    Panel Settings 의 Vertex Budget 도 4편에서 이름만 냈습니다. 문서 설명이 “Set the approximate number of vertices the panel uses. This helps initialize vertex buffers with the right size, which can reduce draw calls” 였습니다.

    기본값이 0 이고, 0 이면 유니티가 알아서 잡습니다. 알아서 잡는다는 것은 모자라면 늘린다 는 뜻이고, 늘리는 과정에서 버퍼가 갈리면 드로우 콜이 늘어납니다.

    문서가 드는 예에서는 이 값을 20,000 으로 올려서 여러 개로 갈려 있던 드로우 콜을 하나로 만듭니다. 화면이 복잡한데 드로우 콜이 이상하게 많다면 여기를 한 번 볼 값어치가 있습니다.

    정점을 아끼는 쪽도 있습니다. 모서리를 둥글리거나 테두리를 그리면 그만큼 정점이 늘어납니다. 8편에서 border-radius 가 정점 버퍼를 다시 만든다고 했는데, 만들어진 결과물도 그만큼 무겁습니다.

    마스크는 두 갈래입니다

    스크롤 목록은 창문 뒤에서 긴 종이를 움직이는 것이다에서 uGUI 의 Mask 와 RectMask2D 를 봤습니다. Mask 는 스텐실을 쓰고 중첩이 여덟 단계에서 막혔고, RectMask2D 는 사각형만 자르는 대신 그 제한이 없었습니다.

    UI Toolkit 도 똑같이 둘로 갈립니다.

    자르는 모양이 비용을 가릅니다 사각 마스크 중첩 제한 없음 셰이더로 자릅니다 GPU 상태를 안 바꿔서 배치가 안 끊깁니다 둥근 모서리 등 복잡한 모양 일곱 단계까지 스텐실 버퍼를 씁니다 GPU 상태가 바뀌면서 배치가 끊깁니다
    uGUI 의 RectMask2D 와 Mask 가 갈리던 자리와 같습니다.
    어떻게 자르나 중첩 배치
    사각 마스크 “shader-based operations” 제한 없음 “preserving batch consistency without GPU state changes”
    스텐실 마스크 둥근 모서리 등 복잡한 모양 일곱 단계까지 “potentially breaking batches”

    권고도 같습니다. 되도록 사각 마스크를 쓰고, 마스크를 계층에서 납작하게 유지하라 는 것입니다.

    체계가 셋인데 결론이 같다는 게 이쯤 되면 우연이 아닙니다. 임의의 모양으로 자르려면 스텐실 버퍼를 써야 하고, 스텐실은 GPU 상태라서 배치를 끊고 중첩 한도가 있습니다. 잘라 내는 모양을 사각형으로 두면 그 비용이 통째로 빠집니다.

    MaskContainer 라는 usageHints 값이 있는 것도 이 맥락입니다. 8편에서 본 넷 중 하나였고, 설명이 “Optimizes rendering of a VisualElement that has multiple descendants with nested masks” 입니다. 중첩 마스크가 많은 묶음의 부모에 답니다.

    겹쳐 그리는 것을 줄입니다

    문서 표현으로 “UI Toolkit renders elements with transparency, which can result in significant overdraw when elements overlap” 입니다. UI 는 대부분 투명을 끼고 그려서, 겹친 만큼 같은 픽셀을 여러 번 칠합니다.

    같은 자리를 몇 번 칠하는가 배경 팝업 뒤 어두운 판 팝업 판 여기는 네 번 칠합니다 투명이 겹치면 가려진 것도 다 칠합니다 opacity: 0 은 안 보여도 한 겹입니다 display: none 이면 그 겹이 빠집니다 전체 화면을 덮는 반투명 판 하나가 화면 전체를 한 번 더 칠합니다 목록이 길면 안 보이는 칸들까지 겹으로 쌓입니다
    안 보이는 것을 빼는 일이 그대로 픽셀 비용이 됩니다.

    문서가 드는 방법이 넷입니다.

    • DisplayStyle.None 을 opacity: 0 대신 씁니다. 8편에서 본 그 얘기입니다. 투명은 안 보일 뿐 계속 칠합니다
    • 가려져서 안 보이는 요소는 아예 뺍니다
    • 목록이 길면 ListView 로 가상화 합니다. 화면에 보이는 만큼만 만듭니다
    • overflow = Overflow.Hidden 으로 “clip content to specific areas”

    세 번째가 다음 편 주제입니다. 스크롤 목록이 길수록 안 보이는 칸들이 통째로 오버드로를 만듭니다.

    전체 화면을 덮는 반투명 판이 있다면 그것 하나가 화면 전체를 한 번 더 칠하고 있다는 것도 기억해 둘 만합니다. 팝업 뒤에 까는 어두운 판이 대표적입니다.

    안 쓰는 화면도 메모리를 씁니다

    마지막이 메모리입니다. 문서 한 줄이 분명합니다. uss 와 uxml 이 참조하는 에셋은 “immediately consume memory – even when not in use” 입니다.

    파일을 불러온 순간 그 안에서 가리키는 텍스처와 글꼴이 다 올라옵니다. 화면을 안 띄웠어도 그렇습니다. uxml 하나에 게임의 모든 화면을 넣어 두면 첫 화면에서 전부 올라오는 셈입니다.

    문서가 드는 대응이 셋입니다.

    • Asset Bundles 나 Addressables 로 필요한 UI 문서만 싣기
    • 큰 uxml 을 작은 템플릿으로 쪼개서 선택적으로 싣기. 7편에서 본 Template 과 Instance 가 그 도구입니다
    • 안 쓰게 된 것은 RemoveFromHierarchy 로 내리기

    쪼개 두면 얻는 것이 하나 더 있습니다. 7편에서 본 것처럼 구조가 고정인 부분을 템플릿으로 떼어 두면 재사용도 되고 메모리도 나눠 싣게 됩니다. 같은 작업이 두 가지 값을 냅니다.

    유니티 6 에서 손본 것들

    문서에 「Unity 6 performance enhancements」 절이 따로 있습니다. 지금 쓰는 버전이 그 이후라면 아래는 이미 들어와 있는 것들입니다.

    • “Event dispatching rules have been simplified, making them easier to understand and twice as fast.” 5편에서 본 전파 규칙이 그 대상입니다
    • 메시 생성이 잡으로 나뉘어 돌게 바뀌었습니다
    • 커스텀 지오메트리를 만들 수 있는 새 공개 API 가 생겼습니다
    • 깊은 계층에서 레이아웃 캐싱이 개선됐습니다
    • TreeView 가 “a new high-performance backend” 로 바뀌었습니다

    오래된 프로젝트에서 성능 얘기가 나올 때 버전을 먼저 확인하는 게 순서 라는 뜻이기도 합니다. 코드를 고치기 전에 올려서 해결되는 부분이 있습니다.

    정리

    • 우버 셰이더가 한 배치에 텍스처 여덟 장 까지 받습니다. uGUI 처럼 한 장으로 몰아야 하는 게 아닙니다
    • 넘기면 배치가 갈립니다. 아틀라스는 한도 안으로 들이는 것 이 목적이고, 런타임 것은 Dynamic Atlas 가 맡습니다
    • 한도 안이어도 글자와 이미지를 번갈아 놓으면 GPU 상태가 바뀌어 끊깁니다. 세 체계가 다 같습니다
    • Vertex Budget 기본값 0 은 알아서 잡는다는 뜻입니다. 드로우 콜이 이상하게 많으면 여기를 봅니다
    • 마스크는 사각이면 셰이더, 복잡한 모양이면 스텐실 입니다. 스텐실은 일곱 단계까지이고 배치를 끊습니다
    • 오버드로는 DisplayStyle.None, 가려진 것 빼기, ListView 가상화, overflow: hidden 으로 줄입니다
    • uss·uxml 이 참조하는 에셋은 안 써도 메모리에 올라옵니다. 쪼개서 싣고 RemoveFromHierarchy 로 내립니다
    • 유니티 6 에서 이벤트 전파가 두 배 빨라졌습니다. 성능 얘기 전에 버전부터 확인합니다