2014-10-11 13 views
20

Sau một số câu hỏi khác trên Stack Overflow, Tôi đã đọc các hướng dẫn để các bên trong của Android Surfaces, SurfaceViews, vv từ đây:Minimize Android GLSurfaceView tụt

https://source.android.com/devices/graphics/architecture.html

dẫn đó đã cho tôi một nhiều cải thiện sự hiểu biết về cách tất cả các phần khác nhau phù hợp với nhau trên Android. Nó bao gồm cách eglSwapBuffers chỉ đẩy khung được trả về vào một hàng đợi mà sau này sẽ được SurfaceFlinger tiêu thụ khi nó chuẩn bị khung tiếp theo để hiển thị. Nếu hàng đợi đầy, sau đó nó sẽ đợi cho đến khi bộ đệm có sẵn cho khung tiếp theo trước khi trở về. Tài liệu ở trên mô tả điều này là "nhồi hàng đợi" và dựa vào "áp suất ngược" của bộ đệm hoán đổi để giới hạn hiển thị đối với vsync của màn hình. Đây là những gì xảy ra khi sử dụng chế độ render liên tục mặc định của GLSurfaceView.

Nếu hiển thị của bạn đơn giản và hoàn thành ít hơn nhiều so với khung thời gian, hiệu ứng tiêu cực của việc này là độ trễ bổ sung do BufferQueue gây ra, vì sự chờ đợi trên SwapBuffers không xảy ra cho đến khi hàng đợi đầy, và do đó khung mà chúng tôi đang hiển thị luôn được đặt ở phía sau hàng đợi và do đó sẽ không được hiển thị ngay lập tức trên VSync tiếp theo vì có khả năng bộ đệm trước nó trong hàng đợi.

Ngược lại vẽ theo yêu cầu thường xảy ra ít thường xuyên hơn dòng tốc độ cập nhật màn hình, vì vậy thường là BufferQueues cho các lần xem là trống rỗng, và do đó bất kỳ bản cập nhật đẩy vào những hàng đợi sẽ được nắm lấy bởi SurfaceFlinger trên vsync hôm sau .

Vì vậy, đây là câu hỏi: Làm thế nào tôi có thể thiết lập một trình kết xuất liên tục, nhưng với độ trễ tối thiểu? Mục tiêu là hàng đợi bộ đệm trống khi bắt đầu mỗi vsync, tôi hiển thị nội dung của mình dưới 16 mili giây, đẩy nó vào hàng đợi (buffer count = 1) và sau đó được SurfaceFlinger tiêu thụ trên bộ tiếp theo (bộ đệm) = 0), lặp lại. Số lượng Bộ đệm trong hàng đợi có thể được nhìn thấy trong systrace, do đó, mục tiêu là để có sự thay thế này giữa 0 và 1.

Tài liệu tôi đề cập ở trên giới thiệu Biên đạo múa như một cách để nhận lại cuộc gọi trên mỗi đồng bộ. Tuy nhiên tôi không tin rằng đó là đủ để có thể đạt được tối thiểu hành vi tụt hậu tôi sau. Tôi đã thử nghiệm làm một requestRender() trên một cuộc gọi ngược lại với một onDrawFrame rất nhỏ() và nó thực sự thể hiện hành vi đếm bộ đệm 0/1. Tuy nhiên, nếu SurfaceFlinger không thể làm tất cả công việc của nó trong một khoảng thời gian khung hình duy nhất (có thể là một thông báo xuất hiện trong hoặc bất kỳ điều gì)? Trong trường hợp đó, tôi mong đợi trình kết xuất của tôi sẽ vui vẻ sản xuất 1 khung hình cho mỗi vsync, nhưng kết thúc người tiêu dùng của BufferQueue đó đã bỏ khung. Kết quả: chúng ta đang xen kẽ giữa 1 và 2 bộ đệm trong hàng đợi của chúng ta, và chúng ta đã đạt được một khung độ trễ giữa việc render và nhìn thấy khung.

Tài liệu dường như đề xuất xem xét thời gian bù đắp giữa thời gian so sánh được báo cáo và khi gọi lại được chạy. Tôi có thể thấy làm thế nào mà có thể giúp đỡ nếu gọi lại của bạn được gửi muộn do chủ đề chính của bạn do một vượt qua bố trí hoặc một cái gì đó. Tuy nhiên tôi không nghĩ rằng sẽ cho phép phát hiện SurfaceFlinger bỏ qua một nhịp và không tiêu thụ một khung. Có cách nào các ứng dụng có thể làm việc ra rằng SurfaceFlinger đã giảm một khung? Nó cũng có vẻ như không có khả năng nói chiều dài của hàng đợi phá vỡ ý tưởng sử dụng thời gian vsync cho các bản cập nhật trạng thái trò chơi, vì có một số lượng khung không xác định trong hàng đợi trước khi kết xuất bạn thực sự được hiển thị.

