RenderScript 개요

RenderScript는 계산이 많은 작업을 Android에서 높은 성능으로 실행하기 위한 프레임워크입니다. RenderScript는 직렬 워크로드에도 유용할 수 있지만 주로 데이터 병렬 계산에 주로 사용되도록 설계되었습니다. RenderScript 런타임은 멀티코어 CPU 및 GPU와 같은 기기에서 사용할 수 있는 여러 프로세서에 걸쳐 작업을 병렬화합니다. 따라서 작업을 예약하는 일보다 알고리즘을 표현하는 데 집중할 수 있습니다. RenderScript는 이미지 처리, 컴퓨테이셔널 포토그래피, 컴퓨터 비전을 실행하는 애플리케이션에 특히 유용합니다.

RenderScript를 시작하려면 두 가지 기본 개념을 이해해야 합니다.

  • 언어 자체는 고성능 컴퓨팅 코드를 작성하기 위한 C99 파생 언어입니다. RenderScript 커널 작성에서는 이 파생 언어를 사용하여 컴퓨팅 커널을 작성하는 방법을 설명합니다.
  • 제어 API는 RenderScript 리소스의 전체 기간을 관리하고 커널 실행을 제어하는 데 사용됩니다. 제어 API는 자바, Android NDK의 C++, C99 파생 커널 언어 자체 등 세 가지 언어로 사용 가능합니다. 자바 코드에서 RenderScript 사용Single-Source RenderScript에서 각각 첫 번째 옵션과 세 번째 옵션을 설명합니다.

RenderScript 커널 작성

