Tổng quan về RenderScript

RenderScript là một khung để chạy các tác vụ tính toán chuyên sâu với hiệu suất cao trên Android. RenderScript được định hướng chủ yếu để sử dụng với tính năng tính toán song song dữ liệu (data-parallel computation), dùng vậy khối lượng công việc nối tiếp (serial workloads) cũng có thể hưởng lợi. Môi trường thời gian chạy RenderScript tải song song công việc trên các bộ xử lý hiện có trên thiết bị, chẳng hạn như GPU và CPU đa nhân. Điều này cho phép bạn tập trung vào việc thể hiện thuật toán thay vì lên lịch công việc. RenderScript đặc biệt hữu ích cho các ứng dụng xử lý hình ảnh, nhiếp ảnh điện toán (computational photography) hoặc thị giác máy tính (computer vision).

Để bắt đầu sử dụng RenderScript, bạn nên tìm hiểu hai khái niệm chính sau đây:

  • Ngôn ngữ (language) là một ngôn ngữ bắt nguồn từ C99 để viết mã tính toán hiệu suất cao. Viết RenderScript Kernel mô tả cách sử dụng đối tượng này để viết hạt nhân tính toán.
  • API kiểm soát được dùng để quản lý vòng đời của tài nguyên RenderScript cũng như kiểm soát quá trình thực thi hạt nhân. Tính năng này được hỗ trợ bằng ba ngôn ngữ: Java, C++ trong Android NDK và chính ngôn ngữ của hạt nhân bắt nguồn từ C99. Sử dụng RenderScript qua Mã JavaRenderScript đơn nguồn tương ứng với cách thứ nhất và thứ ba.

Viết hạt nhân RenderScript