Giảm độ dài tối đa của hàng đợi và dựa vào áp suất ngược sẽ là một cách để đạt được điều này, nhưng tôi không nghĩ có API để đặt số lượng bộ đệm tối đa trong GLSurfaceView BufferQueue?

+1

Đây là một câu hỏi tuyệt vời! –

+0

Câu hỏi hay! –

Trả lời

18

Câu hỏi hay.

chút nhanh của nền cho bất cứ ai khác đọc bài viết này:

Mục tiêu ở đây là để giảm thiểu độ trễ màn hình, ví dụ: thời gian giữa khi ứng dụng làm cho một khung và khi bảng điều khiển màn hình sáng lên các pixel. Nếu bạn chỉ cần ném nội dung vào màn hình, điều đó không quan trọng, bởi vì người dùng không thể nói sự khác biệt. Tuy nhiên, nếu bạn đang phản hồi cảm biến đầu vào, mọi khung thời gian chờ sẽ làm cho ứng dụng của bạn cảm thấy ít phản hồi hơn một chút.

Sự cố tương tự với đồng bộ hóa A/V, nơi bạn cần âm thanh được liên kết với khung để đi ra loa khi khung video đang được hiển thị trên màn hình. Trong trường hợp đó, độ trễ tổng thể không quan trọng miễn là nó luôn nhất quán về cả đầu ra âm thanh và video. Điều này phải đối mặt với các vấn đề rất giống nhau, bởi vì bạn sẽ mất đồng bộ nếu các quầy hàng SurfaceFlinger và video của bạn luôn được hiển thị một khung sau đó.

SurfaceFlinger chạy ở mức ưu tiên cao và ít hoạt động tương đối, vì vậy không có khả năng bỏ lỡ nhịp đập ... nhưng điều đó có thể xảy ra. Ngoài ra, nó được tổng hợp các khung từ nhiều nguồn, một số trong đó sử dụng các hàng rào để báo hiệu hoàn thành không đồng bộ. Nếu một khung video on-time được kết hợp với đầu ra OpenGL và kết xuất GLES chưa hoàn thành khi thời hạn kết thúc, toàn bộ bố cục sẽ được hoãn lại đến VSYNC tiếp theo.

Mong muốn giảm thiểu độ trễ đủ mạnh để bản phát hành Android KitKat (4.4) giới thiệu tính năng "DispSync" trong SurfaceFlinger, giúp giảm nửa thời gian trễ của độ trễ hai khung thông thường. (Điều này được đề cập ngắn gọn trong tài liệu kiến ​​trúc đồ họa, nhưng nó không được sử dụng rộng rãi.)

Vì vậy, đó là tình huống. Trong quá khứ, vấn đề này ít hơn đối với video vì video 30fps cập nhật mọi khung hình khác. Các trục trặc tự làm việc tự nhiên bởi vì chúng tôi không cố giữ hàng đợi đầy. Chúng tôi bắt đầu thấy video 48Hz và 60Hz, vì vậy điều này quan trọng hơn.

Câu hỏi đặt ra là, làm cách nào để chúng tôi phát hiện xem khung hình mà chúng tôi gửi cho SurfaceFlinger có được hiển thị sớm nhất có thể hay đang chi tiêu thêm khung chờ đợi sau bộ đệm mà chúng tôi đã gửi trước đó?

Phần đầu tiên của câu trả lời là: bạn không thể. Không có truy vấn trạng thái hoặc gọi lại trên SurfaceFlinger sẽ cho bạn biết trạng thái của nó là gì. Về lý thuyết, bạn có thể tự truy vấn BufferQueue, nhưng điều đó không nhất thiết phải cho bạn biết những gì bạn cần biết.

Vấn đề với truy vấn và gọi lại là họ không thể cho bạn biết trạng thái , chỉ trạng thái . Vào thời điểm ứng dụng nhận được thông tin và hành động trên đó, tình hình có thể hoàn toàn khác. Ứng dụng sẽ chạy ở mức độ ưu tiên bình thường, vì vậy ứng dụng sẽ bị chậm trễ.