일반적으로 RenderScript 커널은 <project_root>/src/rs 디렉터리의 .rs 파일에 있으며, 각 .rs 파일을 스크립트라고 합니다. 스크립트마다 자체 커널과 함수, 변수가 있습니다. 또한 다음 항목도 있을 수 있습니다.

  • pragma 선언(#pragma version(1)) - 이 스크립트에 사용되는 RenderScript 커널 언어의 버전을 선언합니다. 현재는 유일하게 1만 유효한 값입니다.
  • pragma 선언(#pragma rs java_package_name(com.example.app)) - 이 스크립트에서 반영된 자바 클래스의 패키지 이름을 선언합니다. .rs 파일이 라이브러리 프로젝트가 아닌 애플리케이션 패키지에 있어야 한다는 점을 유의하세요.
  • 0개 이상의 호출 가능 함수. 호출 가능 함수는 자바 코드에서 임의의 인수를 사용하여 호출할 수 있는 단일 스레드 RenderScript 함수입니다. 이러한 함수는 대개 대규모 처리 파이프라인 내에서 직렬 계산 또는 초기 설정을 하는 데 유용합니다.
  • 0개 이상의 스크립트 전역 변수. 스크립트 전역 변수는 C의 전역 변수와 유사합니다. 자바 코드에서 액세스할 수 있는 스크립트 전역 변수는 매개변수를 RenderScript 커널에 전달하는 데 주로 사용됩니다. 스크립트 전역 변수에 관한 자세한 내용은 여기에 나와 있습니다.

  • 0개 이상의 컴퓨팅 커널. 컴퓨팅 커널은 RenderScript 런타임이 데이터 모음에서 병렬로 실행되도록 명령할 수 있는 함수 또는 함수 모음입니다. 컴퓨팅 커널에는 매핑 커널(foreach 커널이라고도 함)과 축소 커널, 이렇게 두 가지가 있습니다.

    매핑 커널은 동일한 차원의 Allocations 모음에서 작동하는 병렬 함수입니다. 기본적으로 매핑 커널은 이러한 차원의 좌표마다 한 번씩 실행됩니다. 그리고 한 번에 한 Element씩 입력 Allocations의 모음을 출력 Allocation으로 변환하는 데 주로 사용(전적으로 이 용도로만 사용되는 것은 아님)됩니다.

    • 다음은 간단한 매핑 커널의 예입니다.

      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;
      }

      많은 측면에서 이 매핑 커널은 표준 C 함수와 동일합니다. 함수 프로토타입에 적용된 RS_KERNEL 속성은 함수가 호출 가능 함수가 아닌 RenderScript 매핑 커널임을 명시합니다. in 인수는 커널 실행에 전달된 입력 Allocation을 기반으로 자동으로 채워집니다. 인수 xy아래에서 논의합니다. 커널에서 반환된 값은 출력 Allocation의 적절한 위치에 자동으로 작성됩니다. 기본적으로 이 커널은 전체 입력 Allocation에서 실행되며 AllocationElement당 커널 함수가 한 번 실행됩니다.

      매핑 커널에는 하나 이상의 입력 Allocations, 단일 출력 Allocation 중 하나 또는 둘 다가 있을 수 있습니다. RenderScript 런타임은 입력과 출력 Allocation이 모두 동일한 차원을 갖고 입력과 출력 Allocation의 Element 유형이 커널의 프로토타입과 일치하는지 검사합니다. 이러한 검사 중 하나라도 실패하면 RenderScript에서 예외가 발생합니다.

      참고: Android 6.0(API 수준 23) 이전의 경우 매핑 커널이 입력 Allocation을 두 개 이상 가지지 못할 수 있습니다.

      커널에 있는 것보다 더 많은 입력 또는 출력 Allocations가 필요한 경우, 이러한 객체를 rs_allocation 스크립트 전역 변수에 바인딩한 다음 커널 또는 호출 가능 함수에서 rsGetElementAt_type() 또는 rsSetElementAt_type()을 통해 액세스해야 합니다.

      참고: RS_KERNEL은 편의를 위해 RenderScript에 의해 자동으로 정의된 매크로입니다.

      #define RS_KERNEL __attribute__((kernel))
      

    축소 커널은 동일한 차원의 입력 Allocations 모음에서 작동하는 함수군입니다. 기본적으로 축소 커널의 accumulator 함수는 이러한 차원의 좌표마다 한 번씩 실행됩니다. 일반적으로 입력 Allocations 모음을 단일 값으로 '축소'하는 데 사용됩니다(전적으로 이 용도로만 사용되는 것은 아님).

    • 다음은 입력의 Elements를 더하는 단순 축소 커널입니다.

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

      축소 커널은 하나 이상의 사용자 작성 함수로 구성됩니다. #pragma rs reduce는 커널 이름(이 예에서는 addint)과 커널을 구성하는 함수 이름과 역할(이 예에서는 accumulator 함수 addintAccum)을 지정하여 커널을 정의하는 데 사용됩니다. 이러한 함수는 모두 static이어야 합니다. 축소 커널은 항상 accumulator 함수가 필요합니다. 커널에 원하는 기능에 따라 다른 함수도 있을 수 있습니다.

      축소 커널의 accumulator 함수는 void를 반환해야 하고 인수를 두 개 이상 가져야 합니다. 첫 번째 인수(이 예에서 accum)는 누산기 데이터 항목에 관한 포인터이고, 두 번째 인수(이 예에서 val)는 커널 실행에 전달된 입력 Allocation에 따라 자동으로 채워집니다. 누산기 데이터 항목은 RenderScript 런타임에 의해 생성되고, 기본적으로 0으로 초기화됩니다. 기본적으로 이 커널은 AllocationElement당 accumulator 함수가 한 번씩 실행됨으로써 전체 입력 Allocation에서 실행됩니다. 기본적으로 누산기 데이터 항목의 최종 값은 축소의 결과로 처리되고 자바로 반환됩니다. RenderScript 런타임은 입력 Allocation의 Element 유형이 accumulator 함수의 프로토타입과 일치하는지 검사합니다. 일치하지 않으면 RenderScript에서 예외가 발생합니다.

      축소 커널에는 하나 이상의 입력 Allocations가 있지만 출력 Allocations는 없습니다.

      축소 커널은 여기에 자세히 설명되어 있습니다.

      축소 커널은 Android 7.0(API 수준 24) 이상에서 지원됩니다.

    매핑 커널 함수 또는 축소 커널 accumulator 함수는 특수 인수 x, y, z(이때 형식은 int 또는 uint32_t여야 함)를 사용하여 현재 실행의 좌표에 액세스할 수 있습니다. 이러한 인수는 선택사항입니다.

    매핑 커널 함수 또는 축소 커널 accumulator 함수는 rs_kernel_context 유형의 특수 인수 context도 선택적으로 취할 수 있습니다. 이 인수는 현재 실행의 특정 속성(예: rsGetDimX)을 쿼리하는 데 사용되는 런타임 API군에 필요합니다. context 인수는 Android 6.0(API 수준 23) 이상에서 사용할 수 있습니다.

  • 선택적 init() 함수. init() 함수는 스크립트가 처음 인스턴스화될 때 RenderScript가 실행하는 특수 유형의 호출 가능 함수입니다. 이 함수를 사용하면 스크립트 생성 시 자동으로 일부 계산이 실행됩니다.
  • 0개 이상의 정적 스크립트 전역 변수 및 함수. 정적 스크립트 전역 변수는 자바 코드에서 액세스할 수 없다는 점을 제외하면 스크립트 전역 변수와 같습니다. 정적 함수는 스크립트의 호출 가능 함수 또는 커널에서 호출할 수 있는 표준 C 함수이지만, 자바 API에 노출되지 않습니다. 스크립트 전역 변수 또는 함수를 자바 코드에서 액세스할 필요가 없는 경우 static으로 선언하는 것이 좋습니다.

부동 소수점 정밀도 설정

스크립트에서 부동 소수점 정밀도 수준이 어느 정도 필요한지 제어할 수 있습니다. 이 작업은 전체 IEEE 754-2008 표준(기본적으로 사용됨)이 필요하지 않은 경우에 유용합니다. 다음 pragmas는 서로 다른 수준의 부동 소수점 정밀도를 설정할 수 있습니다.

  • #pragma rs_fp_full (아무것도 지정되지 않은 경우의 기본값): IEEE 754-2008 표준에 설명된 것처럼 부동 소수점 정밀도가 필요한 앱에 사용됩니다.
  • #pragma rs_fp_relaxed: 엄격한 IEEE 754-2008 준수가 필요하지 않고 낮은 정밀도를 허용하는 앱에 적용됨. 이 모드는 FTZ(flush-to-zero)(비정상의 경우)와 RTZ(round-towards-zero)를 실행합니다.
  • #pragma rs_fp_imprecise: 엄격한 정밀도 요구 사항이 없는 앱에 적용됨. 이 모드는 다음 내용과 함께 rs_fp_relaxed의 모든 사항을 실행합니다.
    • 결과가 -0.0인 연산이 대신 +0.0을 반환할 수 있습니다.
    • INF 및 NAN의 연산은 정의되지 않습니다.

대부분의 애플리케이션은 어떤 부작용 없이 rs_fp_relaxed를 사용할 수 있습니다. 정밀도가 낮은 경우에만 추가 최적화가 제공되기 때문에, 일부 아키텍처에서 rs_fp_relaxed는 아주 유용할 수 있습니다(예: SIMD CPU 명령어).

자바에서 RenderScript API에 액세스

RenderScript를 사용하는 Android 애플리케이션을 개발할 때 다음 두 가지 방법 중 하나로 자바에서 API에 액세스할 수 있습니다.

  • android.renderscript - 이 클래스 패키지의 API는 Android 3.0 (API 수준 11) 이상을 실행하는 기기에서 사용할 수 있습니다.
  • android.support.v8.renderscript - 이 패키지의 API는 Android 2.3(API 수준 9) 이상을 실행하는 기기에서 API를 사용할 수 있게 해 주는 지원 라이브러리를 통해 사용할 수 있습니다.

다음은 상호 보완되는 장단점입니다.

  • Support Library API를 사용하는 경우, 사용하는 RenderScript 기능과 관계없이 애플리케이션의 RenderScript 부분이 Android 2.3(API 수준 9) 이상을 실행하는 기기와 호환됩니다. 그러면 네이티브 (android.renderscript) API를 사용할 때보다 더 많은 기기에서 애플리케이션이 작동될 수 있습니다.
  • 일부 RenderScript 기능은 Support Library API를 통해 사용할 수 없습니다.
  • Support Library API를 사용하는 경우 네이티브 (android.renderscript) API를 사용할 때보다 APK가 더 커집니다(상당히 커질 수 있음).

RenderScript Support Library API 사용

Support Library RenderScript API를 사용하려면 이러한 API에 액세스할 수 있도록 개발 환경을 구성해야 합니다. 이러한 API를 사용하려면 다음 Android SDK 도구가 필요합니다.

  • Android SDK 도구 버전 22.2 이상
  • Android SDK 빌드 도구 버전 18.1.0 이상

Android SDK 빌드 도구 24.0.0부터는 Android 2.2(API 수준 8)가 더 이상 지원되지 않습니다.

Android SDK Manager에서 이러한 도구의 설치된 버전을 확인하고 업데이트할 수 있습니다.

Support Library RenderScript API를 사용하려면 다음 단계를 따르세요.

  1. 필수 Android SDK 버전이 설치되어 있는지 확인합니다.
  2. RenderScript 설정을 포함하도록 Android 빌드 프로세스의 설정을 업데이트합니다.
    • 애플리케이션 모듈의 앱 폴더에서 build.gradle 파일을 엽니다.
    • 파일에 다음 RenderScript 설정을 추가합니다.

      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
                  }
              }
              

      위에 나열된 설정은 Android 빌드 프로세스의 특정 동작을 제어합니다.

      • renderscriptTargetApi - 생성할 바이트 코드 버전을 지정합니다. 이 값은 사용 중인 모든 기능을 제공할 수 있는 가장 낮은 API 수준으로 설정하고, renderscriptSupportModeEnabledtrue로 설정하는 것이 좋습니다. 이 설정에 유효한 값은 11부터 최근에 출시된 API 수준까지의 정숫값입니다. 애플리케이션 매니페스트에 지정된 최소 SDK 버전이 다른 값으로 설정된 경우 이 값은 무시되고 빌드 파일의 타겟 값이 최소 SDK 버전을 설정하는 데 사용됩니다.
      • renderscriptSupportModeEnabled - 생성된 바이트 코드가 실행되는 기기가 타겟 버전을 지원하지 않는 경우 그 바이트 코드를 호환되는 버전으로 대체하도록 지정합니다.
  3. RenderScript를 사용하는 애플리케이션 클래스에서 지원 라이브러리 클래스용 가져오기를 추가합니다.

    Kotlin

    import android.support.v8.renderscript.*
    

    Java

    import android.support.v8.renderscript.*;
    