Hạt nhân RenderScript thường nằm trong tệp .rs trong thư mục <project_root>/src/rs; mỗi tệp .rs được gọi là một tập lệnh (script). Mỗi tập lệnh chứa tập hợp hạt nhân, hàm và biến riêng. Tập lệnh có thể chứa:

  • Một phần khai báo pragma (#pragma version(1)) khai báo phiên bản của ngôn ngữ hạt nhân RenderScript dùng trong tập lệnh này. Hiện tại, 1 là giá trị hợp lệ duy nhất.
  • Một phần khai báo pragma (#pragma rs java_package_name(com.example.app)) khai báo tên gói của lớp (class) Java được phản ánh qua tập lệnh này. Xin lưu ý rằng tệp .rs của bạn phải thuộc gói ứng dụng chứ không phải trong dự án thư viện.
  • Không có hoặc có hàm không gọi được (invokable function). Hàm không gọi được là một hàm RenderScript đơn luồng mà bạn có thể gọi qua mã Java bằng các đối số tuỳ ý. Các chỉ số này thường hữu ích cho quá trình thiết lập ban đầu hoặc tính toán nối tiếp trong một quy trình xử lý lớn hơn.
  • Không có hoặc có tập lệnh toàn cục (script global). Tập lệnh toàn cục tương tự như một biến toàn cục trong C. Bạn có thể truy cập vào các tập lệnh toàn cục qua mã Java và các lệnh này thường được dùng để truyền tham số đến hạt nhân RenderScript. Bạn có thể xem thông tin giải thích chi tiết về tập lệnh toàn cục tại đây.

  • Không có hoặc có hạt nhân tính toán (compute kernel). Hạt nhân tính toán là một hàm hoặc tập hợp hàm mà bạn có thể ra lệnh cho thời gian chạy RenderScript thực thi song song trên một tập hợp dữ liệu. Có hai loại hạt nhân tính toán: hạt nhân ánh xạ (mapping kernel, còn gọi là hạt nhân foreach) và hạt nhân rút gọn (reduction kernel).

    Hạt nhân ánh xạ là một hàm song song hoạt động trên tập hợp Allocations cùng chiều (dimension). Theo mặc định, hạt nhân này thực thi một lần cho mọi toạ độ trong các chiều đó. Hạt nhân này thường dùng để (nhưng không chỉ để) chuyển đổi một tập hợp Allocations đầu vào thành một đầu ra Allocation, một Element mỗi lần.

    • Sau đây là ví dụ về một hạt nhân ánh xạ đơn giản:

      uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) {
        uchar4 out = in;
        out.r = 255 - in.r;
        out.g = 255 - in.g;
        out.b = 255 - in.b;
        return out;
      }

      Trong hầu hết trường hợp, hàm này giống hệt với hàm C tiêu chuẩn. Thuộc tính RS_KERNEL được áp dụng cho nguyên mẫu hàm chỉ định rằng hàm này là một hạt nhân ánh xạ RenderScript thay vì một hàm có thể gọi. Đối số in được tự động điền dựa trên dữ liệu đầu vào Allocation truyền vào khi khởi chạy hạt nhân. Các đối số xy sẽ được thảo luận bên dưới. Giá trị mà nhân trả về sẽ được tự động ghi vào vị trí thích hợp trong đầu ra Allocation. Theo mặc định, hạt nhân hệ điều hành này sẽ chạy trên toàn bộ đầu vào Allocation, trong đó có một lượt thực thi của hàm hạt nhân trên Element trong Allocation.

      Mỗi hạt nhân ánh xạ có thể có một hoặc nhiều đầu vào Allocations, một đầu ra Allocation hoặc cả hai. Môi trường thời gian chạy RenderScript đảm bảo rằng tất cả đầu vào và đầu ra đều ở cùng chiều và Allocations loại Element đầu vào và đầu ra khớp với nguyên mẫu của hạt nhân. Nếu một trong hai cách kiểm tra này không thành công, thì RenderScript sẽ gửi ra một trường hợp ngoại lệ.

      LƯU Ý: Trước Android 6.0 (API cấp 23), mỗi hạt nhân ánh xạ có thể không được có nhiều hơn một đầu vào Allocation.

      Nếu bạn cần nhiều Allocations đầu vào hoặc đầu ra hơn so với hạt nhân, thì các đối tượng đó phải được liên kết với tập lệnh toàn cục rs_allocation và được truy cập qua một nhân hoặc hàm gọi được qua rsGetElementAt_type() hoặc rsSetElementAt_type().

      LƯU Ý: RS_KERNEL là một macro được RenderScript tự động xác định để thuận tiện cho bạn:

      #define RS_KERNEL __attribute__((kernel))
      

    Hạt nhân rút gọn là một nhóm hàm hoạt động trên một tập hợp đầu vào Allocations cùng chiều. Theo mặc định, hàm tích luỹ sẽ thực thi một lần cho mọi toạ độ trong những chiều đó. Hạt nhân này thường dùng để (nhưng không chỉ để) "rút gọn" một tập hợp đầu vào Allocations thành một giá trị duy nhất.

    • Sau đây là ví dụ về một hạt nhân rút gọn đơn giản cộng thêm Elements của đầu vào:

      #pragma rs reduce(addint) accumulator(addintAccum)
      
      static void addintAccum(int *accum, int val) {
        *accum += val;
      }

      Hạt nhân rút gọn bao gồm một hoặc nhiều hàm do người dùng viết. #pragma rs reduce được dùng để xác định hạt nhân bằng cách chỉ định tên của hạt nhân đó (trong ví dụ này là addint) và tên cũng như vai trò của các hàm tạo nên hạt nhân đó (trong ví dụ này là hàm accumulator addintAccum). Tất cả những hàm đó đều phải là static. Hạt nhân luôn đòi hỏi hàm accumulator; dù loại hạt nhân này cũng có thể có các hàm khác, tuỳ thuộc vào hàm mà bạn muốn hạt nhân thực hiện.

      Hàm tích luỹ hạt nhân rút gọn phải trả về void và phải có ít nhất 2 đối số. Đối số đầu tiên (trong ví dụ này là accum) là một con trỏ tới mục dữ liệu tích luỹ và đối số thứ hai (trong ví dụ này là val) sẽ được tự động điền dựa trên dữ liệu đầu vào Allocation được truyền đến quy trình chạy nhân. Mục dữ liệu tích luỹ được tạo bởi thời gian chạy RenderScript; theo mặc định, ứng dụng sẽ khởi chạy bằng không. Theo mặc định, hạt nhân hệ điều hành này sẽ chạy trên toàn bộ đầu vào Allocation, trong đó có một lượt thực thi của hàm tích luỹ trên Element trong Allocation. Theo mặc định, giá trị cuối cùng của mục dữ liệu tích luỹ được coi là kết quả của việc rút gọn và được trả về Java. Môi trường thời gian chạy RenderScript kiểm tra để đảm bảo rằng loại Element của Allocation khớp với nguyên mẫu của hàm tích luỹ; nếu không khớp, RenderScript sẽ gửi ra một ngoại lệ.

      Hạt nhân rút gọn có một hoặc nhiều đầu vào Allocations nhưng không có đầu ra Allocations.

      Bạn có thể xem thông tin giải thích chi tiết hơn về hạt nhân rút gọn tại đây.

      Hạt nhân rút gọn được hỗ trợ trên Android 7.0 (API cấp 24) trở lên.

    Hàm hạt nhân ánh xạ hoặc hàm tích luỹ hạt nhân rút gọn có thể truy cập vào toạ độ của cách thực thi hiện tại bằng cách sử dụng các đối số đặc biệt x, yz (phải thuộc loại int hoặc uint32_t). Những đối số này là không bắt buộc.

    Hàm hạt nhân ánh xạ hoặc hàm tích luỹ hạt nhân rút gọn cũng có thể lấy đối số đặc biệt không bắt buộc context thuộc kiểu rs_kernel_context. Cần có một bộ API thời gian chạy dùng để truy vấn một số thuộc tính nhất định của lượt thực thi hiện tại – ví dụ: rsGetDimX. (Đối số context có trong Android 6.0 (API cấp 23) trở lên.)

  • Một hàm init() không bắt buộc. Hàm init() là một hàm gọi được loại đặc biệt mà hàm RenderScript chạy trong lần đầu tập lệnh tạo thực thể. Điều này cho phép một số quy trình tính toán tự động thực hiện khi tạo tập lệnh.
  • Không có hoặc có hàm và tập lệnh toàn cục tĩnh. Tập lệnh toàn cục tĩnh tương đương với một tập lệnh toàn cục ngoại trừ việc không thể truy cập được qua mã Java. Hàm tĩnh là một hàm C chuẩn có thể được gọi từ bất kỳ hàm hạt nhân hoặc hàm gọi được nào trong tập lệnh nhưng không được hiển thị cho API Java. Nếu một hàm hoặc tập lệnh toàn cục không cần được truy cập qua mã Java, bạn nên khai báo static.

Đặt độ chính xác cho dấu phẩy động

Bạn có thể kiểm soát cấp độ chính xác cần thiết của dấu phẩy động trong một tập lệnh. Điều này sẽ hữu ích nếu không cần đến tiêu chuẩn IEEE 754-2008 đầy đủ (dùng theo mặc định). Những pragma sau đây có thể đặt nhiều mức độ chính xác cho dấu phẩy động:

  • #pragma rs_fp_full (mặc định nếu không được chỉ định): Đối với ứng dụng yêu cầu độ chính xác dấu phẩy động theo tiêu chuẩn IEEE 754-2008.
  • #pragma rs_fp_relaxed: Đối với ứng dụng không yêu cầu tuân thủ nghiêm ngặt IEEE 754-2008 và có thể chấp nhận mức độ chính xác thấp hơn. Chế độ này bật tính năng flush-to-zero cho denorm và round-towards-zero.
  • #pragma rs_fp_imprecise: Đối với ứng dụng không đáp ứng được các yêu cầu nghiêm ngặt về độ chính xác. Chế độ này cho phép mọi nội dung trong rs_fp_relaxed cùng với các tính năng sau:
    • Các thao tác dẫn đến -0.0 có thể trả về +0.0.
    • Thao tác trên INF và NAN chưa được xác định.

Hầu hết ứng dụng đều có thể sử dụng rs_fp_relaxed mà không có tác dụng phụ nào. Điều này có thể mang lại nhiều lợi ích trên một số cấu trúc do có các tính năng tối ưu hoá bổ sung chỉ hoạt động với độ chính xác tương đối (chẳng hạn như các lệnh SIMD của CPU).

Truy cập API RenderScript qua Java

Khi phát triển một ứng dụng Android sử dụng RenderScript, bạn có thể truy cập API của ứng dụng đó qua Java theo một trong hai cách sau:

Sau đây là một số đánh đổi:

  • Nếu bạn sử dụng API Thư viện hỗ trợ, phần RenderScript của ứng dụng sẽ tương thích với thiết bị chạy Android 2.3 (API cấp 9) trở lên, cho dù bạn sử dụng tính năng RenderScript nào. Điều này cho phép ứng dụng của bạn hoạt động trên nhiều thiết bị hơn so với khi bạn sử dụng API (android.renderscript) gốc.
  • Một số tính năng của RenderScript không được cung cấp bởi API Thư viện hỗ trợ.
  • Nếu sử dụng API Thư viện hỗ trợ, bạn sẽ nhận được các APK lớn hơn (có thể lớn hơn đáng kể) so với API gốc (android.renderscript).

Sử dụng API Thư viện hỗ trợ RenderScript

Để sử dụng các API Thư viện hỗ trợ RenderScript, bạn phải định cấu hình môi trường phát triển để có thể truy cập vào các API đó. Bạn cần có các công cụ SDK Android sau đây để sử dụng những API này:

  • Bản sửa đổi Bộ công cụ SDK Android 22.2 trở lên
  • Bản sửa đổi SDK Android Build-tools 18.1.0 trở lên

Lưu ý rằng kể từ phiên bản Android SDK Build-tools 24.0.0, Android 2.2 (API cấp 8) không còn được hỗ trợ nữa.

Bạn có thể kiểm tra và cập nhật phiên bản đã cài đặt của các công cụ này trong Trình quản lý SDK Android.

Cách sử dụng API Thư viện hỗ trợ RenderScript:

  1. Đảm bảo bạn đã cài đặt phiên bản SDK Android cần thiết.
  2. Cập nhật chế độ cài đặt cho quy trình xây dựng Android để bao gồm các chế độ cài đặt RenderScript:
    • Mở tệp build.gradle trong thư mục ứng dụng của mô-đun ứng dụng.
    • Thêm các chế độ cài đặt RenderScript sau đây vào tệp:

      Groovy

              android {
                  compileSdkVersion 33
      
                  defaultConfig {
                      minSdkVersion 9
                      targetSdkVersion 19
      
                      renderscriptTargetApi 18
                      renderscriptSupportModeEnabled true
                  }
              }
              

      Kotlin

              android {
                  compileSdkVersion(33)
      
                  defaultConfig {
                      minSdkVersion(9)
                      targetSdkVersion(19)
      
                      renderscriptTargetApi = 18
                      renderscriptSupportModeEnabled = true
                  }
              }
              

      Các chế độ cài đặt nêu trên kiểm soát hành vi cụ thể trong quá trình xây dựng Android:

      • renderscriptTargetApi – Xác định phiên bản mã byte sẽ được tạo. Bạn nên thiết lập giá trị này ở cấp độ API thấp nhất có thể để cung cấp tất cả chức năng hiện đang sử dụng và thiết lập renderscriptSupportModeEnabled thành true. Giá trị hợp lệ cho chế độ cài đặt này là mọi giá trị số nguyên từ 11 đến cấp API được phát hành gần đây nhất. Nếu phiên bản SDK tối thiểu được chỉ định trong tệp kê khai ứng dụng được thiết lập thành một giá trị khác, thì giá trị đó sẽ bị bỏ qua và giá trị mục tiêu trong tệp bản dựng sẽ được dùng để thiết lập phiên bản SDK tối thiểu.
      • renderscriptSupportModeEnabled – Chỉ định rằng mã byte được tạo sẽ quay lại phiên bản tương thích nếu thiết bị đang chạy không hỗ trợ phiên bản mục tiêu.
  3. Trong các lớp ứng dụng sử dụng RenderScript, hãy thêm một tệp nhập cho các lớp Thư viện hỗ trợ:

    Kotlin

    import android.support.v8.renderscript.*
    

    Java

    import android.support.v8.renderscript.*;
    

Sử dụng RenderScript qua mã Java hoặc Kotlin

Việc sử dụng RenderScript qua mã Java hoặc Kotlin dựa trên các lớp API nằm trong gói android.renderscript hoặc gói android.support.v8.renderscript. Hầu hết ứng dụng đều tuân theo cùng một kiểu sử dụng cơ bản:

  1. Khởi chạy ngữ cảnh RenderScript. Bối cảnh RenderScript, được tạo bằng create(Context), đảm bảo rằng RenderScript có thể sử dụng và cung cấp một đối tượng để kiểm soát toàn thời gian của tất cả đối tượng RenderScript tiếp theo. Bạn nên coi việc tạo bối cảnh là một hoạt động có thể tồn tại lâu dài, vì hoạt động này có thể tạo ra các tài nguyên trên nhiều phần cứng; nó không nên nằm trong đường dẫn quan trọng của ứng dụng nếu có thể. Thông thường, mỗi ứng dụng sẽ chỉ có một bối cảnh RenderScript duy nhất tại mỗi thời điểm.
  2. Tạo ít nhất một Allocation để truyền vào một tập lệnh. Allocation là một đối tượng RenderScript, cung cấp dung lượng lưu trữ cho một lượng dữ liệu cố định. Hạt nhân trong các tập lệnh lấy đối tượng Allocation làm đầu vào và đầu ra, đồng thời đối tượng Allocation có thể được truy cập trong các hạt nhân bằng cách sử dụng rsGetElementAt_type()rsSetElementAt_type() khi được liên kết như các tập lệnh toàn cục. Đối tượng Allocation cho phép truyền mảng qua mã Java đến mã RenderScript và ngược lại. Đối tượng Allocation thường được tạo bằng cách sử dụng createTyped() hoặc createFromBitmap().
  3. Tạo bất kỳ tập lệnh nào cần thiết. Hiện có hai loại tập lệnh khi bạn sử dụng RenderScript:
    • ScriptC: Đây là các tập lệnh do người dùng xác định như được mô tả trong phần Viết hạt nhân RenderScript nêu trên. Mỗi tập lệnh có một lớp Java được trình biên dịch RenderScript phản ánh để giúp bạn dễ dàng truy cập tập lệnh qua mã Java; lớp này có tên là ScriptC_filename. Ví dụ: nếu hạt nhân ánh xạ ở trên nằm trong invert.rs và ngữ cảnh RenderScript đã nằm trong mRenderScript, thì mã Java hoặc Kotlin để tạo tập lệnh sẽ là:

      Kotlin

      val invert = ScriptC_invert(renderScript)
      

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic: Đây là các hạt nhân RenderScript tích hợp sẵn cho các thao tác phổ biến, chẳng hạn như làm mờ Gaussian, tích chập và kết hợp hình ảnh. Để biết thêm thông tin, hãy xem các lớp con của ScriptIntrinsic.
  4. Điền dữ liệu vào Allocations. Ngoại trừ Allocations được tạo bằng createFromBitmap(), Allocations được điền sẵn dữ liệu trống trong lần tạo đầu tiên. Để điền sẵn Allocations, hãy sử dụng một trong các phương thức "sao chép" trong Allocation. Các phương thức "sao chép" này có tính đồng bộ.
  5. Đặt mọi tập lệnh toàn cục cần thiết. Bạn có thể đặt tập lệnh toàn cục bằng các phương thức trong cùng một lớp ScriptC_filename tên là set_globalname. Ví dụ: để đặt biến int tên là threshold, hãy sử dụng phương thức Javaset_threshold(int); và để đặt biến rs_allocation tên là lookup, hãy sử dụng phương thức Java set_lookup(Allocation). Các phương thức set không đồng bộ.
  6. Khởi chạy các hạt nhân thích hợp và các hàm gọi được.

    Các phương thức để khởi chạy một hạt nhân nhất định được phản ánh trong cùng một lớp ScriptC_filename bằng các phương thức có tên forEach_mappingKernelName() hoặc reduce_reductionKernelName(). Các lần khởi chạy này không đồng bộ. Tuỳ thuộc vào các đối số cho hạt nhân, phương thức sẽ lấy một hoặc nhiều Allocations, tất cả đều phải ở cùng một chiều. Theo mặc định, một hạt nhân thực thi trên mọi toạ độ trong các chiều đó; để thực thi một hạt nhân trên một tập hợp con của các toạ độ đó, hãy truyền Script.LaunchOptions phù hợp làm đối số cuối cùng cho phương thức forEach hoặc reduce.

    Chạy các hàm có thể gọi bằng các phương thức invoke_functionName đã phản ánh trong cùng một lớp ScriptC_filename. Các lần khởi chạy này không đồng bộ.

  7. Truy xuất dữ liệu từ đối tượng Allocation và đối tượng javaFutureType. Để truy cập vào dữ liệu từ Allocation trong mã Java, bạn phải sao chép dữ liệu đó trở lại Java bằng một trong các phương thức "sao chép" trong Allocation. Để nhận được kết quả của hạt nhân rút gọn, bạn phải sử dụng phương thức javaFutureType.get(). Phương thức "sao chép" và get() có tính đồng bộ.
  8. Phân tích ngữ cảnh RenderScript. Bạn có thể huỷ bối cảnh RenderScript bằng destroy() hoặc bằng cách cho phép đối tượng RenderScript thu thập rác. Điều này khiến mọi trường hợp sử dụng sau này của bất cứ đối tượng nào thuộc về bối cảnh đó có thể gửi ra một ngoại lệ.

Mô hình thực thi không đồng bộ

Các phương thức forEach, invoke, reduceset được phản ánh có tính không đồng bộ – mỗi phương thức có thể quay lại Java trước khi hoàn tất hành động được yêu cầu. Tuy nhiên, các thao tác riêng lẻ được chuyển đổi tuần tự theo thứ tự khởi động.

Lớp Allocation cung cấp các phương thức "sao chép" để sao chép dữ liệu vào và từ Allocations. Phương thức "sao chép" có tính đồng bộ và được tuần tự hoá theo mọi hành động không đồng bộ ở trên chạm vào cùng một Allocations.

Các lớp javaFutureType được phản ánh cung cấp phương thức get() để lấy kết quả rút gọn. get() có tính đồng bộ và được chuyển đổi tuần tự theo mức rút gọn (không đồng bộ).

RenderScript đơn nguồn

Android 7.0 (API cấp 24) giới thiệu một tính năng lập trình mới có tên là RenderScript đơn nguồn (Single-Source RenderScript), trong đó các hạt nhân được khởi chạy từ tập lệnh mà chúng được định nghĩa chứ không phải từ Java. Phương pháp này hiện giới hạn ở hạt nhân ánh xạ, trong mục này chỉ gọi là "hạt nhân" cho ngắn gọn. Tính năng mới này cũng hỗ trợ việc tạo Allocations thuộc loại rs_allocation từ bên trong tập lệnh. Bây giờ, bạn có thể triển khai toàn bộ thuật toán chỉ trong một tập lệnh, ngay cả khi cần khởi chạy nhiều hạt nhân. Lợi ích nhận được là gấp đôi: dễ đọc hơn vì mã này giúp triển khai thuật toán bằng một ngôn ngữ; và có thể viết mã nhanh hơn, vì có ít chuyển đổi giữa Java và RenderScript giữa nhiều lần khởi chạy hạt nhân.

Trong RenderScript đơn nguồn, bạn viết các hạt nhân như mô tả trong phần Viết hạt nhân RenderScript. Sau đó, bạn có thể viết một hàm không gọi được để gọi rsForEach() để chạy các hàm đó. API đó nhận một hàm hạt nhân làm thông số đầu tiên, sau đó là các allocations đầu vào và đầu ra. API tương tự rsForEachWithOptions() sẽ lấy thêm một đối số loại rs_script_call_t để chỉ định một tập hợp con phần tử từ allocations đầu vào và đầu ra cho hạt nhân để xử lý.

Để bắt đầu tính toán RenderScript, bạn gọi hàm gọi được qua Java. Làm theo các bước trong phần Sử dụng RenderScript qua mã Java. Trong bước khởi chạy hạt nhân thích hợp, hãy gọi hàm không gọi được bằng cách sử dụng invoke_function_name(). Hàm này sẽ khởi động toàn bộ quá trình tính toán, bao gồm cả việc khởi chạy các hạt nhân.

Allocations thường cần thiết để lưu và truyền kết quả trung gian từ lần khởi chạy hạt nhân này lần khởi chạy hạt nhân khác. Bạn có thể tạo các đối tượng này bằng cách sử dụng rsCreateAllocation(). Một dạng dễ sử dụng của API đó là rsCreateAllocation_<T><W>(…), trong đó T là loại dữ liệu cho phần tử và W là chiều rộng vectơ cho phần tử. API sẽ lấy kích thước trong các chiều X, Y và Z làm đối số. Đối với các mô hình Allocations 1D hoặc 2D, bạn có thể bỏ qua kích thước của chiều Y hoặc Z. Ví dụ: rsCreateAllocation_uchar4(16384) tạo allocations 1D gồm 16384 phần tử, mỗi phần tử thuộc loại uchar4.

Allocations sẽ do hệ thống quản lý tự động. Bạn không cần phải huỷ bỏ hoặc giải phóng một cách rõ ràng. Tuy nhiên, bạn có thể gọi rsClearObject(rs_allocation* alloc) để cho biết là bạn không cần xử lý alloc sang mô hình phân bổ cơ bản để hệ thống có thể giải phóng tài nguyên sớm nhất có thể.

Phần Viết hạt nhân RenderScript có một ví dụ về hạt nhân đảo ngược hình ảnh. Ví dụ dưới đây mở rộng cách áp dụng nhiều hiệu ứng cho một hình ảnh, bằng cách sử dụng RenderScript đơn nguồn. Tính năng này bao gồm một hạt nhân khác là greyscale, biến hình ảnh màu thành đen trắng. Sau đó, một hàm process() không gọi được sẽ áp dụng hai hạt nhân đó liên tục đến một hình ảnh đầu vào và tạo ra một hình ảnh đầu ra. Allocations cho cả đầu vào và đầu ra được truyền vào dưới dạng các đối số loại rs_allocation.

// File: singlesource.rs

#pragma version(1)
#pragma rs java_package_name(com.android.rssample)

static const float4 weight = {0.299f, 0.587f, 0.114f, 0.0f};

uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) {
  uchar4 out = in;
  out.r = 255 - in.r;
  out.g = 255 - in.g;
  out.b = 255 - in.b;
  return out;
}

