Di chuyển tập lệnh sang OpenGL ES 3.1

Đối với tải công việc mà điện toán GPU là giải pháp lý tưởng, việc di chuyển tập lệnh RenderScript sang OpenGL ES (GLES) sẽ cho phép các ứng dụng viết bằng Kotlin, Java hoặc sử dụng NDK có thể tận dụng phần cứng GPU. Dưới đây là thông tin tổng quan cấp cao để giúp bạn sử dụng chương trình tính toán đổ bóng OpenGL ES 3.1 để thay thế tập lệnh RenderScript.

Khởi động GLES

Thay vì tạo đối tượng bối cảnh RenderScript, hãy thực hiện các bước sau để tạo bối cảnh ngoài màn hình GLES bằng EGL:

  1. Nhận màn hình mặc định

  2. Khởi động EGL bằng màn hình mặc định, chỉ định phiên bản GLES.

  3. Chọn một cấu hình EGL có kiểu bề mặt là EGL_PBUFFER_BIT.

  4. Sử dụng màn hình và cấu hình để tạo bối cảnh EGL.

  5. Tạo bề mặt ngoài màn hình bằng eglCreatePBufferSurface. Nếu bối cảnh chỉ được dùng cho mục đích điện toán, thì đây có thể là một bề mặt nhỏ (1x1).

  6. Tạo luồng kết xuất và gọi eglMakeCurrent trong luồng kết xuất đó với bối cảnh màn hình, bề mặt và EGL để liên kết bối cảnh GL với luồng đó.

Ứng dụng mẫu minh hoạ cách khởi chạy bối cảnh GLES trong GLSLImageProcessor.kt. Để tìm hiểu thêm, hãy xem bài viết EGLSurface và OpenGL ES.

Kết quả gỡ lỗi GLES

Việc nhận các lỗi từ OpenGL sẽ giúp ích cho việc sử dụng một tiện ích để bật tính năng ghi nhật ký gỡ lỗi nhằm thiết lập lệnh gọi lại cho kết quả gỡ lỗi. Phương thức để thực hiện việc này từ SDK (glDebugMessageCallbackKHR) chưa bao giờ được triển khai và sẽ gửi ra một ngoại lệ. Ứng dụng mẫu có một trình bao bọc cho lệnh gọi lại từ mã NDK.

Lớp truyền dữ liệu giữa các nhân GLES

Bạn có thể di chuyển Lớp truyền dữ liệu giữa các nhân RenderScript (RenderScript Allocation) sang Hoạ tiết bộ nhớ bất biến (Immutable storage texture) hoặc Đối tượng đệm bộ nhớ đổ bóng (Shader Storage Buffer Object). Đối với hình ảnh chỉ có thể đọc, bạn có thể dùng Đối tượng trình lấy mẫu (Sampler Object) để lọc.

Tài nguyên GLES được phân bổ trong GLES. Để tránh tình trạng bộ nhớ sao chép các tập hợp tài nguyên phát sinh gián tiếp khi tương tác với các thành phần khác của Android, có một tiện ích là KHR Images (Hình ảnh KHR) cho phép chia sẻ các mảng 2D chứa dữ liệu hình ảnh. Tiện ích này là bắt buộc đối với các thiết bị Android chạy Android 8.0 trở lên. Thư viện graphics-core (lõi đồ hoạ) Android Jetpack hỗ trợ việc tạo những hình ảnh này trong đoạn mã được quản lý và ánh xạ chúng đến một HardwareBuffer được phân bổ:

val outputBuffers = Array(numberOfOutputImages) {
  HardwareBuffer.create(
    width, height, HardwareBuffer.RGBA_8888, 1,
    HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
  )
}
val outputEGLImages = Array(numberOfOutputImages) { i ->
    androidx.opengl.EGLExt.eglCreateImageFromHardwareBuffer(
        display,
        outputBuffers[i]
    )!!
}

Thật không may là quá trình này không tạo ra hoạ tiết lưu trữ bất biến cần thiết để chương trình đổ bóng điện toán ghi trực tiếp vào vùng đệm. Mẫu này sẽ sử dụng glCopyTexSubImage2D để sao chép hoạ tiết lưu trữ mà chương trình đổ bóng điện toán sử dụng vào KHR Image. Nếu trình điều khiển OpenGL hỗ trợ tiện ích EGL Image Storage (Bộ nhớ hình ảnh EGL), thì bạn có thể dùng tiện ích đó để tạo hoạ tiết lưu trữ bất biến dùng chung nhằm tránh việc sao chép.

Chuyển đổi sang chương trình đổ bóng điện toán GLSL

Tập lệnh RenderScript sẽ được chuyển đổi thành chương trình đổ bóng điện toán GLSL.

Viết chương trình đổ bóng điện toán GLSL

Trong OpenGL ES,chương trình đổ bóng điện toán được viết trong phần tử Ngôn ngữ tô bóng OpenGL (GLSL).

Điều chỉnh tập lệnh toàn cục

Dựa trên đặc điểm của tập lệnh toàn cục, bạn có thể dùng các đối tượng đồng nhất hoặc đối tượng đệm đồng nhất cho các tập lệnh toàn cục không bị sửa đổi trong chương trình đổ bóng:

  • Vùng đệm đồng nhất: Nên dùng cho tập lệnh toàn cục được thay đổi thường xuyên có kích thước lớn hơn kích thước của hằng số đẩy.

