B블로그

ESP32-S3에서 소프트웨어로 60 fps

픽셀마다 함수 호출이 되어 버린 memcpy, 전송에 31 ms가 걸리던 프레임, 그리고 놀고 있던 두 번째 코어. 320x240 게임이 초당 16프레임에서 60프레임이 되기까지.

게시일
작성자
DevKirbyrawr
읽는 시간
약 4분
엔진이 320x240, RGB565로 렌더링한 프레임 세 장.

Deki는 모든 것을 소프트웨어로 그립니다. GPU 없이 CPU가 프레임 버퍼를 채우고, 버스가 그것을 패널로 실어 나릅니다. PSRAM과 320x240 SPI 디스플레이를 갖춘 ESP32-S3에서 데모는 처음에 초당 16프레임으로 돌아갔습니다. 화면에 그만큼 무거운 것은 없습니다. 배경, 스프라이트 몇 개, 텍스트, 파티클 조금이 전부입니다. 지금은 모든 씬이 60에서 63프레임으로 돌아갑니다. 무엇이 걸림돌이었는지 정리합니다.

디바이스에서 측정하기

데스크톱 프로파일은 마이크로컨트롤러에 대해 알려 주는 것이 거의 없으므로, 첫 번째 변경은 보드 자체에 물어볼 방법을 만드는 것이었습니다. 이제 펌웨어는 USB 시리얼 라인으로 몇 가지 요청에 응답합니다. 초당 프레임 수와 평균 프레임의 업데이트·렌더링·전송 시간, 컴포넌트 타입별 렌더링 시간, 그리고 스크립트가 모든 씬을 돌며 측정할 수 있도록 하는 “이 씬을 열어라"입니다. (에디터도 같은 요청을 사용합니다. 보드에서 가져오는 스크린샷과 통계를 참고하세요.) 아래의 숫자는 모두 여기서 나왔습니다.

컴파일러가 디버거용으로 빌드하고 있었습니다

ESP-IDF는 기본적으로 디버깅에 최적화된 -Og로 빌드합니다. 픽셀 단위 C++ 코드는 바로 이 설정에서 가장 느려지는 코드이며, 이제 Deki의 디바이스 빌드는 -O2를 사용합니다.

픽셀마다 함수 호출

픽셀은 memcpy(&v, p, 2)로 읽고 있었습니다. 정렬되지 않았을 수도 있는 2바이트를 읽는 이식성 있는 방법입니다. x86에서는 명령어 하나입니다. 하지만 비정렬 로드가 없는 S3의 Xtensa 코어에서 GCC는 이것을 모든 스프라이트의 모든 픽셀마다 memcpy를 호출하는 코드로 만들었습니다. 2바이트를 각각 읽어 시프트로 합치자 블리팅이 6배 빨라졌습니다.

산술 없는 틴트

틴트가 적용된 스프라이트, 예를 들어 색을 입힌 UI 패널은 범용 파이프라인을 거쳤습니다. 픽셀마다 풀기, 채널별 곱셈, 나눗셈, 블렌딩, 다시 묶기를 했습니다. 하지만 RGB565 이미지에는 빨강 32단계, 초록 64단계, 파랑 32단계밖에 없으므로, 이제 틴트는 그릴 때마다 한 번 룩업 테이블로 계산되고, 불투명 픽셀 하나는 룩업 세 번이면 됩니다. 모든 픽셀이 이전과 똑같이 나오는지 테스트로 확인합니다.

아예 틴트를 하지 않는 것이 더 좋습니다. 9-슬라이스 패널은 이미 캐시된 이미지로 늘려 두므로, 이제 틴트도 그 이미지에 함께 베이크하고, 틴트가 바뀔 때만 다시 만듭니다. 대부분 패널로 이루어진 화면은 55 fps에서 약 62 fps가 되었습니다.

프레임 하나를 보내는 데 31 ms

320x240 프레임은 150 KB입니다. 디스플레이가 처음 쓰던 40 MHz SPI 클럭에서는 전송에 31 ms가 걸렸는데, 60 fps 기준으로 거의 두 프레임 분량입니다. 패널은 80 MHz를 받아들이고, 그것으로 시간이 절반이 되었습니다.

그래도 여전히 그리기와 전송을 차례로 하고 있었고, S3의 두 번째 코어는 놀고 있었습니다. 이제 완성된 프레임은 PSRAM에 있는 두 버퍼 중 하나로 들어가고, 다음 프레임을 그리는 동안 다른 코어가 그것을 전송합니다.

타임라인: 코어 하나일 때는 프레임마다 그린 다음 전송합니다. 코어 둘일 때는 코어 0이 그리는 동안 코어 1이 이전 프레임을 전송합니다

보드는 디스플레이 설정 하나로 이 기능을 켭니다.

작은 개선들, 그리고 효과가 없었던 하나

  • 캐시 라인. 프레임 버퍼는 PSRAM에 있으며, S3의 데이터 캐시는 64바이트 라인일 때 이를 더 잘 처리합니다. 캐시를 더 키우면 더 빨라졌지만 내부 RAM을 차지해서, 스프라이트가 많은 씬에서는 메모리가 부족해 크래시가 났습니다.
  • 파티클. 각 파티클은 이미터의 이미지에 들어가는 동안 범용 파이프라인을 거쳤습니다. 이 경우를 위한 전용 루프를 만들자 분수 두 개가 프레임당 3.6 ms에서 2.4 ms가 되었습니다.
  • 더티 렉트는 효과가 없었습니다. 모든 씬에 변하는 전체 화면 배경이 있어서, 화면 전체가 항상 더티였습니다. 대부분 정지해 있는 씬을 위해 그대로 둡니다.

최종 결과

데모 씬별 초당 프레임 수: 첫 실행은 16. 지금은 모든 씬이 61.8에서 62.8 사이로 60 fps 선을 넘습니다

회색 막대는 틴트 베이크와 파티클 루프를 넣기 직전, 다른 수정은 이미 들어간 상태에서의 마지막 측정값입니다. 테스트 보드는 LilyGO T-Deck Plus였지만, 여기에 그 보드에만 해당하는 것은 없습니다. PSRAM이 있고 LovyanGFX로 SPI 패널을 구동하는 S3라면 모두 같은 경로를 탑니다.

← 모든 글