uchar4 RS_KERNEL greyscale(uchar4 in) {
  const float4 inF = rsUnpackColor8888(in);
  const float4 outF = (float4){ dot(inF, weight) };
  return rsPackColorTo8888(outF);
}

void process(rs_allocation inputImage, rs_allocation outputImage) {
  const uint32_t imageWidth = rsAllocationGetDimX(inputImage);
  const uint32_t imageHeight = rsAllocationGetDimY(inputImage);
  rs_allocation tmp = rsCreateAllocation_uchar4(imageWidth, imageHeight);
  rsForEach(invert, inputImage, tmp);
  rsForEach(greyscale, tmp, outputImage);
}

Bạn có thể gọi hàm process() qua Java hoặc Kotlin như sau:

Kotlin

val RS: RenderScript = RenderScript.create(context)
val script = ScriptC_singlesource(RS)
val inputAllocation: Allocation = Allocation.createFromBitmapResource(
        RS,
        resources,
        R.drawable.image
)
val outputAllocation: Allocation = Allocation.createTyped(
        RS,
        inputAllocation.type,
        Allocation.USAGE_SCRIPT or Allocation.USAGE_IO_OUTPUT
)
script.invoke_process(inputAllocation, outputAllocation)

Java

// File SingleSource.java

RenderScript RS = RenderScript.create(context);
ScriptC_singlesource script = new ScriptC_singlesource(RS);
Allocation inputAllocation = Allocation.createFromBitmapResource(
    RS, getResources(), R.drawable.image);
