기여 목록 / 8448731
[viz] Don't release presented overlays on failed swaps
Chromium viz 계층에서 swap이 실패했을 때, 여전히 화면에 표시 중인 오버레이의 접근 권한이 파괴되던 버그를 수정했다. DoFinishSwapBuffers()가 swap 결과를 인자로 받으면서도 읽지 않고 이전 프레임의 오버레이를 무조건 해제하던 것이 원인이었다. 해제 대상을 결정하는 시점을 제출(Present())에서 완료(DoFinishSwapBuffers())로 옮겼고, 리뷰를 거쳐 같은 판단을 하는 viz 쪽 SkiaRenderer까지 맞춰 해결했다.
문제 설명
- 오버레이는 하드웨어에 제출된 뒤에도 다음 프레임이 덮어쓸 때까지 계속 화면에 표시된다. 그래서 해제를 한 프레임 미뤄야 하고,
SkiaOutputDeviceBufferQueue는 이를 참조 카운트로 관리한다. Unref()는 카운트를 하나 줄이는 것에서 끝나지 않고 representation과 scoped read access를 파괴한다.
원인
Present()가 해제 대상 목록을 완료 콜백에 바인딩한다. base::BindOnce는 인자를 자기 내부 필드로 이동시켜 보관하므로 이 시점에 해제 대상이 고정된다. 그래서 DoFinishSwapBuffers()는 result.swap_result를 인자로 받고도 읽지 않았다. 읽어봐야 바꿀 수 없었기 때문이다.
쉽게 생각했을 때 if문 하나를 추가하는 것으로 해결하고자 하였으나 해결되지 않았다. 실패했을 때 해제해야 하는 것은 이번 프레임의 목록인데 그 목록은 다음 콜백에 바인딩될 예정이라 이 함수에서는 접근할 수 없었다.
증상
swap이 실패하면 그 프레임은 화면에 표시되지 못하므로 이전 프레임(A)이 그대로 표시되고 있다. 그런데 코드는 바로 그 A의 접근 권한을 끝낸다. 반대로 화면에 표시된 적 없는 오버레이(B)는 다음 swap까지 접근 권한을 유지하고 있다.
해결 내용
1. 겹친 의미를 가진 변수를 분리하고 결정을 완료 시점으로 이동
committed_overlay_mailboxes_ 하나가 제출됨과 표시 중을 같이 뜻하고 있었다. swap이 성공하는 동안에는 두 의미가 같지만 실패하면 달라지는데, 변수가 하나뿐이라 그 상태를 표현할 수 없었다.
따라서 pending, submitted, presented 세 단계로 나눴다. 중간 단계는 base::queue로 두어 부모 클래스의 SkiaOutputDevice::pending_swaps_와 같은 순서로 꺼낸다.
// Present() — 결정하지 않고 큐에 적재만
submitted_overlay_mailboxes_.push(std::move(pending_overlay_mailboxes_));
pending_overlay_mailboxes_.clear();
// DoFinishSwapBuffers() — 결과를 아는 지금 판단
if (result.swap_result != gfx::SwapResult::SWAP_FAILED) {
released_overlay_mailboxes = std::move(presented_overlay_mailboxes_);
presented_overlay_mailboxes_ = std::move(swapped_overlay_mailboxes);
} else {
// 화면은 그대로, 못 뜬 이번 것을 해제
released_overlay_mailboxes = std::move(swapped_overlay_mailboxes);
}2. 리뷰 반영: viz 쪽 판단까지 일치시키기
처음에는 SWAP_ACK만 성공으로 보는 화이트리스트를 택해 SWAP_FAILED와 SWAP_NAK_RECREATE_BUFFERS를 모두 "표시되지 않음"으로 처리했다. 리뷰에서 같은 swap에 대해 viz 쪽 SkiaRenderer::SwapBuffersComplete()도 무엇이 표시 중인지 따로 판단하고 있고, 두 판단이 맞아야 한다는 지적을 받았다. SkiaRenderer는 SWAP_SKIPPED일 때만 did_present를 false로 보고 있었기 때문에, GPU 쪽만 바꾸면 실패한 swap에서 viz는 A의 lock을 풀고 BufferQueue에서 A를 재사용 가능으로 돌려놓는 반면 GPU 쪽은 A를 계속 붙잡는 불일치가 생겼다.
같은 문제를 SWAP_SKIPPED에 대해 해결한 선례(CL 6071903)가 viz 쪽을 GPU 쪽에 맞춘 방식을 따라, did_present가 SWAP_FAILED에서도 false가 되도록 바꿨다. 반면 SWAP_NAK_RECREATE_BUFFERS는 플랫폼마다 의미가 달라(DRM에서는 page flip 커밋 전 반환이지만 Wayland에서는 이전 프레임의 release fence와 함께 온다) did_present = false 경로의 release fence CHECK에 걸릴 수 있었다. 그래서 변경 범위를 SWAP_FAILED로 좁혀 양쪽 모두 NAK는 이전처럼 표시된 것으로 두었다. 결과적으로 두 쪽이 모든 SwapResult에 대해 같은 판단을 한다.
테스트 방법
회귀 테스트
ScheduleOverlaysSwapFailed를 추가했다.<Figure 3>의 시나리오를 그대로 코드로 옮겼다. 또한, 기존MockPresenter::SwapComplete()가SWAP_ACK을 하드코딩하고 있어서 실패한 swap을 흉내낼 수 없었고, 결과를 인자로 받도록 고쳤다.범위를 좁힌 뒤
ScheduleOverlaysSwapNak를 추가해SWAP_NAK_RECREATE_BUFFERS가SWAP_ACK과 같이 동작하는 것을 고정했다.테스트가 실제로 버그를 잡는지 역으로 확인했다. 수정 로직만 임시로 되돌리자 정확히 세 지점에서 실패했다.
HasOverlay(A) → false ← 화면에 떠 있는데 파괴됨
HasOverlay(B) → true ← 못 뜬 건데 살아있음- 로컬에서
SkiaOutputDeviceBufferQueueTest,BufferQueueTest와 디스플레이·오버레이 관련viz_unittests가 통과했다.
실제 SWAP_FAILED는 주로 ChromeOS DRM 경로에서 발생하기 때문에 로컬 macOS 테스트로는 검증되지 않는다. CQ의 빌더로 확인했고, 26.10.22에 CQ를 통과해 머지되었다(첫 dry run은 Android Pixel 6의 context_lost_passthrough_graphite_tests에서 실패했고 이어진 CQ 실행에서는 통과했다).
배운 점
- 표면적으로는
if문이 빠진 것처럼 보이지만, 실제로는 결정을 내리는 시점이 정보가 도착하는 시점보다 앞서 있는 구조 문제였다(<Figure 2>). 이런 경우에는 분기를 추가하려 해도 한쪽 가지에 넣을 데이터가 존재하지 않는다. - 이름 하나가 두 의미를 겸하면 표현할 수 없는 상태가 생긴다. 변수를 나눈 것이 리팩터링이 아니라 수정의 일부였던 이유다.
- 같은 사실("지금 무엇이 화면에 있는가")을 viz 쪽과 GPU 쪽이 각자 판단하고 있었다. 한쪽만 고치면 둘이 어긋나므로, 판단하는 코드가 어디에 더 있는지 먼저 찾아야 한다(
<Figure 5>). - 같은 이름의 결과값이라도 플랫폼마다 의미가 다를 수 있다.
SWAP_NAK_RECREATE_BUFFERS가 그랬고, 범위를 확신할 수 있는SWAP_FAILED로 좁혀 리뷰를 통과했다. SkiaOutputSurfaceImplOnGpu::PostSubmit()의 skip 경로는Present()를 거치지 않아pending_overlay_mailboxes_가 비워지지 않는다.Reshape()의DCHECK가 실패할 수 있어 별도 이슈로 낼 만하다.
참고 자료
- 이슈 40248635, CL 8448731
skia_output_device_buffer_queue.cc(수정 대상)skia_renderer.cc(did_present판단)skia_output_device.cc(pending_swaps_FIFO 큐)ui/gfx/swap_result.h(SwapResult의미)- CL 6071903 (
SWAP_SKIPPED에 대한 선례), CL 6320593 (Windows DComp의 실패 swap 처리)