자바 또는 Kotlin 코드에서 RenderScript 사용

자바 또는 Kotlin 코드에서 RenderScript를 사용하는 경우 android.renderscript 또는 android.support.v8.renderscript 패키지에 있는 API 클래스를 사용하게 됩니다. 대부분의 애플리케이션은 동일한 기본 사용 패턴을 따릅니다.

  1. RenderScript 컨텍스트 초기화. create(Context)로 생성된 RenderScript 컨텍스트는 RenderScript를 사용 가능한 상태로 만들고, 모든 후속 RenderScript 객체의 전체 기간을 제어하는 객체를 제공합니다. 컨텍스트 생성은 하드웨어의 여러 부분에 리소스를 만들 수 있으므로 잠재적으로 장기 실행 작업으로 여겨야 합니다. 가능하다면 컨텍스트 생성은 애플리케이션의 중요 경로에 있어서는 안 됩니다. 일반적으로 애플리케이션은 한 번에 하나의 RenderScript 컨텍스트만 갖습니다.
  2. 스크립트에 전달할 하나 이상의 Allocation 생성. Allocation은 일정량의 데이터를 저장할 공간을 제공하는 RenderScript 객체입니다. 스크립트의 커널은 Allocation 객체를 입력 및 출력으로 취하고, Allocation 객체가 스크립트 전역 변수로 바인딩된 경우 커널에서 rsGetElementAt_type()rsSetElementAt_type()을 사용하여 이러한 객체에 액세스할 수 있습니다. Allocation 객체를 사용하면 배열을 자바 코드에서 RenderScript 코드로 전달하거나 또는 그 반대로 전달할 수 있습니다. Allocation 객체는 일반적으로 createTyped() 또는 createFromBitmap()을 사용하여 생성됩니다.
  3. 종류와 관계없이 필요한 스크립트 생성. RenderScript를 사용할 경우 두 가지 스크립트를 사용할 수 있습니다.
    • ScriptC: 위의 RenderScript 커널 작성에 설명된 사용자 정의 스크립트입니다. 자바 코드에서 스크립트에 쉽게 액세스할 수 있도록 모든 스크립트에는 RenderScript 컴파일러에 의해 반영된 자바 클래스가 있습니다. 이 클래스의 이름은 ScriptC_filename입니다. 예를 들어 위의 매핑 커널이 invert.rs에 있고 RenderScript 컨텍스트가 이미 mRenderScript에 있는 경우 스크립트를 인스턴스화하는 자바 또는 Kotlin 코드는 다음과 같습니다.

      Kotlin

      val invert = ScriptC_invert(renderScript)
      

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic: 가우시안 블러, 컨볼루션, 이미지 블렌딩 같은 일반 작업을 위해 내장된 RenderScript 커널입니다. 자세한 내용은 ScriptIntrinsic의 서브클래스를 참고하세요.
  4. Allocation을 데이터로 채움. createFromBitmap()으로 생성된 Allocation을 제외하고, Allocation은 처음 생성될 때 빈 데이터로 채워집니다. Allocation을 채우려면 Allocation의 'copy' 메서드 중 하나를 사용합니다. 'copy' 메서드는 동기식입니다.
  5. 필요한 모든 스크립트 전역 변수 설정. 동일한 ScriptC_filename 클래스에 set_globalname 이름의 메서드를 사용하여 전역 변수를 설정할 수 있습니다. 예를 들어 threshold라는 이름의 int 변수를 설정하려면 자바 메서드 set_threshold(int)를 사용합니다. lookup이라는 이름의 rs_allocation 변수를 설정하려면 자바 메서드 set_lookup(Allocation)을 사용합니다. set 메서드는 비동기식입니다.
  6. 적절한 커널과 호출 가능 함수 실행

    지정된 커널을 실행하기 위한 메서드는 forEach_mappingKernelName() 또는 reduce_reductionKernelName() 이름의 메서드와 함께 동일한 ScriptC_filename 클래스에 반영됩니다. 이러한 실행은 비동기식입니다. 커널에 관한 인수에 따라 메서드는 하나 이상의 Allocation을 취합니다. 이때 Allocation은 모두 동일한 차원을 가져야 합니다. 기본적으로 커널은 이러한 차원의 모든 좌표에 관해 실행됩니다. 이러한 좌표의 하위 집합에 관해 커널을 실행하려면 적절한 Script.LaunchOptions를 마지막 인수로 forEach 또는 reduce 메서드에 전달합니다.

    동일한 ScriptC_filename 클래스에 반영된 invoke_functionName 메서드를 사용하여 호출 가능 함수를 실행합니다. 이러한 실행은 비동기식입니다.

  7. Allocation 객체 및 javaFutureType 객체에서 데이터 검색. Java 코드에서 Allocation의 데이터에 액세스하려면 Allocation의 'copy' 메서드 중 하나를 사용하여 데이터를 Java에 다시 복사해야 합니다. 축소 커널을 결과로 얻으려면 javaFutureType.get() 메서드를 사용해야 합니다. 'copy' 및 get() 메서드는 동기식입니다.
  8. RenderScript 컨텍스트 제거. RenderScript 컨텍스트를 제거하려면 destroy()를 사용하거나 RenderScript 컨텍스트 객체가 가비지 컬렉션되도록 허용하면 됩니다. 그렇게 하면 그 컨텍스트에 속하는 객체를 추가로 사용할 경우 예외가 발생합니다.