Allocation outputAllocation = Allocation.createTyped(
    RS, inputAllocation.getType(),
    Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT);
script.invoke_process(inputAllocation, outputAllocation);

Ví dụ này cho thấy cách thuật toán liên quan đến 2 lần khởi chạy hạt nhân có thể được triển khai hoàn toàn bằng chính ngôn ngữ RenderScript. Nếu không có RenderScript đơn nguồn, bạn sẽ phải khởi chạy cả hai nhân qua mã Java, tách khởi chạy hạt nhân khỏi định nghĩa hạt nhân và khiến hệ thống khó hiểu được toàn bộ thuật toán. Mã RenderScript đơn nguồn không chỉ dễ đọc hơn mà còn loại bỏ việc chuyển đổi giữa Java và tập lệnh giữa các lần khởi chạy nhân. Một số thuật toán lặp lại có thể khởi chạy hạt nhân hàng trăm lần, làm tăng đáng kể chi phí chuyển đổi.

Tập lệnh toàn cục

Tập lệnh toàn cục là một biến toàn cục thông thường không phải static trong tệp tập lệnh (.rs). Đối với tập lệnh toàn cục tên var được xác định trong tệpfilename.rs, sẽ có một phương thức get_var được phản ánh trong lớp ScriptC_filename. Trừ trường hợp tập lệnh toàn cục là const, cũng sẽ có một phương thức set_var.

Một tập lệnh cụ thể chung có hai giá trị riêng biệt: giá trị Java và giá trị tập lệnh. Các giá trị này có hành vi như sau:

  • Nếu var có một trình khởi động tĩnh trong tập lệnh, thì thuộc tính này sẽ chỉ định giá trị ban đầu của var trong cả Java và tập lệnh. Nếu không, giá trị ban đầu sẽ bằng 0.
  • Có quyền truy cập vào var trong tập lệnh đọc và ghi giá trị tập lệnh.
  • Phương thức get_var đọc giá trị Java.
  • Phương thức set_var (nếu có) ghi giá trị Java ngay lập tức và ghi giá trị tập lệnh theo cách không đồng bộ.

LƯU Ý: Điều này có nghĩa là ngoại trừ bất kỳ trình khởi động tĩnh nào trong tập lệnh, các giá trị được ghi vào tập lệnh toàn cục từ trong một tập lệnh sẽ không hiển thị trong Java.

Thông tin chuyên sâu về hạt nhân rút gọn

Rút gọn (reduction) là quá trình kết hợp một tập hợp dữ liệu vào một giá trị duy nhất. Đây là một quá trình nguyên gốc hữu ích trong chương trình song song, với các ứng dụng như sau:

  • tính toán tổng hoặc tích dữ liệu trên tất cả dữ liệu
  • phép toán logic (and, or, xor) trên tất cả dữ liệu
  • tìm giá trị nhỏ nhất hoặc lớn nhất trong dữ liệu
  • tìm kiếm một giá trị cụ thể hoặc toạ độ của một giá trị cụ thể trong dữ liệu

Trong Android 7.0 (API cấp 24) trở lên, RenderScript hỗ trợ nhân rút gọn để cho phép các thuật toán rút gọn hiệu quả do người dùng viết. Bạn có thể khởi chạy hạt nhân rút gọn trên các đầu vào có 1, 2 hoặc 3 chiều.

Ví dụ ở trên cho thấy một trình hạt nhân rút gọn addint đơn giản. Sau đây là một hạt nhân rút gọn findMinAndMax phức tạp hơn. Hạt nhân này tìm các vị trí của giá trị long tối thiểu và tối đa trong một Allocation 1 chiều:

#define LONG_MAX (long)((1UL << 63) - 1)
#define LONG_MIN (long)(1UL << 63)

#pragma rs reduce(findMinAndMax) \
  initializer(fMMInit) accumulator(fMMAccumulator) \
  combiner(fMMCombiner) outconverter(fMMOutConverter)

// Either a value and the location where it was found, or INITVAL.
typedef struct {
  long val;
  int idx;     // -1 indicates INITVAL
} IndexedVal;

typedef struct {
  IndexedVal min, max;
} MinAndMax;

// In discussion below, this initial value { { LONG_MAX, -1 }, { LONG_MIN, -1 } }
// is called INITVAL.
static void fMMInit(MinAndMax *accum) {
  accum->min.val = LONG_MAX;
  accum->min.idx = -1;
  accum->max.val = LONG_MIN;
  accum->max.idx = -1;
}

//----------------------------------------------------------------------
// In describing the behavior of the accumulator and combiner functions,
// it is helpful to describe hypothetical functions
//   IndexedVal min(IndexedVal a, IndexedVal b)
//   IndexedVal max(IndexedVal a, IndexedVal b)
//   MinAndMax  minmax(MinAndMax a, MinAndMax b)
//   MinAndMax  minmax(MinAndMax accum, IndexedVal val)
//
// The effect of
//   IndexedVal min(IndexedVal a, IndexedVal b)
// is to return the IndexedVal from among the two arguments
// whose val is lesser, except that when an IndexedVal
// has a negative index, that IndexedVal is never less than
// any other IndexedVal; therefore, if exactly one of the
// two arguments has a negative index, the min is the other
// argument. Like ordinary arithmetic min and max, this function
// is commutative and associative; that is,
//
//   min(A, B) == min(B, A)               // commutative
//   min(A, min(B, C)) == min((A, B), C)  // associative
//
// The effect of
//   IndexedVal max(IndexedVal a, IndexedVal b)
// is analogous (greater . . . never greater than).
//
// Then there is
//
//   MinAndMax minmax(MinAndMax a, MinAndMax b) {
//     return MinAndMax(min(a.min, b.min), max(a.max, b.max));
//   }
//
// Like ordinary arithmetic min and max, the above function
// is commutative and associative; that is:
//
//   minmax(A, B) == minmax(B, A)                  // commutative
//   minmax(A, minmax(B, C)) == minmax((A, B), C)  // associative
//
// Finally define
//
//   MinAndMax minmax(MinAndMax accum, IndexedVal val) {
//     return minmax(accum, MinAndMax(val, val));
//   }
//----------------------------------------------------------------------

// This function can be explained as doing:
//   *accum = minmax(*accum, IndexedVal(in, x))
//
// This function simply computes minimum and maximum values as if
// INITVAL.min were greater than any other minimum value and
// INITVAL.max were less than any other maximum value.  Note that if
// *accum is INITVAL, then this function sets
//   *accum = IndexedVal(in, x)
//
// After this function is called, both accum->min.idx and accum->max.idx
// will have nonnegative values:
// - x is always nonnegative, so if this function ever sets one of the
//   idx fields, it will set it to a nonnegative value
// - if one of the idx fields is negative, then the corresponding
//   val field must be LONG_MAX or LONG_MIN, so the function will always
//   set both the val and idx fields
static void fMMAccumulator(MinAndMax *accum, long in, int x) {
  IndexedVal me;
  me.val = in;
  me.idx = x;

  if (me.val <= accum->min.val)
    accum->min = me;
  if (me.val >= accum->max.val)
    accum->max = me;
}