Đối với các tập lệnh toàn cục được thay đổi trong chương trình đổ bóng, bạn có thể sử dụng Hoạ tiết bộ nhớ bất biến hoặc Đối tượng đệm lưu trữ đổ bóng (Shader Storage Buffer Object).

Thực thi tính toán

Chương trình đổ bóng điện toán không thuộc quy trình đồ hoạ, mà phục vụ cho mục đích chung và được thiết kế để tính toán các công việc có hiệu quả tải song song ở mức cao. Điều này cho phép bạn kiểm soát nhiều hơn cách thức thực thi các công việc đó, nhưng cũng có nghĩa là bạn phải hiểu thêm một chút về cách thực thi song song tác vụ của mình.

Tạo và khởi chạy chương trình điện toán

Việc tạo và khởi chạy chương trình điện toán có rất nhiều điểm tương đồng với việc sử dụng mọi chương trình đổ bóng GLES khác.

  1. Tạo chương trình và chương trình đổ bóng điện toán liên kết với chương trình đó.

  2. Đính kèm nguồn chương trình đổ bóng, biên dịch chương trình đổ bóng (và kiểm tra kết quả của quá trình biên dịch).

  3. Đính kèm chương trình đổ bóng, liên kết với chương trình và sử dụng chương trình.

  4. Tạo, khởi động và liên kết mọi đối tương đồng nhất.

Bắt đầu tính toán

Chương trình đổ bóng điện toán hoạt động trong không gian 1D, 2D hoặc 3D trừu tượng trên một loạt nhóm công việc (workgroup). Các nhóm công việc này được xác định trong mã nguồn chương trình đổ bóng và biểu thị quy mô tối thiểu trong lệnh gọi, cũng như hình dạng hình học của chương trình đổ bóng. Chương trình đổ bóng sau đây hoạt động trên hình ảnh 2D và xác định các nhóm công việc theo hai chiều:

private const val WORKGROUP_SIZE_X = 8
private const val WORKGROUP_SIZE_Y = 8
private const val ROTATION_MATRIX_SHADER =
    """#version 310 es
    layout (local_size_x = $WORKGROUP_SIZE_X, local_size_y = $WORKGROUP_SIZE_Y, local_size_z = 1) in;

Các nhóm công việc có thể chia sẻ bộ nhớ, được xác định bằng GL_MAX_COMPUTE_SHARED_MEMORY_SIZE, tối thiểu là 32 KB, và có thể sử dụng memoryBarrierShared() để cung cấp quyền truy cập bộ nhớ nhất quán.

Xác định quy mô nhóm công việc

Ngay cả trong bài tập có không gian hoạt động tốt với quy mô nhóm làm việc là 1, việc thiết lập quy mô phù hợp cho nhóm công việc việc là rất quan trọng để đồng bộ hoá chương trình đổ bóng điện toán. Chẳng hạn, nếu quy mô quá nhỏ, trình điều khiển GPU có thể không tải song song đủ phép tính của bạn. Tốt nhất là bạn nên điều chỉnh các quy mô này trên mỗi GPU, mặc dù các giá trị mặc định hợp lý sẽ hoạt động đủ hiệu quả trên các thiết bị hiện tại (ví dụ: quy mô nhóm công việc là 8x8 trong đoạn mã chương trình đổ bóng).

Có một GL_MAX_COMPUTE_WORK_GROUP_COUNT, nhưng khá đáng kể; giá trị này tối thiểu phải là 65535 ở cả 3 trục theo thông số kỹ thuật.

Điều phối chương trình đổ bóng

Bước cuối cùng trong việc thực thi các phép tính là điều phối chương trình đổ bóng bằng một trong các hàm điều phối như glDispatchCompute. Hàm điều phối sẽ chịu trách nhiệm thiết lập số lượng nhóm công việc cho mỗi trục:

GLES31.glDispatchCompute(
  roundUp(inputImage.width, WORKGROUP_SIZE_X),
  roundUp(inputImage.height, WORKGROUP_SIZE_Y),
  1 // Z workgroup size. 1 == only one z level, which indicates a 2D kernel
)

Để trả về giá trị, trước tiên hãy đợi thao tác điện toán hoàn tất bằng cách sử dụng rào cản bộ nhớ (memorybarrier):

GLES31.glMemoryBarrier(GLES31.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT)

Để liên kết nhiều nhân với nhau, (ví dụ: để di chuyển mã bằng ScriptGroup), hãy tạo và điều phối nhiều chương trình, đồng thời đồng bộ hoá quyền truy cập vào dữ liệu đầu ra bằng các rào cản bộ nhớ.

Ứng dụng mẫu minh hoạ hai tác vụ tính toán:

  • Xoay HUE: Một tác vụ điện toán bằng một chương trình đổ bóng điện toán. Hãy xem GLSLImageProcessor::rotateHue để biết mã mẫu.
  • Làm mờ: Một tác vụ điện toán phức tạp, thực thi tuần tự hai chương trình đổ bóng điện toán. Hãy xem GLSLImageProcessor::blur để biết mã mẫu.

Để tìm hiểu thêm về các rào cản bộ nhớ, hãy tham khảo Đảm bảo khả năng hiển thị cũng như Biến dùng chung của Google.