비동기 실행 모델

반영된 forEach, invoke, reduce, set 메서드는 비동기식이며, 요청된 작업을 완료하기 전에 각각 자바로 반환될 수 있습니다. 하지만 개별 작업은 실행된 순서대로 직렬화됩니다.

Allocation 클래스는 Allocation에(서) 데이터를 복사하는 'copy' 메서드를 제공합니다. 'copy' 메서드는 동기식이며 동일한 Allocation을 다루는 위의 비동기 작업과 관련하여 직렬화됩니다.

반영된 javaFutureType 클래스는 축소 결과를 가져오는 get() 메서드를 제공합니다. get()은 동기식이며, 축소(비동기식)와 관련하여 직렬화됩니다.

Single-Source RenderScript

Android 7.0(API 수준 24)에는 Single-Source RenderScript라는 새로운 프로그래밍 기능이 도입되었습니다. 이 기능에서는 커널이 자바가 아닌, 커널이 정의된 스크립트에서 시작됩니다. 현재 이 접근법은 커널 매핑에만 적용됩니다. 이 섹션에서는 커널 매핑을 편의상 '커널'이라고 부릅니다. 이 새로운 기능은 스크립트 내에서 rs_allocation 유형의 Allocation을 생성하는 것도 지원합니다. 이제 여러 개의 커널 실행이 필요한 경우에도 스크립트 내에서만 전체 알고리즘을 구현할 수 있습니다. 이로 인한 장점은 두 가지가 있습니다. 하나는 알고리즘 구현을 한 언어로 유지하기 때문에 코드가 더 읽기 쉽다는 것이고, 다른 하나는 여러 커널 실행에서 자바와 RenderScript 간의 전환이 줄어들기 때문에 코드 실행이 빠를 수 있다는 점입니다.

Single-Source RenderScript에서는 RenderScript 커널 작성에 설명된 대로 커널을 작성할 수 있습니다. 그런 다음 rsForEach()를 호출하는 호출 가능 함수를 작성하여 커널을 실행하면 됩니다. 이 API는 커널 함수를 첫 번째 매개변수로 취하고 그다음에 입력 및 출력 Allocation을 취합니다. 유사한 API rsForEachWithOptions()rs_script_call_t 유형의 추가 인수를 취합니다. 이러한 인수는 입력 및 출력 Allocation에서 커널 함수가 처리할 요소의 하위 집합을 지정합니다.

RenderScript 계산을 시작하려면 자바에서 호출 가능 함수를 호출합니다. 자바 코드에서 RenderScript 사용에 나와 있는 단계를 따릅니다. 적절한 커널 실행 단계에서 invoke_function_name()을 사용하여 호출 가능 함수를 호출합니다. 그러면 커널이 실행되는 등 전체 계산이 시작됩니다.

Allocation은 중간 결과를 저장하고 커널 실행 간에 이 결과를 전달하는 데 자주 필요합니다. Allocation은 rsCreateAllocation()을 사용하여 생성할 수 있습니다. 이 API를 쉽게 사용할 수 있는 한 가지 형태는 rsCreateAllocation_<T><W>(…)입니다. 여기서 T는 요소의 데이터 유형이고, W는 요소의 벡터 너비입니다. API는 차원 X, Y, Z의 크기를 인수로 취합니다. 1차원 또는 2차원 Allocation의 경우 차원 Y 또는 Z 크기를 생략할 수 있습니다. 예를 들어 rsCreateAllocation_uchar4(16384)는 16384개 요소로 구성된 1차원 Allocation을 만듭니다. 이때 각 요소의 유형은 uchar4입니다.

Allocation은 시스템에서 자동으로 관리됩니다. 명시적으로 해제하거나 삭제할 필요가 없습니다. 하지만 rsClearObject(rs_allocation* alloc)를 호출하여, 기본 Allocation에 핸들 alloc가 더 이상 필요하지 않음을 나타낼 수 있습니다. 그러면 시스템이 최대한 빨리 리소스를 확보할 수 있습니다.

RenderScript 커널 작성 섹션에는 이미지를 반전하는 예제 커널이 나와 있습니다. 다음은 Single-Source RenderScript를 사용하여 이미지에 두 개 이상의 효과를 적용하기 위해 커널을 확장하는 예제입니다. 이 예제에는 색상 이미지를 흑백으로 바꾸는 greyscale 커널이 하나 더 포함되어 있습니다. 그런 다음 호출 가능 함수 process()가 두 커널을 연속으로 입력 이미지에 적용한 다음, 출력 이미지를 생성합니다. 입력과 출력 Allocation이 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);
}

다음과 같이 자바 또는 Kotlin에서 process() 함수를 호출할 수 있습니다.

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);

이 예에서는 두 개의 커널 실행과 관련된 알고리즘을 RenderScript 언어 자체로 어떻게 완벽히 구현할 수 있는지 보여줍니다. Single-Source RenderScript를 사용하지 않으면, Java 코드에서 두 커널을 모두 실행하고 커널 정의에서 커널 실행을 분리해야 하기 때문에 전체 알고리즘을 이해하기가 어렵습니다. Single-Source RenderScript 코드는 읽기가 쉬울 뿐만 아니라, 커널 실행에서 자바와 스크립트 간에 전환해야 하는 동작이 필요 없습니다. 일부 반복 알고리즘은 커널을 수백 번 실행할 수 있기 때문에, 그러한 전환의 오버헤드가 상당히 클 수 있습니다.

스크립트 전역 변수

스크립트 전역 변수는 스크립트 .rs 파일에 있는 일반 비 static 전역 변수입니다. 파일 filename.rs에 정의된 var 이름의 스크립트 전역 변수의 경우 클래스 ScriptC_filename에 반영된 메서드 get_var이 있습니다. 전역 변수가 const가 아닌 경우 메서드 set_var도 있습니다.