// This function can be explained as doing:
//   *accum = minmax(*accum, *val)
//
// This function simply computes minimum and maximum values as if
// INITVAL.min were greater than any other minimum value and
// INITVAL.max were less than any other maximum value.  Note that if
// one of the two accumulator data items is INITVAL, then this
// function sets *accum to the other one.
static void fMMCombiner(MinAndMax *accum,
                        const MinAndMax *val) {
  if ((accum->min.idx < 0) || (val->min.val < accum->min.val))
    accum->min = val->min;
  if ((accum->max.idx < 0) || (val->max.val > accum->max.val))
    accum->max = val->max;
}

static void fMMOutConverter(int2 *result,
                            const MinAndMax *val) {
  result->x = val->min.idx;
  result->y = val->max.idx;
}

LƯU Ý: Bạn có thể xem thêm ví dụ hạt nhân rút gọn tại đây.

Để chạy hạt nhân rút gọn, thời gian chạy RenderScript sẽ tạo một hoặc nhiều biến gọi là mục dữ liệu tích luỹ để chứa trạng thái của quá trình rút gọn. Thời gian chạy RenderScript chọn số lượng mục dữ liệu tích luỹ theo cách tối đa hoá hiệu suất. Loại mục dữ liệu tích luỹ (accumType) được xác định bởi hàm tích luỹ của hạt nhân – đối số đầu tiên cho hàm đó là một con trỏ tới mục dữ liệu tích luỹ. Theo mặc định, mọi mục dữ liệu tích luỹ đều được khởi tạo bằng 0 (như là memset); tuy nhiên, bạn có thể viết hàm khởi động (initializer function) để thực hiện việc khác.

Ví dụ: Trong khung addint, các mục dữ liệu tích luỹ (loại int) sẽ được dùng để thêm tổng giá trị đầu vào. Không có hàm khởi động, do đó, mỗi mục dữ liệu tích luỹ được khởi động về 0.

Ví dụ: Trong hạt nhân findMinAndMax, các mục dữ liệu tích luỹ (kiểu MinAndMax) được dùng để theo dõi các giá trị tối thiểu và tối đa đã tìm thấy cho đến thời điểm này. Có một hàm khởi động để đặt các giá trị này thành LONG_MAXLONG_MIN tương ứng; và để đặt vị trí của các giá trị này là -1, cho biết rằng các giá trị không thực sự hiện diện trong phần (trống) của đầu vào đã được xử lý.

RenderScript gọi hàm tích luỹ của bạn một lần cho mỗi toạ độ trong (các) đầu vào. Thông thường, hàm của bạn phải cập nhật mục dữ liệu tích luỹ theo một cách nào đó tuỳ theo đầu vào.

Ví dụ: Trong hạt nhân addint, hàm tích luỹ sẽ thêm giá trị của một Phần tử đầu vào vào mục dữ liệu tích luỹ.

Ví dụ: Trong hạt nhân findMinAndMax, hàm tích luỹ kiểm tra xem liệu giá trị của một Phần tử đầu vào có nhỏ hơn hoặc bằng giá trị tối thiểu được ghi lại trong mục dữ liệu tích luỹ và/hoặc lớn hơn hoặc bằng giá trị tối đa được ghi lại trong mục dữ liệu tích luỹ, rồi cập nhật mục dữ liệu tích luỹ cho phù hợp.

Sau khi hàm tích luỹ được gọi một lần cho mỗi toạ độ trong (các) dữ liệu đầu vào, RenderScript phải kết hợp các mục dữ liệu tích luỹ với nhau thành một mục dữ liệu tích luỹ duy nhất. Bạn có thể viết hàm kết hợp (combiner function) để thực hiện việc này. Nếu hàm tích luỹ có một đầu vào duy nhất và không có đối số đặc biệt nào, thì bạn không cần phải viết hàm kết hợp; RenderScript sẽ sử dụng hàm tích luỹ để kết hợp các mục dữ liệu tích luỹ. (Bạn vẫn có thể viết hàm kết hợp nếu hành vi mặc định này không phải là hành vi bạn muốn.)

Ví dụ: Trong hạn nhân addint không có hàm kết hợp nào, hàm tích luỹ sẽ được sử dụng. Đây là hành vi chính xác, vì nếu chúng ta chia một tập hợp giá trị thành hai phần rồi cộng các giá trị trong hai phần đó một cách riêng biệt, thì cộng hai tổng đó cũng giống như cộng toàn bộ bộ sưu tập.

Ví dụ: Trong hạt nhân findMinAndMax, hàm kết hợp kiểm tra để xem liệu giá trị tối thiểu ghi lại trong mục dữ liệu tích luỹ "nguồn" *val có nhỏ hơn giá trị tối thiểu được ghi lại trong mục dữ liệu tích luỹ "đích" *accum hay không rồi cập nhật *accum theo đó. Giá trị tối đa cũng tương tự. Việc này cập nhật *accum thành trạng thái sẽ xảy ra nếu tất cả đầu vào đã được tích luỹ vào *accum thay vì một số vào *accum và một số vào *val.

Sau khi tất cả mục dữ liệu tích luỹ được kết hợp, RenderScript xác định kết quả của việc rút gọn để quay lại Java. Bạn có thể viết hàm chuyển đổi (outconverter function) để thực hiện việc này. Bạn không cần phải viết hàm chuyển đổi nếu muốn giá trị cuối cùng của các mục dữ liệu tích luỹ là kết quả của việc rút gọn.

Ví dụ: Trong nhân addint, không có hàm chuyển đổi. Giá trị cuối cùng của các mục dữ liệu tích luỹ là tổng của tất cả phần tử của đầu vào, chính là giá trị mà chúng ta muốn trả về.

Ví dụ: Trong hạt nhân findMinAndMax, hàm chuyển đổi sẽ khởi chạy một giá trị kết quả int2 để giữ các vị trí của giá trị tối thiểu và tối đa đến từ việc kết hợp tất cả mục dữ liệu tích luỹ.

Viết hạt nhân rút gọn

#pragma rs reduce xác định hạt nhân rút gọn bằng cách chỉ định tên hạt nhân đó cũng như tên và vai trò của các hàm tạo nên nhân đó. Tất cả hàm như vậy đêu phải là static. Nhân rút gọn luôn đòi hỏi hàm accumulator; bạn có thể bỏ qua một số hoặc tất cả hàm khác, tuỳ thuộc vào hàm mà bạn muốn hạt nhân thực hiện.

#pragma rs reduce(kernelName) \
  initializer(initializerName) \
  accumulator(accumulatorName) \
  combiner(combinerName) \
  outconverter(outconverterName)