Đối với đồng bộ hóa A/V, điều này hơi phức tạp hơn, vì ứng dụng không thể biết các đặc tính hiển thị. Ví dụ: một số màn hình có "bảng thông minh" có bộ nhớ được tích hợp sẵn. (Nếu những gì trên màn hình không cập nhật thường xuyên, bạn có thể tiết kiệm rất nhiều năng lượng bằng cách không có bảng điều khiển quét các điểm ảnh trên xe buýt bộ nhớ 60x mỗi giây.) Đây có thể thêm một khung thời gian chờ bổ sung phải được tính.

Giải pháp mà Android đang hướng tới để đồng bộ hóa A/V là yêu cầu ứng dụng thông báo cho SurfaceFlinger khi nó muốn khung được hiển thị. Nếu SurfaceFlinger bỏ lỡ thời hạn, nó sẽ giảm khung. Điều này đã được thêm vào thực nghiệm trong 4.4, mặc dù nó không thực sự có ý định được sử dụng cho đến khi phát hành tiếp theo (nó sẽ hoạt động tốt trong "L preview", mặc dù tôi không biết nếu nó bao gồm tất cả các phần cần thiết để sử dụng nó đầy đủ).

Cách ứng dụng sử dụng cách này là gọi số máy lẻ eglPresentationTimeANDROID() trước eglSwapBuffers(). Đối số cho hàm là thời gian trình bày mong muốn, tính bằng nano giây, sử dụng cùng một thời gian như biên đạo múa (cụ thể là Linux CLOCK_MONOTONIC). Vì vậy, đối với mỗi khung, bạn lấy dấu thời gian bạn nhận được từ Biên đạo múa, thêm số khung hình mong muốn nhân với tỷ lệ làm mới gần đúng (bạn có thể nhận được bằng cách truy vấn đối tượng Hiển thị - xem MiscUtils#getDisplayRefreshNsec()) và chuyển nó đến EGL. Khi bạn trao đổi bộ đệm, thời gian trình bày mong muốn được chuyển cùng với bộ đệm.

Nhớ lại rằng SurfaceFlinger tỉnh dậy một lần cho mỗi VSYNC, xem bộ sưu tập các bộ đệm đang chờ xử lý và cung cấp một bộ phần cứng hiển thị thông qua phần cứng Composer. Nếu bạn yêu cầu hiển thị tại thời điểm T và SurfaceFlinger tin rằng một khung được chuyển tới phần cứng hiển thị sẽ hiển thị tại thời điểm T-1 trở về trước, khung sẽ được giữ lại (và khung hình trước được hiển thị lại). Nếu khung sẽ xuất hiện tại thời điểm T, nó sẽ được gửi đến màn hình. Nếu khung sẽ xuất hiện tại thời điểm T + 1 hoặc sau đó (tức là nó sẽ bỏ lỡ thời hạn), có một khung khác phía sau nó trong hàng đợi được lên lịch sau (ví dụ: khung dành cho thời gian T + 1) , sau đó khung dành cho thời gian T sẽ bị loại bỏ.

Giải pháp không hoàn toàn phù hợp với vấn đề của bạn. Đối với đồng bộ hóa A/V, bạn cần độ trễ không đổi, không phải độ trễ tối thiểu. Nếu bạn nhìn vào hoạt động "scheduled swap" của Grafika, bạn có thể tìm thấy một số mã sử dụng eglPresentationTimeANDROID() theo cách tương tự như những gì trình phát video sẽ làm. (Trong trạng thái hiện tại, nó ít hơn một "bộ tạo âm" để tạo ra đầu ra systrace, nhưng các phần cơ bản là ở đó.) Chiến lược có để render vài khung hình phía trước, vì vậy SurfaceFlinger không bao giờ chạy khô, nhưng đó chính xác là sai cho bạn ứng dụng.

Cơ chế hiển thị thời gian thực hiện, tuy nhiên, cung cấp cách thả khung hình thay vì cho phép chúng sao lưu. Nếu bạn tình cờ biết rằng có hai khung thời gian trễ giữa thời gian được Choreographer báo cáo và thời điểm khung của bạn có thể được hiển thị, bạn có thể sử dụng tính năng này để đảm bảo rằng khung sẽ bị xóa thay vì xếp hàng nếu chúng quá xa quá khứ. Hoạt động Grafika cho phép bạn thiết lập tốc độ khung hình và độ trễ được yêu cầu, và sau đó xem kết quả trong systrace.

Sẽ rất hữu ích nếu một ứng dụng biết có bao nhiêu khung thời gian SurfaceFlinger thực sự có, nhưng không có truy vấn cho điều đó. Tuy nhiên, trừ khi bạn đang làm việc trên đồng bộ hóa A/V, tất cả những gì bạn thực sự quan tâm là giảm thiểu độ trễ SurfaceFlinger. hợp lý an toàn để giả sử hai khung hình trên 4.3+. Nếu nó không phải là hai khung hình, bạn có thể có hiệu suất tối ưu, nhưng hiệu ứng ròng sẽ không tồi tệ hơn bạn sẽ nhận được nếu bạn không đặt thời gian trình bày ở tất cả.

Bạn có thể thử đặt thời gian trình bày mong muốn bằng dấu thời gian Choreographer; dấu thời gian trong quá khứ gần đây có nghĩa là "hiển thị CÀNG SỚM CÀNG TỐT". Điều này đảm bảo độ trễ tối thiểu, nhưng có thể phản tác dụng về độ mượt. SurfaceFlinger có độ trễ hai khung vì nó cung cấp mọi thứ trong hệ thống đủ thời gian để hoàn thành công việc. Nếu khối lượng công việc của bạn không đồng đều, bạn sẽ lung lay giữa độ trễ một khung và khung đôi, và đầu ra sẽ trông có vẻ lóng ngóng ở các hiệu ứng chuyển tiếp. (Đây là một mối quan tâm đối với DispSync, làm giảm tổng thời gian xuống 1.5 khung hình.)

Tôi không nhớ khi nào chức năng eglPresentationTimeANDROID() được thêm vào, nhưng trên bản phát hành cũ, nó phải là không có.

Dòng dưới cùng: cho 'L' và ở mức độ nào đó 4.4, bạn sẽ có thể nhận được hành vi bạn muốn bằng cách sử dụng phần mở rộng EGL với hai khung thời gian chờ. Trên các bản phát hành trước đó, không có sự trợ giúp nào từ hệ thống. Nếu bạn muốn đảm bảo không có bộ đệm theo cách của bạn, bạn có thể cố tình thả một khung hình một cách thường xuyên để cho hàng đợi bộ đệm thoát.

Cập nhật: một cách để tránh xếp hàng đợi khung hình là gọi eglSwapInterval(0). Nếu bạn đang gửi đầu ra trực tiếp đến màn hình, cuộc gọi sẽ vô hiệu hóa đồng bộ hóa với VSYNC, hủy giới hạn tốc độ khung hình của ứng dụng. Khi kết xuất thông qua SurfaceFlinger, điều này đặt BufferQueue vào "chế độ không đồng bộ", làm cho nó thả khung nếu chúng được gửi nhanh hơn hệ thống có thể hiển thị chúng.

Lưu ý bạn vẫn còn ba bộ đệm: một bộ đệm đang được hiển thị, một bộ đệm được giữ bởi SurfaceFlinger sẽ được hiển thị ở lần tiếp theo và một ứng dụng đang được ứng dụng vẽ vào.

+1

Cảm ơn bạn đã phản hồi chi tiết. Có SurfaceFlinger thả khung nếu có một hàng đợi xếp hàng khác có vẻ giống như hành vi phù hợp với tôi và đặt thời gian trình bày thành thời gian của âm thanh vsync như một cách hợp lý để đạt được điều đó. – tangobravo

+0

Theo dõi một số điểm khác: "Pointer Trail" khá lag với những con đường mòn dài trên Moto G của tôi trên 4.4, và nó xuất hiện bên trong SurfaceFlinger mà vẽ, vì vậy đó là một cách dễ dàng để tái tạo hành vi. Trên đối số mượt mà, ngay sau khi bạn không thể hiển thị ở 60FPS, bạn sẽ có một số khung hình hiển thị hai lần, điều này sẽ gây ra hiện tượng jank hiển thị. Trong trường hợp đó, có lẽ tốt nhất là hãy giảm xuống 30FPS và chỉ bắt đầu hiển thị mọi tên miền khác. Nếu kết xuất của bạn mất dưới 16 mili giây, thì hàng đợi đầy sẽ mua thêm một khoảng thời gian để bắt kịp bất kỳ đột biến nào. – tangobravo

+0

Vì vậy, tôi muốn nói lời khuyên về vòng lặp trò chơi tốt nhất là chọn "xếp hàng đợi" để bạn có thêm khoảng thời gian để xử lý gai nhưng với khung thời gian chờ thêm hoặc đi kèm với biên đạo múa được kích hoạt bằng thời gian trình bày cho độ trễ tối thiểu. Suy nghĩ? – tangobravo

Các vấn đề liên quan