지정된 스크립트 전역 변수는 자바 값과 스크립트 값이라는 두 개의 개별 값을 갖습니다. 이 두 값은 다음과 같이 기능합니다.

  • 스크립트 내 var에 정적 이니셜라이저가 있는 경우 정적 이니셜라이저가 자바와 스크립트의 var 초깃값을 지정합니다. 그 외의 경우 초깃값은 0입니다.
  • 스크립트 내 var 액세스를 통해 스크립트 값 읽기와 쓰기가 실행됩니다.
  • get_var 메서드가 자바 값을 읽습니다.
  • set_var 메서드(있는 경우)가 자바 값을 즉시 쓰고, 스크립트 값을 비동기식으로 씁니다.

참고: 따라서 스크립트 내 정적 이니셜라이저를 제외하고, 스크립트에서 전역 변수에 작성된 값은 자바에 표시되지 않습니다.

축소 커널의 세부정보

축소는 데이터 모음을 단일 값으로 결합하는 프로세스입니다. 이 프로세스는 다음과 같은 사례와 함께 병렬 프로그래밍에서 오랫동안 유용하게 사용되어 왔습니다.

  • 모든 데이터에 관해 합 또는 곱을 계산
  • 모든 데이터에 관해 논리 연산(and, or, xor)을 계산
  • 데이터 내에서 최솟값 또는 최댓값 찾기
  • 특정 값 검색 또는 데이터 내 특정 값의 좌표 검색

Android 7.0(API 수준 24) 이상에서는 사용자가 작성하는 효율적인 축소 알고리즘을 허용하기 위해 RenderScript가 축소 커널 을 지원합니다. 1차원, 2차원 또는 3차원 입력에 축소 커널을 실행할 수 있습니다.

위의 예는 간단한 addint 축소 커널을 보여줍니다. 다음은 1차원 Allocation에서 최소 및 최대 long 값의 위치를 구하는, 좀 더 복잡한 findMinAndMax 축소 커널입니다.

#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;
}

참고: 여기에 더 많은 예제 축소 커널이 있습니다.

축소 커널을 실행하기 위해 RenderScript 런타임은 누산기 데이터 항목이라는 변수를 하나 이상 생성하여 축소 프로세스의 상태를 저장합니다. RenderScript 런타임은 성능을 최대화할 수 있는 방식으로 누산기 데이터 항목 수를 선택합니다. 누산기 데이터 항목의 유형(accumType)은 커널의 accumulator 함수에 의해 결정되고, 이 함수의 첫 번째 인수가 누산기 데이터 항목에 관한 포인터입니다. 기본적으로 모든 누산기 데이터 항목은 0으로 초기화됩니다(memset에 의한 동작처럼 보임). 그러나 다른 동작을 실행하도록 initializer 함수를 작성할 수 있습니다.

예: addint 커널에서 누산기 데이터 항목(유형 int)은 입력 값을 더하는 데 사용됩니다. initializer 함수가 없으므로 각 누산기 데이터 항목은 0으로 초기화됩니다.

예: findMinAndMax 커널에서 누산기 데이터 항목(유형 MinAndMax)은 지금까지 찾은 최솟값과 최댓값을 추적하는 데 사용됩니다. 이러한 값을 LONG_MAXLONG_MIN으로 설정하는 initializer 함수가 있습니다. 이러한 값의 위치를 -1로 설정하면 처리된 입력의 (빈) 부분에 실제로는 값이 없음을 나타냅니다.

RenderScript는 입력의 좌표마다 accumulator 함수를 한 번씩 호출합니다. 일반적으로 함수는 누산기 데이터 항목을 어떤 방법으로든 입력에 따라 업데이트해야 합니다.

예: addint 커널에서 accumulator 함수는 입력 요소의 값을 누산기 데이터 항목에 추가합니다.

예: findMinAndMax 커널에서 accumulator 함수는 입력 요소의 값이 누산기 데이터 항목에 기록된 최솟값보다 작거나 같은지 또는 누산기 데이터 항목에 기록된 최댓값보다 크거나 같은지 확인한 다음, 그에 따라 누산기 데이터 항목을 업데이트합니다.

accumulator 함수가 입력의 좌표마다 한 번씩 호출되면 RenderScript는 누산기 데이터 항목을 하나의 누산기 데이터 항목으로 결합해야 합니다. 이 작업을 위해 combiner 함수를 작성할 수 있습니다. accumulator 함수가 단일 입력을 갖고 특수 인수를 갖지 않는 경우 combiner 함수를 작성할 필요가 없습니다. RenderScript가 accumulator 함수를 사용하여 누산기 데이터 항목을 결합합니다. 이 기본 동작을 원치 않으면 combiner 함수를 작성하면 됩니다.

예: addint 커널에는 combiner 함수가 없으므로 accumulator 함수가 사용됩니다. 이는 올바른 동작입니다. 값의 모음을 두 부분으로 나눠서 두 부분의 값을 따로 더하면 두 총합을 더한 것과 전체 모음을 더한 것이 같기 때문입니다.

예: findMinAndMax 커널에서 combiner 함수는 '소스' 누산기 데이터 항목 *val에 기록된 최솟값이 '대상' 누산기 데이터 항목 *accum에 기록된 최솟값보다 작은지 확인한 다음, 그에 따라 *accum을 업데이트합니다. combiner 함수는 최댓값의 경우에도 이와 유사하게 작업합니다. 그리고 *accum을 업데이트하는데, 이때 입력 값이 일부는 *accum에, 나머지는 *val에 누적된 것이 아니라 모두 *accum에 누적되었을 때의 상태로 업데이트합니다.

모든 누산기 데이터 항목이 결합되면 RenderScript는 자바에 반환할 축소의 결과를 확인합니다. 이 작업은 outconverter 함수를 작성하여 실행할 수 있습니다. 결합된 누산기 데이터 항목의 최종 값을 축소의 결과로 얻고자 할 때에는 outconverter 함수를 작성할 필요가 없습니다.

예: addint 커널에는 outconverter 함수가 없습니다. 결합된 데이터 항목의 최종 값은 입력의 모든 요소의 합으로, 원하는 반환 값입니다.

예: findMinAndMax 커널에서 outconverter 함수는 모든 누산기 데이터 항목의 결합으로 얻은 최솟값 및 최댓값의 위치를 보유하기 위해 int2 결과 값을 초기화합니다.