Các mục trong #pragma có ý nghĩa như sau:

  • reduce(kernelName) (bắt buộc): Xác định rằng một hạt nhân rút gọn đang được xác định. Phương thức Java dùng để phản ánh reduce_kernelName sẽ khởi chạy hạt nhân.
  • initializer(initializerName) (không bắt buộc): Chỉ định tên của hàm khởi tạo cho hạt nhân rút gọn này. Khi bạn khởi chạy hạt nhân, RenderScript sẽ gọi hàm này một lần cho mỗi mục dữ liệu tích luỹ. Hàm phải được xác định như sau:

    static void initializerName(accumType *accum) { … }

    accum là một con trỏ tới mục dữ liệu tích luỹ để hàm này khởi chạy.

    Nếu bạn không cung cấp hàm khởi tạo, RenderScript sẽ khởi tạo mọi mục dữ liệu tích luỹ về 0 (như thể là do memset), xử lý như thể có một hàm khởi động giống như sau:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName) (bắt buộc): Chỉ định tên của hàm tích luỹ cho nhân rút gọn này. Khi bạn khởi chạy nhân, RenderScript gọi hàm này một lần cho mọi toạ độ trong (các) đầu vào, để cập nhật một mục dữ liệu tích luỹ theo một cách nào đó cho phù hợp với (các) đầu vào. Hàm phải được xác định như sau:

    static void accumulatorName(accumType *accum,
                                in1Type in1, …, inNType inN
                                [, specialArguments]) { … }
    

    accum là một con trỏ tới mục dữ liệu tích luỹ cho phép hàm này sửa đổi. in1 đến inN là một hoặc nhiều đối số được điền tự động dựa trên các đầu vào được truyền vào lần khởi chạy hạt nhân, một đối số cho mỗi đầu vào. Hàm tích luỹ có thể tuỳ ý nhận bất cứ đối số đặc biệt nào.

    Một hạt nhân ví vụ có nhiều đầu vào dotProduct.

  • combiner(combinerName)

    (không bắt buộc): Chỉ định tên của hàm kết hợp cho hạt nhân rút gọn này. Sau khi RenderScript gọi hàm tích luỹ một lần cho mọi toạ độ trong (các) đầu vào, hạt nhân này sẽ gọi hàm này nhiều lần cho đến mức cần thiết để kết hợp tất cả mục dữ liệu tích luỹ vào một mục dữ liệu tích luỹ duy nhất. Hàm phải được định nghĩa như sau:

    static void combinerName(accumType *accum, const accumType *other) { … }

    accum là một con trỏ tới mục dữ liệu tích luỹ "đích" ("destination") để hàm này sửa đổi. other là một con trỏ tới mục dữ liệu tích luỹ "nguồn" ("source") của hàm này để "kết hợp" ("combine") thành *accum.

    LƯU Ý: Có thể *accum, *other hoặc cả hai đều đã được khởi tạo nhưng chưa được truyền đến hàm tích luỹ; tức là một trong hai hoặc cả hai chưa từng được cập nhật theo bất cứ dữ liệu đầu vào nào. Ví dụ: trong hạt nhân findMinAndMax, hàm kết hợp fMMCombiner kiểm tra rõ ràng cho idx < 0 vì điều đó cho biết một mục dữ liệu tích luỹ có giá trị là INITVAL.

    Nếu bạn không cung cấp hàm kết hợp, RenderScript sẽ sử dụng hàm tích hợp trong vị trí đó, hoạt động như thể có một hàm kết hợp như sau:

    static void combinerName(accumType *accum, const accumType *other) {
      accumulatorName(accum, *other);
    }

    Hàm kết hợp là bắt buộc nếu hạt nhân có nhiều hơn một dữ liệu đầu vào, nếu kiểu dữ liệu đầu vào không giống với kiểu dữ liệu tích luỹ, hoặc nếu hàm tích hợp lấy một hoặc nhiều đối số đặc biệt.

  • outconverter(outconverterName) (không bắt buộc): Chỉ định tên của hàm chuyển đổi cho hạt nhân rút gọn này. Sau khi RenderScript kết hợp tất cả mục dữ liệu tích luỹ, lệnh này sẽ gọi hàm này để xác định kết quả của việc rút gọn để quay lại Java. Hàm phải được định nghĩa như sau:

    static void outconverterName(resultType *result, const accumType *accum) { … }

    result là con trỏ đến một mục dữ liệu kết quả (được phân bổ nhưng không được khởi chạy trong thời gian chạy RenderScript) để hàm này khởi chạy bằng kết quả rút gọn. resultType là kiểu của mục dữ liệu đó, không cần giống như accumType. accum là một con trỏ tới mục dữ liệu tích luỹ cuối cùng được tính bằng hàm kết hợp.

    Nếu bạn không cung cấp hàm chuyển đổi, RenderScript sẽ sao chép mục dữ liệu tích luỹ cuối cùng vào mục dữ liệu kết quả, hoạt động như thể có một hàm chuyển đổi giống như sau:

    static void outconverterName(accumType *result, const accumType *accum) {
      *result = *accum;
    }

    Nếu muốn có kiểu kết quả khác với kiểu dữ liệu tích luỹ, thì bạn bắt buộc phải sử dụng hàm chuyển đổi.

Xin lưu ý rằng một hạt nhân có các loại đầu vào, loại mục dữ liệu tích luỹ và loại kết quả, không có cái nào giống nhau. Ví dụ: trong hạt nhân findMinAndMax, loại đầu vào long, loại mục dữ liệu tích luỹ MinAndMax và loại kết quả int2 đều khác nhau.

Bạn không thể giả định điều gì?

Bạn không được dựa vào số mục dữ liệu tích luỹ do RenderScript tạo ra để thực hiện một lượt khởi chạy hạt nhân. Không có gì đảm bảo rằng hai lần chạy cùng một hạt nhân với cùng một đầu vào sẽ tạo ra cùng số mục dữ liệu tích luỹ.

Bạn không được dựa vào thứ tự RenderScript gọi các hàm khởi tạo, tích luỹ và kết hợp; thậm chí các hàm này có thể được gọi song song. Không có gì đảm bảo rằng hai lượt khởi chạy cùng một hạt nhân với cùng một đầu vào sẽ tuân theo cùng một thứ tự. Điều đảm bảo duy nhất là chỉ hàm khởi tạo mới có thể thấy một mục dữ liệu tích luỹ chưa khởi tạo. Ví dụ:

  • Không có gì đảm bảo rằng mục dữ liệu tích luỹ sẽ được khởi tạo trước khi gọi hàm tích hợp, mặc dù dữ liệu này chỉ được gọi trên mục dữ liệu tích luỹ.
  • Chúng tôi không đảm bảo về thứ tự truyền phần tử đầu vào đến hàm tích luỹ.
  • Không có gì đảm bảo rằng hàm tích luỹ đã được gọi cho tất cả Phần tử đầu vào trước khi gọi hàm kết hợp.

Một hệ quả của việc này là hạt nhân findMinAndMax không mang tính quyết định: Nếu dữ liệu đầu vào chứa nhiều lần xuất hiện với cùng một giá trị tối thiểu hoặc tối đa, bạn không có cách nào để biết hạt nhân sẽ tìm thấy lần xuất hiện nào.

Bạn phải đảm bảo những gì?

Vì hệ thống RenderScript có thể chọn thực thi một hạt nhân theo nhiều cách, nên bạn phải tuân theo một số quy tắc nhất định để đảm bảo hạt nhân hoạt động theo cách bạn muốn. Nếu không tuân thủ những quy tắc này, bạn có thể nhận được kết quả không chính xác, hành vi không xác định hoặc lỗi thời gian chạy.

Các quy tắc bên dưới thường cho biết rằng hai mục dữ liệu tích luỹ phải có "cùng một giá trị". Như thế nghĩa là sao? Điều đó phụ thuộc vào những gì bạn muốn hạt nhân này thực hiện. Đối với rút gọn về toán học, chẳng hạn như addint, "giống nhau" ("the same") có thường có nghĩa là bằng nhau trong toán họ. Đối với lượt tìm kiếm "chọn bất kỳ" ("pick any") chẳng hạn như findMinAndMax ("tìm vị trí của các đầu vào tối thiểu và tối đa") khi có thể xuất hiện giá trị đầu vào giống nhau, tất cả vị trí của một đầu vào đã cho phải được coi là "giống nhau". Bạn có thể viết một hạt nhân tương tự để "tìm vị trí của giá trị đầu vào tối thiểu và tối đa cuối cùng bên trái (leftmost)", trong đó giá trị tối thiểu tại vị trí 100 được ưu tiên hơn giá trị tối thiểu giống nhau tại vị trí 200; đối với hạt nhân này, "giống nhau" có nghĩa là vị trí giống hệt nhau chứ không chỉ là giá trị giống hệt nhau và hàm tích luỹ cũng như hàm kết hợp phải khác nhau cho findMinAndMax.

Hàm khởi tạo phải tạo một giá trị nhận dạng. Nghĩa là, nếu IA là các mục dữ liệu tích luỹ do hàm khởi động tạo ra, và I chưa từng được truyền vào hàm tích luỹ (nhưng A thì có thể), thì
  • combinerName(&A, &I) phải để A giống nhau
  • combinerName(&I, &A) phải để I giống với A

Ví dụ: Trong hạt nhân addint, một mục dữ liệu tích luỹ sẽ được khởi đồng về 0. Hàm kết hợp cho hạt nhân này thực hiện thao tác bổ sung; 0 là giá trị nhận dạng để thêm.

Ví dụ: Trong hạt nhân findMinAndMax, một mục dữ liệu tích luỹ được khởi động lên INITVAL.

  • fMMCombiner(&A, &I) giữ nguyên A, vì IINITVAL.
  • fMMCombiner(&I, &A) đặt I thành A, vì IINITVAL.

Do đó, INITVAL thực sự là một giá trị nhận dạng.

Hàm kết hợp phải có tính giao hoán. Tức là nếu AB là các mục dữ liệu tích luỹ do hàm khởi tạo khởi chạy và có thể đã được truyền cho hàm tích luỹ 0 hoặc nhiều lần thì combinerName(&A, &B) phải đặt A thành cùng giá trịcombinerName(&B, &A) đặt B.

Ví dụ: Trong hạt nhân addint, hàm kết hợp sẽ thêm hai giá trị mục dữ liệu tích luỹ; việc cộng thêm có tính giao hoán.

Ví dụ: Trong hạt nhân findMindAndMax, fMMCombiner(&A, &B) giống như A = minmax(A, B)minmax mang tính giao hoán, vì vậy fMMCombiner cũng tương tự.

Hàm kết hợp phải có tính kết hợp. Nghĩa là, nếu A, BC là các mục dữ liệu tích luỹ do hàm khởi động khởi động và có thể đã được truyền vào hàm tích luỹ bằng 0 hoặc nhiều lần hơn thì hai chuỗi mã sau phải đặt A thành cùng một giá trị:

  • combinerName(&A, &B);
    combinerName(&A, &C);
    
  • combinerName(&B, &C);
    combinerName(&A, &B);
    