축소 커널 작성

#pragma rs reduce는 커널의 이름과 커널을 구성하는 함수의 이름과 역할을 지정하여 축소 커널을 정의합니다. 이러한 모든 함수는 static이어야 합니다. 축소 커널에는 항상 accumulator 함수가 있어야 합니다. 그러나 다른 함수의 경우에는 커널에 원하는 기능에 따라 일부 또는 전체를 생략할 수 있습니다.

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

#pragma의 항목은 다음 의미를 갖습니다.

  • reduce(kernelName)(필수): 축소 커널이 정의됨을 명시합니다. 반영된 Java 메서드 reduce_kernelName이 커널을 실행합니다.
  • initializer(initializerName)(선택사항): 이 축소 커널의 initializer 함수 이름을 지정합니다. 커널이 실행되면 RenderScript가 각 누산기 데이터 항목에 한 번씩 이 함수를 호출합니다. 함수는 다음과 같이 정의해야 합니다.

    static void initializerName(accumType *accum) { … }

    accum은 이 함수가 초기화할 누산기 데이터 항목에 관한 포인터입니다.

    제공된 initializer 함수가 없으면, RenderScript는 모든 누산기 데이터 항목을 0으로 초기화하고(memset에 의한 동작처럼 보임), 다음과 같은 initializer 함수가 있는 것처럼 동작합니다.

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName)(필수): 이 축소 커널에 관한 accumulator 함수의 이름을 지정합니다. 커널이 실행되면 RenderScript가 입력의 좌표마다 이 함수를 한 번씩 호출하여, 입력에 따라 어떤 식으로든 누산기 데이터 항목을 업데이트합니다. 함수는 다음과 같이 정의해야 합니다.

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

    accum은 이 함수가 수정할 누산기 데이터 항목에 관한 포인터입니다. in1~inN은 커널 실행에 전달된 입력에 따라 자동으로 채워지는 하나 이상의 인수로, 입력당 인수가 하나씩입니다. accumulator 함수는 특수 인수를 선택적으로 취할 수 있습니다.

    dotProduct는 여러 개의 입력이 있는 예제 커널입니다.

  • combiner(combinerName)

    (선택사항): 이 축소 커널의 combiner 함수 이름을 지정합니다. RenderScript는 입력의 좌표마다 accumulator 함수를 한 번씩 호출한 후, 모든 누산기 데이터 항목을 단일 누산기 데이터 항목으로 결합하는 데 필요한 만큼 combiner 함수를 호출합니다. 함수는 다음과 같이 정의해야 합니다.

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

    accum은 이 함수가 수정할 '대상' 누산기 데이터 항목에 관한 포인터입니다. other는 이 함수가 *accum으로 '결합'할 '소스' 누산기 데이터 항목에 관한 포인터입니다.

    참고: *accum, *other 중 하나 또는 둘 다 초기화되었지만 accumulator 함수로 전달되지 않아서 입력 데이터에 따라 업데이트되지 않았을 수 있습니다. 예를 들어 findMinAndMax 커널에서 combiner 함수 fMMCombiner는 명시적으로 idx < 0인지 확인합니다. 그 값으로 확인되면 그러한 누산기 데이터 항목(값이 INITVAL임)임을 나타내기 때문입니다.

    제공된 combiner 함수가 없는 경우 RenderScript는 그 자리에 accumulator 함수를 사용하고 다음과 같은 combiner 함수가 있는 것처럼 동작합니다.

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

    커널에 입력이 두 개 이상 있거나 입력 데이터 유형이 누산기 데이터 유형과 같지 않거나 accumulator 함수가 하나 이상의 특수 인수를 취하는 경우 combiner 함수는 필수입니다.

  • outconverter(outconverterName)(선택사항): 이 축소 커널의 outconverter 함수 이름을 지정합니다. RenderScript는 모든 누산기 데이터 항목을 결합한 후 outconverter 함수를 호출하여 자바에 반환할 축소 결과를 확인합니다. 함수는 다음과 같이 정의해야 합니다.

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

    result는 이 함수가 축소의 결과로 초기화할 결과 데이터 항목에 관한 포인터(RenderScript 런타임에 의해 할당되었지만 초기화되지 않음)입니다. resultType은 데이터 항목 유형으로, accumType과 같을 필요는 없습니다. accumcombiner 함수로 계산된, 최종 누산기 데이터 항목에 관한 포인터입니다.

    제공된 outconverter 함수가 없는 경우 RenderScript는 최종 누산기 데이터 항목을 결과 데이터 항목에 복사하고, 다음과 같은 outconverter 함수가 있는 것처럼 작동합니다.

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

    누산기 데이터 유형과 다른 결과 유형을 원한다면 outconverter 함수는 필수입니다.

커널에는 입력 유형, 누산기 데이터 항목 유형, 결과 유형이 있으며, 어느 것도 동일할 필요가 없습니다. 예를 들어 findMinAndMax 커널에서 입력 유형 long, 누산기 데이터 항목 유형 MinAndMax, 결과 유형 int2는 모두 다릅니다.

추측할 수 없는 사항

지정된 커널을 실행할 때, RenderScript에서 생성된 누산기 데이터 항목의 수에 의존해서는 안 됩니다. 동일한 입력을 갖는 동일한 커널을 두 번 실행해도 생성되는 누산기 데이터 항목의 수가 동일하다는 보장이 없습니다.

또한 RenderScript가 initializer, accumulator 및 combiner 함수를 호출하는 순서에 의존해서도 안 됩니다. RenderScript는 이러한 함수 중 일부를 병렬로 실행할 수도 있습니다. 동일한 입력을 갖는 동일한 커널을 두 번 실행해도 동일한 순서를 따른다는 보장이 없습니다. initializer 함수가 초기화되지 않은 누산기 데이터 항목을 확인한다는 점만 유일하게 보장됩니다. 예:

  • accumulator 함수가 초기화된 누산기 데이터 항목에만 호출되지만, accumulator 함수 호출 전에 모든 누산기 데이터 항목이 초기화된다는 보장이 없습니다.
  • 입력 요소가 accumulator 함수에 전달되는 순서가 보장되지 않습니다.
  • combiner 함수 호출 전에 모든 입력 요소에 accumulator 함수가 호출되었다는 보장이 없습니다.

이로 인해 findMinAndMax 커널은 결정적이지 않습니다. 입력에 동일한 최솟값 또는 최댓값이 두 개 이상 발생하는 경우 커널에서 어떤 것을 찾을지 알 수 없습니다.

반드시 따라야 하는 사항

RenderScript 시스템에서는 커널을 다양한 여러 방식으로 실행할 수 있으므로 커널이 원하는 방식대로 작동하게 하려면 특정 규칙을 따라야 합니다. 그러한 규칙을 따르지 않으면 잘못된 결과나 비결정적 동작 또는 런타임 오류가 발생할 수 있습니다.

아래의 규칙에 따르면 두 개의 누산기 데이터 항목이 '같은 값'을 가져야 합니다. 무슨 의미인가요? 이는 커널에 원하는 기능에 따라 다릅니다. addint와 같은 수학적 축소를 원하는 경우 '같은 값'은 대개 수학적 상등을 의미합니다. findMinAndMax('최소 입력 값과 최대 입력 값의 위치 찾기)와 같이 동일한 입력 값이 두 번 이상 발생할 수 있는 '항목 선택' 검색에서는 지정된 입력 값의 위치가 모두 '동일'해야 한다는 의미입니다. 예를 들어, '가장 왼쪽에 있는 최소 및 최대 입력 값의 위치 찾기'와 유사한 커널을 작성할 수 있습니다. 이 경우 위치 100에 있는 최솟값이 위치 200에 있는 동일한 최솟값보다 선호됩니다. 이 커널에서 '동일'하다는 의미는 단순히 이 같다는 의미가 아니라 위치가 같다는 의미입니다. 이때 accumulator 함수와 combiner 함수는 findMinAndMax의 accumulator 함수와 combiner 함수와 달라야 합니다.

initializer 함수가 식별 값을 생성해야 합니다. 즉, IA가 initializer 함수에 의해 초기화된 누산기 데이터 항목이고 I가 accumulator 함수에 전달된 적이 없는 경우(그러나 A는 전달되었을 수 있음) 다음 사항이 적용됩니다.
  • combinerName(&A, &I)A동일하게 두어야 합니다.
  • combinerName(&I, &A)IA동일하게 두어야 합니다.

예: addint 커널에서 누산기 데이터 항목은 0으로 초기화됩니다. 이 커널의 combiner 함수가 덧셈을 실행합니다. 0은 덧셈을 위한 식별 값입니다.

예: findMinAndMax 커널에서 누산기 데이터 항목은 INITVAL로 초기화됩니다.

  • IINITVAL이므로, fMMCombiner(&A, &I)A를 동일하게 유지합니다.
  • IINITVAL이므로 fMMCombiner(&I, &A)IA로 설정합니다.

따라서 INITVAL는 실제로 식별 값입니다.

combiner 함수는 가환성이어야 합니다. 즉, AB가 initializer 함수에 의해 초기화된 누산기 데이터 항목이고 0회 이상 accumulator 함수에 전달되었을 수 있는 경우, combinerName(&A, &B)A에 설정하는 값은 combinerName(&B, &A)B에 설정하는 값과 같아야 합니다.

예: addint 커널에서 combiner 함수가 두 개의 누산기 데이터 항목 값을 더하는데, 이때 덧셈이 가환성입니다.

예: findMinAndMax 커널에서 fMMCombiner(&A, &B)A = minmax(A, B)와 동일하고 minmax는 가환성이므로 fMMCombiner도 가환성입니다.

combiner 함수는 결합성을 지녀야 합니다. 즉, A, B, C가 initializer 함수에 의해 초기화된 누산기 데이터 항목이고 0회 이상 accumulator 함수에 전달되었을 수 있는 경우 다음 두 코드 시퀀스는 A동일한 값으로 설정해야 합니다.

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

예: addint 커널에서 combiner 함수는 다음과 같이 두 개의 누산기 데이터 항목 값을 더합니다.

  • 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
    

덧셈이 결합성을 지니기 때문에 combiner 함수도 결합성을 지닙니다.

예: findMinAndMax 커널에서

fMMCombiner(&A, &B)
는 다음과 동일합니다.
A = minmax(A, B)
따라서 두 시퀀스는 다음과 같습니다.

  • 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가 결합성을 지니기 때문에 fMMCombiner도 결합성을 지닙니다.

accumulator 함수와 combiner 함수는 기본 접기 규칙을 서로 준수해야 합니다. 즉, AB가 누산기 데이터 항목이고 A가 initializer 함수에 의해 초기화되었고 accumulator 함수에 0번 이상 전달되었을 수 있는 경우 B가 초기화되지 않았고 args가 accumulator 함수에 관한 특정 호출의 입력 인수 및 특수 인수의 목록이면, 다음 두 코드 시퀀스는 A동일한 값으로 설정해야 합니다.

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

예: addint 커널에서 입력 값 V에 다음이 적용됩니다.

  • Statement 1은 A += V와 동일합니다.
  • Statement 2는 B = 0과 동일합니다.
  • Statement 3은 B += V와 동일합니다. 이 값은 B = V와 같습니다.
  • Statement 4는 A += B와 동일합니다. 이 값은 A += V와 같습니다.

Statement 1과 4는 A를 동일한 값으로 설정하므로 이 커널은 기본 접기 규칙을 준수합니다.

예: findMinAndMax 커널에서 좌표 X의 입력 값 V에 다음이 적용됩니다.

  • Statement 1은 A = minmax(A, IndexedVal(V, X))와 동일합니다.
  • Statement 2는 B = INITVAL과 동일합니다.
  • Statement 3은 다음과 동일합니다.
    B = minmax(B, IndexedVal(V, X))
    
    B가 초기값이므로 이것은 다음과 동일합니다.
    B = IndexedVal(V, X)
    
  • Statement 4는 다음과 동일합니다.
    A = minmax(A, B)
    
    이것은 다음과 동일합니다.
    A = minmax(A, IndexedVal(V, X))
    

Statement 1과 4는 A를 동일한 값으로 설정하므로 이 커널은 기본 접기 규칙을 준수합니다.

자바 코드에서 축소 커널 호출

파일 filename.rs에 정의된 kernelName 이름의 축소 커널에는 다음과 같은 세 개의 메서드가 클래스 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);

다음은 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();

메서드 1은 커널의 accumulator 함수의 입력 인수마다 한 개의 입력 Allocation 인수를 갖습니다. RenderScript 런타임은 모든 입력 Allocation이 동일한 차원이고 각 입력 Allocation의 Element 유형이 accumulator 함수의 프로토타입의 입력 인수와 일치하는지 검사합니다. 이러한 검사 중 하나라도 실패하면 RenderScript에서 예외가 발생합니다. 커널은 이러한 차원의 좌표마다 실행됩니다.