Ví dụ: Trong hạt nhân addint, hàm kết hợp sẽ thêm hai giá trị mục dữ liệu tích luỹ:

  • A = A + B
    A = A + C
    // Same as
    //   A = (A + B) + C
    
  • B = B + C
    A = A + B
    // Same as
    //   A = A + (B + C)
    //   B = B + C
    

Phép cộng có tính liên kết nên hàm kết hợp cũng thế.

Ví dụ: Trong hạt nhân findMinAndMax,

fMMCombiner(&A, &B)
giống như
A = minmax(A, B)
Cho nên, 2 trình tự sẽ là

  • A = minmax(A, B)
    A = minmax(A, C)
    // Same as
    //   A = minmax(minmax(A, B), C)
    
  • B = minmax(B, C)
    A = minmax(A, B)
    // Same as
    //   A = minmax(A, minmax(B, C))
    //   B = minmax(B, C)
    

minmax là có tính kết hợp và fMMCombiner cũng vậy.

Hàm tích luỹ và hàm kết hợp phải tuân theo quy tắc gấp uốn cơ bản. Nghĩa là, nếu AB là các mục dữ liệu tích luỹ, A đã được tạo bởi hàm khởi tạo và có thể đã được truyền đến hàm tích luỹ bằng 0 hoặc nhiều lần hơn, B chưa được khởi tạo và args là danh sách các đối số đầu vào và đối số đặc biệt cho một lệnh gọi cụ thể đến hàm tích luỹ, thì hai trình tự mã sau đây phải thiết lập A đến cùng một giá trị:

  • accumulatorName(&A, args);  // statement 1
    
  • initializerName(&B);        // statement 2
    accumulatorName(&B, args);  // statement 3
    combinerName(&A, &B);       // statement 4
    

Ví dụ: Trong hạt nhân addint, cho một giá trị đầu vào V:

  • Câu lệnh 1 giống với A += V
  • Câu lệnh 2 giống với B = 0
  • Câu lệnh 3 giống với B += V, giống với B = V
  • Câu lệnh 4 giống với A += B, giống với A += V

Câu lệnh 1 và 4 thiết lập A thành cùng một giá trị, do đó nhân này tuân theo quy tắc gấp uốn cơ bản.

Ví dụ: Trong hạt nhân findMinAndMax, cho một đầu vào V tại toạ độ X:

  • Câu lệnh 1 giống với A = minmax(A, IndexedVal(V, X))
  • Câu lệnh 2 giống với B = INITVAL
  • Câu lệnh 2 giống với
    B = minmax(B, IndexedVal(V, X))
    
    , bởi vì B là giá trị ban đầu, giống với
    B = IndexedVal(V, X)
    
  • Câu lệnh 4 giống với
    A = minmax(A, B)
    
    , giống với
    A = minmax(A, IndexedVal(V, X))
    

Câu lệnh 1 và 4 thiết lập A thành cùng một giá trị, do đó nhân này tuân theo quy tắc gấp uốn cơ bản.

Gọi một hạt nhân rút gọn qua mã Java

Đối với hạt nhân rút gọn có tên kernelName được xác định trong tệp filename.rs, có 3 phương thức được phản ánh trong lớp ScriptC_filename:

Kotlin

// Function 1
fun reduce_kernelName(ain1: Allocation, …,
                               ainN: Allocation): javaFutureType

// Function 2
fun reduce_kernelName(ain1: Allocation, …,
                               ainN: Allocation,
                               sc: Script.LaunchOptions): javaFutureType

// Function 3
fun reduce_kernelName(in1: Array<devecSiIn1Type>, …,
                               inN: Array<devecSiInNType>): javaFutureType

Java

// Method 1
public javaFutureType reduce_kernelName(Allocation ain1, …,
                                        Allocation ainN);

// Method 2
public javaFutureType reduce_kernelName(Allocation ain1, …,
                                        Allocation ainN,
                                        Script.LaunchOptions sc);

// Method 3
public javaFutureType reduce_kernelName(devecSiIn1Type[] in1, …,
                                        devecSiInNType[] inN);

Sau đây là một số ví dụ về cách gọi nhân addint:

Kotlin

val script = ScriptC_example(renderScript)

// 1D array
//   and obtain answer immediately
val input1 = intArrayOf()
val sum1: Int = script.reduce_addint(input1).get()  // Method 3

// 2D allocation
//   and do some additional work before obtaining answer
val typeBuilder = Type.Builder(RS, Element.I32(RS)).apply {
    setX()
    setY()
}
val input2: Allocation = Allocation.createTyped(RS, typeBuilder.create()).also {
    populateSomehow(it) // fill in input Allocation with data
}
val result2: ScriptC_example.result_int = script.reduce_addint(input2)  // Method 1
doSomeAdditionalWork() // might run at same time as reduction
val sum2: Int = result2.get()

Java

ScriptC_example script = new ScriptC_example(renderScript);

// 1D array
//   and obtain answer immediately
int input1[] = ;
int sum1 = script.reduce_addint(input1).get();  // Method 3

// 2D allocation
//   and do some additional work before obtaining answer
Type.Builder typeBuilder =
  new Type.Builder(RS, Element.I32(RS));
typeBuilder.setX();
typeBuilder.setY();
Allocation input2 = createTyped(RS, typeBuilder.create());
populateSomehow(input2);  // fill in input Allocation with data
ScriptC_example.result_int result2 = script.reduce_addint(input2);  // Method 1
doSomeAdditionalWork(); // might run at same time as reduction
int sum2 = result2.get();

Phương thức 1 có một đối số Allocation đầu vào cho mọi đối số đầu vào trong hàm tổng hợp của hạt nhân. Môi trường thời gian chạy RenderScript kiểm tra để đảm bảo rằng tất cả Allocations đầu vào có cùng chiều và kiểu Element của từng Allocations đầu vào phù hợp với kiểu của đối số đầu vào tương ứng của hàm tích luỹ nguyên mẫu. Nếu bất kỳ bước kiểm tra nào không thực hiện được, RenderScript sẽ gửi ra một ngoại lệ. Hạt nhân thực thi trên mọi toạ độ trong những chiều đó.

Phương pháp 2 giống như Phương pháp 1, ngoại trừ việc Phương pháp 2 lấy thêm một đối số sc có thể dùng để giới hạn lệnh thực thi hạt nhân với một nhóm toạ độ.

Phương thức 3 cũng giống như Phương thức 1, ngoại trừ việc thay vì nhận đầu vào Attribution thì nhập đầu vào mảng Java. Điều này rất tiện lợi vì giúp bạn tiết kiệm được việc phải viết mã để tạo một Allocations rồi sao chép dữ liệu vào đó một cách rõ ràng từ một mảng Java. Tuy nhiên, việc sử dụng Phương pháp 3 thay vì Phương pháp 1 sẽ không làm tăng hiệu suất của mã. Đối với mỗi mảng đầu vào, Phương thức 3 sẽ tạo một Allocations 1 chiều tạm thời và áp dụng loại Element thích hợp, đồng thời setAutoPadding(boolean) sẽ sao chép mảng đó vào Allocations như khi triển khai Phương thức copyFrom() của Allocation. Sau đó, Phương thức 1 được gọi, truyền những Allocations tạm thời đó.

LƯU Ý: Nếu ứng dụng của bạn thực hiện nhiều lệnh gọi hạt nhân có cùng một mảng hoặc có nhiều mảng cùng chiều và kiểu Phần tử, thì bạn có thể cải thiện hiệu suất bằng cách tạo, điền rõ ràng và tái sử dụng Allocations, thay vì sử dụng Phương pháp 3.

javaFutureType, loại dữ liệu trả về của các phương thức rút gọn được phản ánh, là một lớp lồng tĩnh được phản ánh trong ScriptC_filename. Thuộc tính này cho biết kết quả trong tương lai của một lần chạy hạt nhân rút gọn. Để lấy kết quả thực tế của lần chạy, hãy gọi phương thức get() của lớp đó. Phương thức này sẽ trả về một giá trị thuộc loại javaResultType. get() có tính đồng bộ.

Kotlin

class ScriptC_filename(rs: RenderScript) : ScriptC(…) {
    object javaFutureType {
        fun get(): javaResultType { … }
    }
}

Java

public class ScriptC_filename extends ScriptC {
  public static class javaFutureType {
    public javaResultType get() { … }
  }
}