메서드 2는 커널 실행을 좌표의 하위 집합으로 제한하는 데 사용할 수 있는 추가 인수 sc를 취한다는 점을 제외하면 메서드 1과 동일합니다.

메서드 3은 Allocation 입력 대신 자바 배열 입력을 취한다는 점을 제외하면 메서드 1과 동일합니다. 이 메서드는 Allocation을 명시적으로 생성한 후 이를 자바 배열에서 데이터로 복사하는 코드를 작성하지 않아도 되므로 편리합니다. 그러나 메서드 1 대신 메서드 3을 사용하면 코드 성능이 향상되지 않습니다. 각 입력 배열의 경우 메서드 3은 적절한 Element 유형과 setAutoPadding(boolean)을 사용 설정한 상태로 1차원 임시 Allocation을 만든 다음, Allocation의 적절한 copyFrom() 메서드가 행한 것처럼 임시 Allocation에 배열을 복사합니다. 그런 다음 메서드 1을 호출하고 그러한 임시 Allocation을 전달합니다.

참고: 애플리케이션이 동일한 배열을 사용하여 또는 동일한 차원과 동일한 요소 유형을 갖는 서로 다른 배열을 사용하여 여러 번의 커널 호출을 하는 경우, 메서드 3을 사용하기보다는 명시적으로 직접 Allocation을 만들고 채우고 재사용하면 성능이 개선될 수 있습니다.

javaFutureType은 반영된 축소 메서드의 반환 유형으로, ScriptC_filename 클래스 내에 반영된 정적 중첩 클래스입니다. 그리고 향후 축소 커널 실행 시의 결과이기도 합니다. 실제 실행 결과를 얻으려면 ScriptC_filename 클래스의 get() 메서드를 호출합니다. 그러면 javaResultType 유형의 값이 반환됩니다. get()동기식입니다.

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() { … }
  }
}

javaResultTypeoutconverter 함수resultType에서 결정됩니다. resultType이 부호 없는 유형(스칼라, 벡터 또는 배열)이 아니면 바로 상응하는 자바 유형이 javaResultType입니다. resultType이 부호 없는 유형이고 부호 있는 더 큰 자바 유형이 있는 경우 부호 있는 더 큰 자바 유형이 javaResultType입니다. 그 외의 경우에는 바로 상응하는 자바 유형이 javaResultType입니다. 예:

  • resultTypeint, int2 또는 int[15]인 경우 javaResultTypeint, Int2 또는 int[]입니다. resultType의 모든 값은 javaResultType으로 표현할 수 있습니다.
  • resultTypeuint, uint2 또는 uint[15]인 경우 javaResultTypelong, Long2 또는 long[]입니다. resultType의 모든 값은 javaResultType으로 표현할 수 있습니다.
  • resultTypeulong, ulong2 또는 ulong[15]인 경우 javaResultTypelong, Long2 또는 long[]입니다. resultType의 특정 값의 경우 javaResultType으로 표현할 수 없습니다.

javaFutureTypeoutconverter 함수resultType에 상응하는 향후 결과 유형입니다.

  • resultType이 배열 유형이 아닌 경우 javaFutureTyperesult_resultType입니다.
  • resultTypememberType 유형의 멤버를 가진 길이 Count의 배열인 경우 javaFutureTyperesultArrayCount_memberType입니다.

예:

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() { … }
  }
}

javaResultType이 객체 유형(배열 유형 포함)인 경우 동일한 인스턴스에서 javaFutureType.get()을 각각 호출하면 같은 객체가 반환됩니다.

javaResultType으로 resultType 유형의 모든 값을 표현할 수는 없고 축소 커널이 표현할 수 없는 값을 생성하면 javaFutureType.get()에서 예외가 발생합니다.

메서드 3 및 devecSiInXType

devecSiInXTypeaccumulator 함수의 상응하는 인수의 inXType에 상응하는 자바 유형입니다. inXType이 부호 없는 유형이거나 벡터 유형이 아닌 경우 devecSiInXType은 바로 상응하는 자바 유형입니다. inXType이 부호 없는 스칼라 유형인 경우 devecSiInXType은 같은 크기의 부호 있는 스칼라 유형에 바로 상응하는 자바 유형입니다. inXType이 부호 있는 벡터 유형이면 devecSiInXType은 벡터 구성요소 유형에 바로 상응하는 자바 유형입니다. inXType이 부호 없는 벡터 유형이면 devecSiInXType은 벡터 구성요소 유형과 같은 크기의 부호 있는 스칼라 유형에 바로 상응하는 자바 유형입니다. 예:

  • inXTypeint이면 devecSiInXTypeint입니다.
  • inXTypeint2이면 devecSiInXTypeint입니다. 배열은 평탄화된 표현입니다. 그리고 Allocation의 2개 구성요소 벡터 요소 수보다 두 배 더 많은 스칼라 요소를 가집니다. 이는 AllocationcopyFrom() 메서드가 작동하는 방식과 동일합니다.
  • inXTypeuint이면 deviceSiInXTypeint입니다. 자바 배열에 있는 부호 있는 값은 Allocation에 있는 동일한 비트 패턴의 부호 없는 값으로 해석됩니다. 이는 AllocationcopyFrom() 메서드가 작동하는 방식과 동일합니다.
  • inXTypeuint2이면 deviceSiInXTypeint입니다. 이는 int2uint의 처리 방식을 결합한 것입니다. 배열은 평탄화된 표현이고, 자바 배열의 부호 있는 값은 RenderScript의 부호 없는 요소 값으로 해석됩니다.

유념할 점은 메서드 3의 경우 입력 유형이 결과 유형과 다르게 처리된다는 점입니다.

  • 스크립트의 벡터 입력은 자바 측에서 평탄화되지만 스크립트의 벡터 결과는 그렇지 않습니다.
  • 스크립트의 부호 없는 입력은 자바 측에서 같은 크기의 부호 있는 입력으로 표현되지만, 스크립트의 부호 없는 결과는 자바 측에서 부호 있는 확장 유형으로 표현됩니다(ulong의 경우 제외).

축소 커널의 추가 예

#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];
}

추가 코드 샘플

BasicRenderScript, RenderScriptIntrinsicHello Compute 샘플에는 이 페이지에서 다루는 API 사용과 관련한 내용이 더 자세히 설명되어 있습니다.