javaResultType được xác định từ resultType của hàm chuyển đổi. Trừ phi resultType là một kiểu chưa có chữ ký (vô hướng, vectơ, hoặc mảng), javaResultType là kiểu Java tương ứng trực tiếp. Nếu resultType là kiểu chưa có chữ ký và có kiểu Java đã ký lớn hơn, thì javaResultType là kiểu đã ký lớn hơn trong Java; nếu không, đó là kiểu Java tương ứng trực tiếp. Ví dụ:

  • Nếu resultTypeint, int2 hoặc int[15], thì javaResultTypeint, Int2 hoặc int[]. Tất cả giá trị của resultType có thể được biểu diễn bằng javaResultType.
  • Nếu resultTypeuint, uint2 hoặc uint[15], thì javaResultTypelong, Long2 hoặc long[]. Tất cả giá trị của resultType có thể được biểu diễn bằng javaResultType.
  • Nếu resultTypeulong, ulong2 hoặc ulong[15], thì javaResultTypelong, Long2 hoặc long[]. Có một số giá trị resultType nhất định không thể biểu diễn bằng javaResultType.

javaFutureType là loại kết quả trong tương lai tương ứng với resultType của hàm chuyển đổi.

  • Nếu resultType không phải là một mảng, thì javaFutureType sẽ là result_resultType.
  • Nếu resultType là một mảng gồm độ dài Count với các thành viên thuộc loại memberType, thì javaFutureTyperesultArrayCount_memberType.

Ví dụ:

Kotlin

class ScriptC_filename(rs: RenderScript) : ScriptC(…) {

    // for kernels with int result
    object result_int {
        fun get(): Int = …
    }

    // for kernels with int[10] result
    object resultArray10_int {
        fun get(): IntArray = …
    }

    // for kernels with int2 result
    //   note that the Kotlin type name "Int2" is not the same as the script type name "int2"
    object result_int2 {
        fun get(): Int2 = …
    }

    // for kernels with int2[10] result
    //   note that the Kotlin type name "Int2" is not the same as the script type name "int2"
    object resultArray10_int2 {
        fun get(): Array<Int2> = …
    }

    // for kernels with uint result
    //   note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint"
    object result_uint {
        fun get(): Long = …
    }

    // for kernels with uint[10] result
    //   note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint"
    object resultArray10_uint {
        fun get(): LongArray = …
    }

    // for kernels with uint2 result
    //   note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2"
    object result_uint2 {
        fun get(): Long2 = …
    }

    // for kernels with uint2[10] result
    //   note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2"
    object resultArray10_uint2 {
        fun get(): Array<Long2> = …
    }
}

Java

public class ScriptC_filename extends ScriptC {
  // for kernels with int result
  public static class result_int {
    public int get() { … }
  }

  // for kernels with int[10] result
  public static class resultArray10_int {
    public int[] get() { … }
  }

  // for kernels with int2 result
  //   note that the Java type name "Int2" is not the same as the script type name "int2"
  public static class result_int2 {
    public Int2 get() { … }
  }

  // for kernels with int2[10] result
  //   note that the Java type name "Int2" is not the same as the script type name "int2"
  public static class resultArray10_int2 {
    public Int2[] get() { … }
  }

  // for kernels with uint result
  //   note that the Java type "long" is a wider signed type than the unsigned script type "uint"
  public static class result_uint {
    public long get() { … }
  }

  // for kernels with uint[10] result
  //   note that the Java type "long" is a wider signed type than the unsigned script type "uint"
  public static class resultArray10_uint {
    public long[] get() { … }
  }

  // for kernels with uint2 result
  //   note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2"
  public static class result_uint2 {
    public Long2 get() { … }
  }

  // for kernels with uint2[10] result
  //   note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2"
  public static class resultArray10_uint2 {
    public Long2[] get() { … }
  }
}

Nếu javaResultType là một loại đối tượng (bao gồm cả loại mảng), thì mỗi lệnh gọi đến javaFutureType.get() trên cùng một phiên bản sẽ trả về cùng một đối tượng.

Nếu javaResultType không thể biểu diễn tất cả giá trị của kiểu resultType, và một hạt nhân rút gọn sẽ tạo ra giá trị không thể biểu thị, thì javaFutureType.get() sẽ gửi ra một ngoại lệ.

Phương pháp 3 và devecSiInXType

devecSiInXType là loại Java tương ứng với inXType của đối số tương ứng của hàm tích luỹ. Trừ phi inXType là kiểu không có chữ ký hoặc kiểu vectơ, thì devecSiInXType là kiểu Java tương ứng trực tiếp. Nếu inXType là kiểu vô hướng không dấu, thì devecSiInXType là kiểu Java trực tiếp tương ứng với kiểu vô hướng có dấu có cùng kích thước. Nếu inXType là kiểu vectơ đã ký, thì devecSiInXType là kiểu Java trực tiếp tương ứng với kiểu thành phần vectơ. Nếu inXType là kiểu vectơ không có chữ ký, thì devecSiInXType là kiểu Java trực tiếp tương ứng với kiểu vô hướng có dấu có cùng kích thước với kiểu thành phần vectơ. Ví dụ:

  • Nếu inXTypeint, thì devecSiInXTypeint.
  • Nếu inXTypeint2, thì devecSiInXTypeint. Mảng đó là giá trị biểu diễn đã làm phẳng: Thuộc tính này có số lượng phần tử vô hướng nhiều gấp đôi so với Allocation có phần tử vectơ 2 thành phần. Phương thức này cũng giống như cách hoạt động của các phương thức copyFrom() của Allocation.
  • Nếu inXTypeuint, thì deviceSiInXType sẽ là int. Giá trị đã ký trong mảng Java được hiểu là giá trị chưa ký của cùng một bit trong Allocation. Phương thức này cũng giống như cách hoạt động của các phương thức copyFrom() của Allocation.
  • Nếu inXTypeuint2, thì deviceSiInXType sẽ là int. Đây là cách kết hợp giữa cách xử lý của int2uint: Mảng này là một giá trị biểu diễn đã làm phẳng và các giá trị ký hiệu trong mảng Java được diễn giải là các giá trị phần tử RenderScript không có chữ ký.

Xin lưu ý rằng đối với Phương pháp 3, các loại dữ liệu đầu vào được xử lý khác với các loại kết quả:

  • Đầu vào vectơ của một tập lệnh sẽ được làm phẳng ở phía Java, trong khi kết quả vectơ của tập lệnh sẽ không được làm phẳng.
  • Dữ liệu đầu vào chưa có chữ ký của một tập lệnh được biểu thị dưới dạng một dữ liệu đầu vào có chữ ký có cùng kích thước ở phía Java, trong khi kết quả không có chữ ký của tập lệnh được biểu thị dưới dạng một kiểu đã ký mở rộng ở phía Java (ngoại trừ trường hợp ulong).

Ví dụ khác về hạt nhân rút gọn

#pragma rs reduce(dotProduct) \
  accumulator(dotProductAccum) combiner(dotProductSum)

// Note: No initializer function -- therefore,
// each accumulator data item is implicitly initialized to 0.0f.

static void dotProductAccum(float *accum, float in1, float in2) {
  *accum += in1*in2;
}

// combiner function
static void dotProductSum(float *accum, const float *val) {
  *accum += *val;
}
// Find a zero Element in a 2D allocation; return (-1, -1) if none
#pragma rs reduce(fz2) \
  initializer(fz2Init) \
  accumulator(fz2Accum) combiner(fz2Combine)

static void fz2Init(int2 *accum) { accum->x = accum->y = -1; }

static void fz2Accum(int2 *accum,
                     int inVal,
                     int x /* special arg */,
                     int y /* special arg */) {
  if (inVal==0) {
    accum->x = x;
    accum->y = y;
  }
}

static void fz2Combine(int2 *accum, const int2 *accum2) {
  if (accum2->x >= 0) *accum = *accum2;
}
// Note that this kernel returns an array to Java
#pragma rs reduce(histogram) \
  accumulator(hsgAccum) combiner(hsgCombine)

#define BUCKETS 256
typedef uint32_t Histogram[BUCKETS];

// Note: No initializer function --
// therefore, each bucket is implicitly initialized to 0.

static void hsgAccum(Histogram *h, uchar in) { ++(*h)[in]; }

static void hsgCombine(Histogram *accum,
                       const Histogram *addend) {
  for (int i = 0; i < BUCKETS; ++i)
    (*accum)[i] += (*addend)[i];
}

// Determines the mode (most frequently occurring value), and returns
// the value and the frequency.
//
// If multiple values have the same highest frequency, returns the lowest
// of those values.
//
// Shares functions with the histogram reduction kernel.
#pragma rs reduce(mode) \
  accumulator(hsgAccum) combiner(hsgCombine) \
  outconverter(modeOutConvert)

static void modeOutConvert(int2 *result, const Histogram *h) {
  uint32_t mode = 0;
  for (int i = 1; i < BUCKETS; ++i)
    if ((*h)[i] > (*h)[mode]) mode = i;
  result->x = mode;
  result->y = (*h)[mode];
}

Mã mẫu khác

Các mẫu BasicRenderScript, RenderScriptIntrinsicHello Compute minh hoạ rõ hơn việc sử dụng các API được trình bày trên trang này.