نمای کلی RenderScript

RenderScript چارچوبی برای اجرای وظایف محاسباتی فشرده با عملکرد بالا در اندروید است. RenderScript اساساً برای استفاده با محاسبات موازی داده طراحی شده است، اگرچه بارهای کاری سریال نیز می توانند مفید باشند. زمان اجرا RenderScript کار را در بین پردازنده‌های موجود در یک دستگاه، مانند پردازنده‌های چند هسته‌ای و پردازنده‌های گرافیکی، موازی می‌کند. این به شما امکان می دهد به جای زمان بندی کار، بر بیان الگوریتم ها تمرکز کنید. RenderScript به ویژه برای برنامه هایی که پردازش تصویر، عکاسی محاسباتی یا بینایی کامپیوتر را انجام می دهند مفید است.

برای شروع با RenderScript، دو مفهوم اصلی وجود دارد که باید درک کنید:

  • خود این زبان یک زبان مشتق شده از C99 برای نوشتن کدهای محاسباتی با کارایی بالا است. نوشتن یک هسته RenderScript نحوه استفاده از آن برای نوشتن هسته های محاسباتی را توضیح می دهد.
  • کنترل API برای مدیریت طول عمر منابع RenderScript و کنترل اجرای کرنل استفاده می شود. این در سه زبان مختلف موجود است: جاوا، C++ در Android NDK، و خود زبان هسته مشتق شده از C99. با استفاده از RenderScript از Java Code و Single-Source RenderScript به ترتیب گزینه های اول و سوم را شرح می دهند.

نوشتن یک هسته RenderScript

یک هسته RenderScript معمولاً در یک فایل .rs در فهرست <project_root>/src/rs قرار دارد. هر فایل .rs یک اسکریپت نامیده می شود. هر اسکریپت حاوی مجموعه ای از هسته ها، توابع و متغیرهای خاص خود است. یک اسکریپت می تواند شامل موارد زیر باشد:

  • یک بیانیه پراگما ( #pragma version(1) ) که نسخه زبان هسته RenderScript استفاده شده در این اسکریپت را اعلام می کند. در حال حاضر، 1 تنها مقدار معتبر است.
  • یک اعلان پراگما ( #pragma rs java_package_name(com.example.app) ) که نام بسته کلاس‌های جاوا منعکس شده از این اسکریپت را اعلام می‌کند. توجه داشته باشید که فایل .rs شما باید بخشی از بسته برنامه شما باشد و نه در پروژه کتابخانه ای.
  • توابع غیر قابل فراخوانی صفر یا بیشتر یک تابع فراخوانی یک تابع RenderScript تک رشته ای است که می توانید از کد جاوا با آرگومان های دلخواه فراخوانی کنید. اینها اغلب برای راه اندازی اولیه یا محاسبات سریال در یک خط لوله پردازش بزرگتر مفید هستند.
  • صفر یا بیشتر جهانی های اسکریپت . یک اسکریپت جهانی شبیه به یک متغیر سراسری در C است. می‌توانید از کد جاوا به جهانی‌های اسکریپت دسترسی داشته باشید، و اغلب برای ارسال پارامتر به هسته‌های RenderScript استفاده می‌شوند. جهانی های اسکریپت در اینجا با جزئیات بیشتری توضیح داده شده است.

  • هسته محاسباتی صفر یا بیشتر هسته محاسباتی یک تابع یا مجموعه ای از توابع است که می توانید زمان اجرا RenderScript را به صورت موازی در مجموعه ای از داده ها اجرا کنید. دو نوع هسته محاسباتی وجود دارد: هسته‌های نقشه‌برداری (که به آن هسته‌های پیشرو نیز گفته می‌شود) و هسته‌های کاهشی .

    هسته نقشه برداری یک تابع موازی است که بر روی مجموعه ای از Allocations با ابعاد مشابه عمل می کند. به طور پیش فرض، برای هر مختصات در آن ابعاد یک بار اجرا می شود. معمولاً (اما نه به طور انحصاری) برای تبدیل مجموعه ای از Allocations ورودی به Allocation خروجی یک Element در یک زمان استفاده می شود.

    • در اینجا یک نمونه از یک هسته نقشه برداری ساده آورده شده است:

      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 ورودی ارسال شده به راه اندازی هسته پر می شود. آرگومان های x و y در زیر مورد بحث قرار می گیرند. مقدار بازگشتی از هسته به طور خودکار در محل مناسب در Allocation خروجی نوشته می شود. به‌طور پیش‌فرض، این هسته در کل Allocation ورودی‌اش اجرا می‌شود، با یک اجرای تابع هسته در هر Element در Allocation .

      یک هسته نگاشت ممکن است یک یا چند Allocations ورودی، یک Allocation خروجی واحد یا هر دو داشته باشد. زمان اجرا RenderScript بررسی می‌کند تا اطمینان حاصل شود که همه تخصیص‌های ورودی و خروجی دارای ابعاد یکسان هستند، و اینکه انواع Element تخصیص ورودی و خروجی با نمونه اولیه هسته مطابقت دارند. اگر هر یک از این بررسی ها ناموفق باشد، RenderScript یک استثنا ایجاد می کند.

      توجه: قبل از Android 6.0 (سطح API 23)، یک هسته نقشه برداری ممکن است بیش از یک Allocation ورودی نداشته باشد.

      اگر به Allocations ورودی یا خروجی بیشتری نسبت به هسته نیاز دارید، آن اشیا باید به اسکریپت های جهانی rs_allocation متصل شوند و از طریق یک هسته یا تابع فراخوانی از طریق rsGetElementAt_ type () یا rsSetElementAt_ type () قابل دسترسی باشند.

      توجه: RS_KERNEL یک ماکرو است که به طور خودکار توسط RenderScript برای راحتی شما تعریف شده است:

      #define RS_KERNEL __attribute__((kernel))
      

    هسته کاهشی خانواده ای از توابع است که بر روی مجموعه ای از Allocations ورودی با ابعاد مشابه عمل می کند. به طور پیش فرض، تابع انباشته آن برای هر مختصات در آن ابعاد یک بار اجرا می شود. معمولاً (اما نه به طور انحصاری) برای "کاهش" مجموعه ای از 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 نیاز دارد. بسته به آنچه که می خواهید هسته انجام دهد، ممکن است عملکردهای دیگری نیز داشته باشد.

      یک تابع انباشته کننده هسته کاهش باید void برگرداند و باید حداقل دو آرگومان داشته باشد. آرگومان اول ( accum ، در این مثال) یک اشاره گر به یک آیتم داده انباشت کننده است و دومی ( val ، در این مثال) به طور خودکار براساس Allocation ورودی ارسال شده به راه اندازی هسته پر می شود. آیتم داده انباشته توسط زمان اجرا RenderScript ایجاد می شود. به طور پیش فرض، مقدار اولیه آن صفر است. به‌طور پیش‌فرض، این هسته در کل Allocation ورودی‌اش اجرا می‌شود، با یک اجرای تابع انباشته‌کننده در هر Element در Allocation . به طور پیش فرض، مقدار نهایی آیتم داده انباشته کننده به عنوان نتیجه کاهش در نظر گرفته می شود و به جاوا برگردانده می شود. زمان اجرا RenderScript بررسی می کند تا مطمئن شود که نوع Element تخصیص ورودی با نمونه اولیه تابع انطباق دهنده مطابقت دارد. اگر مطابقت نداشته باشد، RenderScript یک استثنا ایجاد می کند.

      یک هسته کاهش دارای یک یا چند Allocations ورودی است اما هیچ Allocations خروجی ندارد.

      هسته های کاهشی در اینجا با جزئیات بیشتری توضیح داده شده اند.

      هسته های کاهش در اندروید 7.0 (سطح API 24) و جدیدتر پشتیبانی می شوند.

    یک تابع هسته نگاشت یا یک تابع انباشته کننده هسته کاهشی ممکن است با استفاده از آرگومان های خاص x ، y و z که باید از نوع int یا uint32_t باشند، به مختصات اجرای فعلی دسترسی پیدا کند. این آرگومان ها اختیاری هستند.

    یک تابع هسته نگاشت یا یک تابع انباشته کننده هسته کاهش نیز ممکن است context آرگومان خاص اختیاری از نوع rs_kernel_context را بگیرد. این مورد برای خانواده ای از APIهای زمان اجرا مورد نیاز است که برای پرس و جو از ویژگی های خاص اجرای فعلی استفاده می شود - برای مثال rsGetDimX . (آگومان context در Android 6.0 (سطح API 23) و جدیدتر موجود است.)

  • یک تابع init() اختیاری. تابع init() نوع خاصی از تابع قابل فراخوانی است که RenderScript زمانی که اسکریپت برای اولین بار نمونه سازی می شود اجرا می کند. این اجازه می دهد تا برخی از محاسبات به طور خودکار هنگام ایجاد اسکریپت انجام شود.
  • جهانی ها و توابع اسکریپت صفر یا بیشتر. یک اسکریپت استاتیک جهانی معادل یک اسکریپت جهانی است با این تفاوت که از کد جاوا قابل دسترسی نیست. یک تابع استاتیک یک تابع استاندارد C است که می تواند از هر هسته یا تابع فراخوانی در اسکریپت فراخوانی شود، اما در معرض Java API نیست. اگر یک اسکریپت جهانی یا تابع نیازی به دسترسی از کد جاوا نداشته باشد، بسیار توصیه می شود که آن را static اعلام کنید.

تنظیم دقت ممیز شناور

شما می توانید سطح مورد نیاز از دقت ممیز شناور را در یک اسکریپت کنترل کنید. اگر استاندارد کامل IEEE 754-2008 (به طور پیش فرض استفاده می شود) مورد نیاز نباشد، مفید است. پراگماهای زیر می توانند سطح متفاوتی از دقت ممیز شناور را تعیین کنند:

  • #pragma rs_fp_full (پیش‌فرض اگر چیزی مشخص نشده باشد): برای برنامه‌هایی که به دقت ممیز شناور طبق استاندارد IEEE 754-2008 نیاز دارند.
  • #pragma rs_fp_relaxed : برای برنامه هایی که به انطباق دقیق IEEE 754-2008 نیاز ندارند و می توانند دقت کمتری را تحمل کنند. این حالت تراز به صفر را برای دنورم و دور به سمت صفر را فعال می کند.
  • #pragma rs_fp_imprecise : برای برنامه هایی که الزامات دقیق دقیقی ندارند. این حالت همه چیز را در rs_fp_relaxed به همراه موارد زیر فعال می کند:
    • عملیاتی که منجر به -0.0 می شود می تواند در عوض +0.0 را برگرداند.
    • عملیات روی INF و NAN تعریف نشده است.

اکثر برنامه ها می توانند از rs_fp_relaxed بدون هیچ گونه عوارض جانبی استفاده کنند. این ممکن است در برخی از معماری‌ها به دلیل بهینه‌سازی‌های اضافی که فقط با دقت آرام در دسترس هستند (مانند دستورالعمل‌های CPU SIMD) بسیار سودمند باشد.

دسترسی به API های RenderScript از جاوا

هنگام توسعه یک برنامه اندرویدی که از RenderScript استفاده می کند، می توانید به یکی از دو روش از جاوا به API آن دسترسی داشته باشید:

  • android.renderscript - APIهای این بسته کلاسی در دستگاه‌های دارای Android نسخه 3.0 (سطح API 11) و بالاتر در دسترس هستند.
  • android.support.v8.renderscript - APIهای موجود در این بسته از طریق کتابخانه پشتیبانی در دسترس هستند که به شما امکان می دهد از آنها در دستگاه های دارای Android نسخه 2.3 (سطح API 9) و بالاتر استفاده کنید.

در اینجا معاوضه ها وجود دارد:

  • اگر از API های کتابخانه پشتیبانی استفاده می کنید، بخش RenderScript برنامه شما با دستگاه های دارای Android نسخه 2.3 (سطح API 9) و بالاتر سازگار خواهد بود، صرف نظر از اینکه از کدام ویژگی های RenderScript استفاده می کنید. این به برنامه شما اجازه می دهد تا در دستگاه های بیشتری نسبت به زمانی که از API های بومی ( android.renderscript ) استفاده می کنید، کار کند.
  • برخی از ویژگی‌های RenderScript از طریق APIهای کتابخانه پشتیبانی در دسترس نیستند.
  • اگر از APIهای کتابخانه پشتیبانی استفاده کنید، APKهای بزرگتری (احتمالاً به طور قابل توجهی) نسبت به APIهای بومی ( android.renderscript ) دریافت خواهید کرد.

با استفاده از APIهای کتابخانه پشتیبانی RenderScript

برای استفاده از API های RenderScript کتابخانه پشتیبانی، باید محیط توسعه خود را پیکربندی کنید تا بتوانید به آنها دسترسی داشته باشید. ابزار Android SDK زیر برای استفاده از این APIها مورد نیاز است:

  • نسخه 22.2 یا بالاتر Android SDK Tools
  • نسخه 18.1.0 یا بالاتر Android SDK Build-tools

توجه داشته باشید که از Android SDK Build-tools 24.0.0، Android 2.2 (سطح API 8) دیگر پشتیبانی نمی‌شود.

می توانید نسخه نصب شده این ابزارها را در Android SDK Manager بررسی و به روز کنید.

برای استفاده از پشتیبانی کتابخانه RenderScript API:

  1. مطمئن شوید که نسخه Android SDK مورد نیاز را نصب کرده اید.
  2. تنظیمات فرآیند ساخت اندروید را به‌روزرسانی کنید تا تنظیمات RenderScript را نیز شامل شود:
    • فایل build.gradle را در پوشه برنامه ماژول برنامه خود باز کنید.
    • تنظیمات RenderScript زیر را به فایل اضافه کنید:

      شیار

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

      کاتلین

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

      تنظیمات لیست شده در بالا رفتار خاصی را در فرآیند ساخت اندروید کنترل می کنند:

      • renderscriptTargetApi - نسخه بایت کدی را که باید تولید شود را مشخص می کند. توصیه می‌کنیم این مقدار را روی پایین‌ترین سطح API تنظیم کنید که می‌تواند تمام عملکردهایی را که استفاده می‌کنید ارائه کند و renderscriptSupportModeEnabled را روی true تنظیم کنید. مقادیر معتبر برای این تنظیم هر عدد صحیحی از 11 تا آخرین سطح API منتشر شده است. اگر حداقل نسخه SDK مشخص شده در مانیفست برنامه شما روی مقدار دیگری تنظیم شده باشد، آن مقدار نادیده گرفته می شود و مقدار هدف در فایل ساخت برای تنظیم حداقل نسخه SDK استفاده می شود.
      • renderscriptSupportModeEnabled - مشخص می کند که اگر دستگاهی که روی آن اجرا می شود نسخه هدف را پشتیبانی نمی کند، بایت کد تولید شده باید به نسخه سازگار برگردد.
  3. در کلاس های برنامه خود که از RenderScript استفاده می کنند، یک import برای کلاس های کتابخانه پشتیبانی اضافه کنید:

    کاتلین

    import android.support.v8.renderscript.*
    

    جاوا

    import android.support.v8.renderscript.*;
    

با استفاده از RenderScript از جاوا یا کد Kotlin

استفاده از RenderScript از کد جاوا یا Kotlin به کلاس های API واقع در بسته android.renderscript یا android.support.v8.renderscript متکی است. اکثر برنامه ها از همان الگوی استفاده اولیه پیروی می کنند:

  1. یک زمینه RenderScript را راه اندازی کنید. زمینه RenderScript که با create(Context) ایجاد شده است، اطمینان می دهد که RenderScript می تواند مورد استفاده قرار گیرد و یک شی برای کنترل طول عمر تمام اشیاء RenderScript بعدی ارائه می دهد. شما باید ایجاد زمینه را یک عملیات بالقوه طولانی مدت در نظر بگیرید، زیرا ممکن است منابعی را روی قطعات مختلف سخت افزار ایجاد کند. در صورت امکان نباید در مسیر بحرانی برنامه قرار گیرد. به طور معمول، یک برنامه در هر زمان تنها یک زمینه RenderScript دارد.
  2. حداقل یک Allocation برای ارسال به یک اسکریپت ایجاد کنید. Allocation یک شی RenderScript است که برای مقدار ثابتی از داده ها ذخیره می کند. هسته‌های موجود در اسکریپت‌ها، اشیاء Allocation را به عنوان ورودی و خروجی خود می‌گیرند، و اشیاء Allocation را می‌توان در هسته‌ها با استفاده از rsGetElementAt_ type () و rsSetElementAt_ type () هنگامی که به‌عنوان جهانی‌های اسکریپت محدود می‌شود، دسترسی داشت. اشیاء Allocation آرایه ها اجازه می دهد تا از کد جاوا به کد RenderScript و بالعکس منتقل شوند. اشیاء Allocation معمولاً با استفاده از createTyped() یا createFromBitmap() ایجاد می شوند.
  3. هر اسکریپت لازم را ایجاد کنید. هنگام استفاده از RenderScript دو ​​نوع اسکریپت در دسترس شماست:
    • ScriptC : اینها اسکریپت های تعریف شده توسط کاربر هستند که در بالا توضیح داده شد . هر اسکریپت دارای یک کلاس جاوا است که توسط کامپایلر RenderScript منعکس شده است تا دسترسی به اسکریپت از کد جاوا آسان شود. این کلاس دارای نام ScriptC_ filename است. برای مثال، اگر هسته نگاشت فوق در invert.rs قرار داشته باشد و یک زمینه RenderScript قبلاً در mRenderScript قرار داشته باشد، کد جاوا یا Kotlin برای نمونه سازی اسکریپت به این صورت خواهد بود:

      کاتلین

      val invert = ScriptC_invert(renderScript)
      

      جاوا

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic : اینها هسته‌های RenderScript داخلی برای عملیات رایج مانند تاری گاوسی، کانولوشن و ترکیب تصویر هستند. برای اطلاعات بیشتر، به زیر کلاس های ScriptIntrinsic مراجعه کنید.
  4. تخصیص ها را با داده ها پر کنید. به‌جز تخصیص‌هایی که با createFromBitmap() ایجاد می‌شوند، یک تخصیص زمانی که برای اولین بار ایجاد می‌شود با داده‌های خالی پر می‌شود. برای پر کردن یک تخصیص، از یکی از روش‌های «کپی» در Allocation استفاده کنید. روش های "کپی" همزمان هستند.
  5. هر جهانی اسکریپت لازم را تنظیم کنید. می‌توانید با استفاده از روش‌هایی در همان کلاس ScriptC_ filename با نام set_ globalname جهانی‌ها را تنظیم کنید. به عنوان مثال، برای تنظیم یک متغیر int به نام threshold ، از متد جاوا set_threshold(int) استفاده کنید. و برای تنظیم یک متغیر rs_allocation با نام lookup ، از متد جاوا set_lookup(Allocation) استفاده کنید. متدهای set ناهمزمان هستند.
  6. هسته های مناسب و توابع فراخوانی را راه اندازی کنید.

    روش‌های راه‌اندازی یک هسته مشخص در همان کلاس ScriptC_ filename با روش‌هایی با نام‌های forEach_ mappingKernelName () یا reduce_ reductionKernelName () منعکس می‌شوند. این راه اندازی ها ناهمزمان هستند. بسته به آرگومان های هسته، متد یک یا چند تخصیص می گیرد که همه آنها باید ابعاد یکسانی داشته باشند. به طور پیش فرض، یک هسته روی هر مختصاتی در آن ابعاد اجرا می شود. برای اجرای یک هسته بر روی زیرمجموعه ای از آن مختصات، یک Script.LaunchOptions مناسب را به عنوان آخرین آرگومان به متد forEach یا reduce ارسال کنید.

    توابع قابل فراخوانی را با استفاده از متدهای invoke_ functionName که در همان کلاس ScriptC_ filename منعکس شده است، راه اندازی کنید. این راه اندازی ها ناهمزمان هستند.

  7. بازیابی داده ها از اشیاء Allocation و javaFutureType . برای دسترسی به داده‌های یک Allocation از کد جاوا، باید آن داده‌ها را با استفاده از یکی از روش‌های «کپی» در Allocation به جاوا کپی کنید. برای به دست آوردن نتیجه یک هسته کاهش، باید از متد javaFutureType .get() استفاده کنید. متدهای "copy" و get() همزمان هستند.
  8. زمینه RenderScript را از بین ببرید. شما می توانید زمینه RenderScript را با destroy() یا با اجازه دادن به آبجکت زمینه RenderScript برای جمع آوری زباله از بین ببرید. این باعث می شود که هر گونه استفاده بیشتر از هر شی متعلق به آن زمینه، یک استثنا ایجاد کند.

مدل اجرای ناهمزمان

متدهای منعکس شده forEach ، invoke ، reduce و set ناهمزمان هستند - هر کدام ممکن است قبل از تکمیل عمل درخواستی به جاوا بازگردند. با این حال، اقدامات فردی به ترتیبی که راه اندازی می شوند سریال می شوند.

کلاس Allocation متدهای "کپی" را برای کپی کردن داده ها به و از Allocations فراهم می کند. یک روش "کپی" همزمان است و با توجه به هر یک از اقدامات ناهمزمان بالا که همان تخصیص را لمس می کنند، سریال سازی می شود.

کلاس های javaFutureType منعکس شده یک متد get() برای به دست آوردن نتیجه کاهش ارائه می کنند. get() همزمان است و با توجه به کاهش (که ناهمزمان است) سریال می شود.

RenderScript تک منبعی

Android 7.0 (سطح API 24) یک ویژگی برنامه نویسی جدید به نام Single-Source RenderScript را معرفی می کند که در آن هسته ها از اسکریپتی که در آن تعریف شده اند، به جای جاوا راه اندازی می شوند. این رویکرد در حال حاضر محدود به هسته‌های نقشه‌برداری است که در این بخش برای مختصر به آن‌ها به سادگی به عنوان «کرنل» یاد می‌شود. این ویژگی جدید همچنین از ایجاد تخصیص هایی از نوع rs_allocation از داخل اسکریپت پشتیبانی می کند. اکنون می‌توان یک الگوریتم کامل را تنها در یک اسکریپت پیاده‌سازی کرد، حتی اگر چندین راه‌اندازی هسته مورد نیاز باشد. مزیت دو جانبه است: کد قابل خواندن تر، زیرا اجرای الگوریتم را در یک زبان حفظ می کند. و کد بالقوه سریعتر، به دلیل انتقال کمتر بین جاوا و رندر اسکریپت در راه اندازی چندین هسته.

در RenderScript تک منبعی، شما هسته ها را همانطور که در نوشتن یک هسته RenderScript توضیح داده شده است، می نویسید. سپس یک تابع فراخوانی می نویسید که rsForEach() را فرا می خواند تا آنها را راه اندازی کند. آن API یک تابع هسته را به عنوان اولین پارامتر و به دنبال آن تخصیص ورودی و خروجی می گیرد. یک API مشابه rsForEachWithOptions() یک آرگومان اضافی از نوع rs_script_call_t می گیرد که زیرمجموعه ای از عناصر را از تخصیص های ورودی و خروجی برای عملکرد هسته برای پردازش مشخص می کند.

برای شروع محاسبات RenderScript، تابع فراخوانی را از جاوا فراخوانی می کنید. مراحل استفاده از RenderScript از کد جاوا را دنبال کنید. در مرحله راه‌اندازی هسته‌های مناسب ، تابع invokable را با استفاده از invoke_ function_name () فراخوانی کنید، که کل محاسبات، از جمله راه‌اندازی هسته‌ها را آغاز می‌کند.

تخصیص ها اغلب برای ذخیره و انتقال نتایج میانی از یک هسته به کرنل دیگر مورد نیاز است. می توانید آنها را با استفاده از rsCreateAllocation() ایجاد کنید. یکی از اشکال آسان برای استفاده از آن API rsCreateAllocation_<T><W>(…) است که در آن T نوع داده برای یک عنصر و W عرض برداری برای عنصر است. API اندازه های ابعاد X، Y و Z را به عنوان آرگومان می گیرد. برای تخصیص یک بعدی یا دو بعدی، اندازه ابعاد Y یا Z را می توان حذف کرد. برای مثال، rsCreateAllocation_uchar4(16384) یک تخصیص 1 بعدی از 16384 عنصر ایجاد می کند که هر کدام از آنها از نوع uchar4 است.

تخصیص ها توسط سیستم به صورت خودکار مدیریت می شوند. شما مجبور نیستید آنها را به صراحت آزاد یا آزاد کنید. با این حال، می‌توانید با rsClearObject(rs_allocation* alloc) تماس بگیرید تا نشان دهید دیگر نیازی به alloc دسته برای تخصیص اصلی ندارید، تا سیستم بتواند منابع را در اسرع وقت آزاد کند.

بخش Writing a RenderScript Kernel شامل یک هسته نمونه است که یک تصویر را معکوس می کند. مثال زیر آن را برای اعمال بیش از یک افکت بر روی یک تصویر، با استفاده از RenderScript تک منبعی گسترش می دهد. این شامل هسته دیگری به نام greyscale است که یک تصویر رنگی را به سیاه و سفید تبدیل می کند. سپس یک تابع فراخوانی process() آن دو هسته را به طور متوالی بر روی یک تصویر ورودی اعمال می کند و یک تصویر خروجی تولید می کند. تخصیص ها برای ورودی و خروجی به عنوان آرگومان هایی از نوع 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);
}

می توانید تابع process() را از جاوا یا کاتلین به صورت زیر فراخوانی کنید:

کاتلین

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)

جاوا

// 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 پیاده سازی کرد. بدون RenderScript یک منبع، شما باید هر دو هسته را از کد جاوا راه اندازی کنید، راه اندازی هسته را از تعاریف هسته جدا کنید و درک کل الگوریتم را دشوارتر کنید. نه تنها خواندن کد RenderScript تک منبعی آسان تر است، بلکه انتقال بین جاوا و اسکریپت را در سراسر راه اندازی هسته حذف می کند. برخی از الگوریتم‌های تکراری ممکن است هسته‌ها را صدها بار راه‌اندازی کنند که هزینه‌های سربار چنین انتقالی را قابل توجه می‌کند.

جهانی های اسکریپت

یک اسکریپت جهانی یک متغیر جهانی static معمولی در یک فایل اسکریپت ( .rs ) است. برای یک اسکریپت جهانی با نام var که در filename .rs تعریف شده است، یک روش get_ var در کلاس ScriptC_ filename منعکس شده است. مگر اینکه global const باشد، متد set_ var نیز وجود خواهد داشت.

یک اسکریپت جهانی دو مقدار جداگانه دارد - یک مقدار جاوا و یک مقدار اسکریپت . این مقادیر به صورت زیر عمل می کنند:

  • اگر var در اسکریپت دارای مقدار اولیه استاتیک باشد، مقدار اولیه var را هم در جاوا و هم در اسکریپت مشخص می کند. در غیر این صورت، آن مقدار اولیه صفر است.
  • دسترسی به var در داخل اسکریپت مقدار اسکریپت آن را می خواند و می نویسد.
  • متد get_ var مقدار جاوا را می خواند.
  • متد set_ var (اگر وجود داشته باشد) مقدار جاوا را فوراً می نویسد و مقدار اسکریپت را به صورت ناهمزمان می نویسد.

توجه: این بدان معنی است که به جز هر مقدار اولیه ایستا در اسکریپت، مقادیری که از داخل یک اسکریپت به یک global نوشته شده است برای جاوا قابل مشاهده نیستند.

کاهش هسته ها در عمق

کاهش فرآیند ترکیب مجموعه ای از داده ها در یک مقدار واحد است. این یک نرم افزار ابتدایی مفید در برنامه نویسی موازی با برنامه هایی مانند موارد زیر است:

  • محاسبه مجموع یا محصول بر روی تمام داده ها
  • محاسبه عملیات منطقی ( and ، or ، xor ) روی همه داده ها
  • یافتن حداقل یا حداکثر مقدار در داده ها
  • جستجو برای یک مقدار خاص یا مختصات یک مقدار خاص در داده ها

در Android 7.0 (سطح API 24) و نسخه‌های جدیدتر، RenderScript از هسته‌های کاهش پشتیبانی می‌کند تا الگوریتم‌های کاهش کارآمد نوشته‌شده توسط کاربر را امکان‌پذیر کند. می توانید هسته های کاهشی را روی ورودی هایی با ابعاد 1، 2 یا 3 راه اندازی کنید.

مثال بالا یک هسته کاهش افزودنی ساده را نشان می دهد. در اینجا یک هسته کاهش findMinAndMax پیچیده تر است که مکان های حداقل و حداکثر مقادیر long را در یک Allocation 1 بعدی پیدا می کند:

#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 ) توسط تابع انباشته کننده هسته تعیین می شود -- اولین آرگومان آن تابع یک اشاره گر به یک آیتم داده انباشته است. به طور پیش‌فرض، هر آیتم داده انباشته‌کننده به صفر مقدار دهی اولیه می‌شود (مثلاً توسط memset ). با این حال، می توانید یک تابع اولیه برای انجام کاری متفاوت بنویسید.

مثال: در هسته addint ، اقلام داده انباشت کننده (از نوع int ) برای جمع کردن مقادیر ورودی استفاده می شود. هیچ تابع اولیه وجود ندارد، بنابراین هر آیتم داده انباشته به صفر مقداردهی اولیه می شود.

مثال: در هسته findMinAndMax ، اقلام داده انباشته کننده (از نوع MinAndMax ) برای پیگیری حداقل و حداکثر مقادیر یافت شده تا کنون استفاده می شود. یک تابع اولیه وجود دارد که اینها را به ترتیب روی LONG_MAX و LONG_MIN تنظیم می کند. و برای تنظیم مکان این مقادیر به -1، که نشان می دهد که مقادیر در واقع در بخش (خالی) ورودی پردازش شده وجود ندارند.

RenderScript تابع انباشتگر شما را یکبار برای هر مختصات در ورودی(ها) فراخوانی می کند. به طور معمول، تابع شما باید آیتم داده انباشته کننده را به نحوی مطابق با ورودی به روز کند.

مثال: در هسته addint ، تابع accumulator مقدار یک عنصر ورودی را به آیتم داده accumulator اضافه می کند.

مثال: در هسته findMinAndMax ، تابع انباشتگر بررسی می کند که آیا مقدار یک عنصر ورودی کمتر یا مساوی با حداقل مقدار ثبت شده در آیتم داده انباشت کننده و/یا بزرگتر یا مساوی با حداکثر مقدار ثبت شده در انباشته است یا خیر. مورد داده، و مورد داده انباشته کننده را بر این اساس به روز می کند.

پس از اینکه تابع انباشت کننده یک بار برای هر مختصات در ورودی (ها) فراخوانی شد، RenderScript باید آیتم های داده انباشته کننده را با هم در یک آیتم داده انباشته ترکیب کند . برای این کار می توانید یک تابع ترکیبی بنویسید. اگر تابع accumulator یک ورودی واحد داشته باشد و هیچ آرگومان خاصی نداشته باشد، دیگر نیازی به نوشتن یک تابع ترکیبی ندارید. RenderScript از تابع accumulator برای ترکیب اقلام داده های انباشته کننده استفاده می کند. (اگر این رفتار پیش‌فرض آن چیزی نیست که می‌خواهید، همچنان می‌توانید یک تابع ترکیبی بنویسید.)

مثال: در کرنل addint ، تابع ترکیبی وجود ندارد، بنابراین از تابع accumulator استفاده خواهد شد. این رفتار درستی است، زیرا اگر مجموعه ای از مقادیر را به دو قسمت تقسیم کنیم و مقادیر آن دو قطعه را جداگانه جمع کنیم، جمع کردن آن دو مجموع مانند جمع کردن کل مجموعه است.

مثال: در هسته findMinAndMax ، تابع ترکیب کننده بررسی می کند که آیا حداقل مقدار ثبت شده در آیتم داده انباشته کننده "منبع" *val کمتر از حداقل مقدار ثبت شده در مورد داده انباشته کننده "مقصد" *accum است یا خیر، و *accum را به روز می کند. بر این اساس. کار مشابهی را برای حداکثر مقدار انجام می دهد. این *accum به حالتی به روز می کند که اگر همه مقادیر ورودی در *accum جمع می شدند به جای برخی در *accum و برخی در *val انباشته می شدند.

پس از اینکه همه موارد داده های انباشته با هم ترکیب شدند، RenderScript نتیجه کاهش را برای بازگشت به جاوا تعیین می کند. برای انجام این کار، می توانید یک تابع تبدیل کننده بنویسید. اگر می خواهید مقدار نهایی اقلام داده های انباشته کننده ترکیبی نتیجه کاهش باشد، نیازی به نوشتن تابع مبدل برون نیست.

مثال: در کرنل addint ، هیچ تابع مبدل برون وجود ندارد. مقدار نهایی اقلام داده ترکیبی مجموع تمام عناصر ورودی است که مقداری است که می خواهیم برگردانیم.

مثال: در هسته findMinAndMax ، تابع outconverter یک مقدار نتیجه int2 را مقداردهی اولیه می‌کند تا مکان‌های حداقل و حداکثر مقادیر حاصل از ترکیب همه آیتم‌های داده انباشته را نگه دارد.

نوشتن یک هسته کاهش

#pragma rs reduce یک هسته کاهش را با مشخص کردن نام آن و نام و نقش توابع تشکیل دهنده هسته تعریف می کند. همه این توابع باید static باشند. یک هسته کاهش همیشه به یک تابع accumulator نیاز دارد. بسته به کاری که می‌خواهید هسته انجام دهد، می‌توانید برخی یا همه توابع دیگر را حذف کنید.

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

معنی موارد موجود در #pragma به شرح زیر است:

  • reduce( kernelName ) (اجباری): مشخص می کند که یک هسته کاهش تعریف می شود. یک روش جاوای منعکس شده reduce_ kernelName هسته را راه اندازی می کند.
  • initializer( initializerName ) (اختیاری): نام تابع اولیه را برای این هسته کاهشی مشخص می کند. هنگامی که هسته را راه اندازی می کنید، RenderScript این تابع را یک بار برای هر آیتم داده انباشته کننده فراخوانی می کند. تابع باید به صورت زیر تعریف شود:

    static void initializerName(accumType *accum) { … }

    accum یک اشاره گر به یک آیتم داده انباشت کننده برای مقداردهی اولیه این تابع است.

    اگر یک تابع اولیه ارائه نکنید، RenderScript هر آیتم داده انباشته‌کننده را صفر می‌کند (مثلاً توسط memset )، طوری رفتار می‌کند که انگار یک تابع اولیه وجود دارد که شبیه این است:

    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 تا in N یک یا چند آرگومان هستند که به طور خودکار بر اساس ورودی‌های ارسال شده به راه‌اندازی هسته، یک آرگومان در هر ورودی، پر می‌شوند. تابع accumulator ممکن است به صورت اختیاری هر یک از آرگومان های خاص را بگیرد.

    یک هسته نمونه با چندین ورودی، dotProduct است.

  • combiner( combinerName )

    (اختیاری): نام تابع ترکیب کننده را برای این هسته کاهشی مشخص می کند. پس از اینکه RenderScript تابع انباشت کننده را یک بار برای هر مختصات در ورودی(ها) فراخوانی کرد، این تابع را هر چند بار که لازم است برای ترکیب همه آیتم های داده انباشته در یک آیتم داده انباشته کننده واحد فراخوانی می کند. تابع باید به صورت زیر تعریف شود:

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

    accum یک اشاره گر به یک آیتم داده انباشت کننده "مقصد" است که این تابع را تغییر می دهد. other یک اشاره گر به یک آیتم داده انباشت کننده "منبع" برای "ترکیب" این تابع در *accum است.

    توجه: ممکن است *accum ، *other ، یا هر دو مقداردهی اولیه شده باشند اما هرگز به تابع accumulator منتقل نشده باشند. یعنی یک یا هر دو هرگز طبق هیچ داده ورودی به روز نشده اند. به عنوان مثال، در هسته findMinAndMax ، تابع ترکیبی fMMCombiner صریحاً idx < 0 را بررسی می‌کند، زیرا چنین آیتم داده انباشته‌ای را نشان می‌دهد که مقدار آن INITVAL است.

    اگر یک تابع ترکیبی ارائه نکنید، RenderScript از تابع ادغام کننده در جای خود استفاده می کند، طوری رفتار می کند که گویی یک تابع ترکیبی وجود دارد که به شکل زیر است:

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

    اگر هسته بیش از یک ورودی داشته باشد، اگر نوع داده ورودی با نوع داده انباشته‌کننده یکسان نباشد، یا اگر تابع انباشته یک یا چند آرگومان خاص داشته باشد، یک تابع ترکیب‌کننده اجباری است.

  • outconverter( outconverterName ) (اختیاری): نام تابع تبدیل کننده را برای این هسته کاهشی مشخص می کند. پس از اینکه RenderScript همه اقلام داده های انباشته کننده را ترکیب کرد، این تابع را فراخوانی می کند تا نتیجه کاهش را برای بازگشت به جاوا مشخص کند. تابع باید به صورت زیر تعریف شود:

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

    result یک اشاره گر به یک آیتم داده نتیجه است (تخصیص داده شده اما توسط زمان اجرا RenderScript مقداردهی اولیه نشده است) تا این تابع با نتیجه کاهش مقداردهی اولیه شود. resultType نوع آن آیتم داده است که نیازی به مشابه accumType ندارد. accum یک اشاره گر به آیتم داده انباشته نهایی است که توسط تابع ترکیب کننده محاسبه می شود.

    اگر تابع مبدل برون‌کنار ارائه نکنید، RenderScript آیتم داده‌های جمع‌آوری نهایی را در آیتم داده نتیجه کپی می‌کند، به گونه‌ای رفتار می‌کند که گویی یک تابع مبدل برون‌گرا وجود دارد که شبیه این است:

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

    اگر نوع نتیجه متفاوتی نسبت به نوع داده انباشته می خواهید، عملکرد مبدل برون اجباری است.

توجه داشته باشید که یک کرنل دارای انواع ورودی، نوع آیتم داده انباشته کننده و نوع نتیجه است که هیچ کدام نیازی به یکسان بودن ندارند. به عنوان مثال، در هسته findMinAndMax ، نوع ورودی long ، نوع داده جمع‌آوری MinAndMax و نوع نتیجه int2 متفاوت است.

چه چیزی را نمی توانید فرض کنید؟

برای راه اندازی هسته معین نباید به تعداد آیتم های داده انباشته کننده ایجاد شده توسط RenderScript تکیه کنید. هیچ تضمینی وجود ندارد که دو راه‌اندازی از یک هسته با ورودی(های) یکسان، تعداد یکسانی از اقلام داده انباشته‌کننده را ایجاد کند.

شما نباید به نظمی که RenderScript با عملکردهای اولیه ، باتری و ترکیبات Combiner تماس می گیرد ، اعتماد کنید. حتی ممکن است برخی از آنها را به طور موازی صدا کند. هیچ تضمینی وجود ندارد که دو پرتاب از همان هسته با همان ورودی از همان ترتیب پیروی کنند. تنها ضمانت این است که فقط عملکرد اولیه سازنده یک مورد داده باتری غیرمجاز را مشاهده می کند. به عنوان مثال:

  • هیچ تضمینی وجود ندارد که قبل از فراخوانی عملکرد باتری ، تمام موارد داده های باتری اولیه اولیه شود ، اگرچه فقط در مورد یک مورد داده های جمع کننده اولیه فراخوانی می شود.
  • هیچ تضمینی در مورد ترتیب انتقال عناصر ورودی به عملکرد باتری وجود ندارد.
  • هیچ تضمینی وجود ندارد که قبل از فراخوانی عملکرد Combiner ، عملکرد باتری برای همه عناصر ورودی فراخوانی شده باشد.

یک نتیجه از این امر این است که هسته FindminandMax قطعی نیست: اگر ورودی حاوی بیش از یک مورد با حداقل ارزش یا حداکثر باشد ، شما هیچ راهی برای دانستن اینکه هسته را پیدا می کند ، ندارید.

چه چیزی را باید تضمین کنید؟

از آنجا که سیستم RenderScript می تواند به روش های مختلف هسته را اجرا کند ، باید از قوانین خاصی پیروی کنید تا اطمینان حاصل شود که هسته شما به روشی که می خواهید رفتار می کند. اگر این قوانین را رعایت نکنید ، ممکن است نتایج نادرست ، رفتار غیر تعیین کننده یا خطاهای زمان اجرا را بدست آورید.

قوانین زیر اغلب می گویند که دو مورد داده باتلاق باید " یک مقدار" داشته باشند. این به چه معناست؟ این بستگی به کاری دارد که شما می خواهید هسته انجام دهد. برای کاهش ریاضی مانند افزودن ، معمولاً معنی دارد که "همان" به معنای برابری ریاضی باشد. برای "انتخاب هر جستجو" مانند FindMinandMax ("مکان حداقل و حداکثر مقادیر ورودی" را پیدا کنید) که ممکن است بیش از یک مورد از مقادیر ورودی یکسان باشد ، تمام مکان های یک مقدار ورودی معین باید "یکسان" در نظر گرفته شوند . شما می توانید یک هسته مشابه را برای "پیدا کردن محل چپ ترین و حداکثر مقادیر ورودی" بنویسید که در آن (مثلاً) حداقل مقدار در مکان 100 بیش از حداقل مقدار یکسان در مکان 200 ترجیح داده می شود. برای این هسته ، "همان" به معنای موقعیت یکسان است ، نه صرفاً ارزش یکسان ، و توابع باتری و ترکیب کننده باید متفاوت از مواردی باشد که برای FindMinandMax وجود دارد.

عملکرد اولیه ساز باید یک مقدار هویت ایجاد کند. یعنی اگر I و A موارد داده های جمع کننده اولیه توسط عملکرد اولیه سازنده شده اند ، و I هرگز به عملکرد باتری منتقل نشده ام (اما A است) ، پس از آن
  • combinerName (& A , & I ) باید A را ترک کند
  • combinerName (& I , & A ) باید I همانند A

مثال: در هسته افزودنی ، یک مورد داده باتری به صفر می رسد. عملکرد Combiner برای این هسته علاوه بر این عمل می کند. صفر مقدار هویت برای افزودن است.

مثال: در هسته FindMinandMax ، یک مورد داده باتری به INITVAL آغاز می شود.

  • fMMCombiner(& A , & I ) A را ترک می کند ، زیرا I INITVAL .
  • fMMCombiner(& I , & A ) I به A می رساند ، زیرا I INITVAL .

بنابراین ، INITVAL در واقع یک ارزش هویت است.

عملکرد Combiner باید قابل حمل باشد. یعنی اگر A و B موارد جمع کننده داده های اولیه توسط عملکرد اولیه سازنده باشند ، و ممکن است به عملکرد باتری صفر یا بیشتر منتقل شود ، سپس combinerName (& A , & B ) باید A مقدار مشابه را combinerName (& B , & A ) کند combinerName (& B , & A ) مجموعه های B .

مثال: در هسته Addint ، عملکرد Combiner دو مقدار مورد داده جمع کننده را اضافه می کند. علاوه بر این ، کمتری است.

مثال: در هسته FindminandMax ، fMMCombiner(& A , & B ) همان A = minmax( A , B ) است و minmax کم است ، بنابراین fMMCombiner نیز هست.

عملکرد Combiner باید انجمنی باشد. این بدان معناست که اگر A ، B و C موارد داده های باتری هستند که توسط عملکرد اولیه سازنده شده اند ، و ممکن است به عملکرد باتری صفر یا بیشتر منتقل شده باشد ، سپس دو دنباله کد زیر باید 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 نیز هست.

عملکرد باتری و عملکرد Combiner با هم باید از قانون اساسی تاشو پیروی کنند. یعنی اگر A و B مورد داده های باتری باشد ، A با عملکرد اولیه سازنده شده است و ممکن است به عملکرد باتری صفر یا بیشتر منتقل شده باشد ، B اولیه نشده است ، و ARG ها لیست آرگومان های ورودی و ویژه است آرگومان برای یک تماس خاص به عملکرد باتری ، سپس دو دنباله کد زیر باید A روی همان مقدار تنظیم کنید:

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

مثال: در هسته افزودنی ، برای یک مقدار ورودی V :

  • بیانیه 1 همان A += V است
  • بیانیه 2 همان B = 0 است
  • بیانیه 3 همان B += V است که همان B = V است
  • بیانیه 4 همان A += B است که همان A += V است

بیانیه های 1 و 4 A به همان مقدار تنظیم می کنند ، بنابراین این هسته از قانون اساسی تاشو پیروی می کند.

مثال: در هسته FindMinandMax ، برای مقدار ورودی V در مختصات x :

  • بیانیه 1 همان A = minmax(A, IndexedVal( V , X )) است
  • بیانیه 2 همان B = INITVAL است
  • بیانیه 3 همان است
    B = minmax(B, IndexedVal(V, X))
    
    که ، زیرا B مقدار اولیه است ، همان است
    B = IndexedVal(V, X)
    
  • بیانیه 4 همان است
    A = minmax(A, B)
    
    که همان است
    A = minmax(A, IndexedVal(V, X))
    

بیانیه های 1 و 4 A به همان مقدار تنظیم می کنند ، بنابراین این هسته از قانون اساسی تاشو پیروی می کند.

فراخوانی هسته کاهش از کد جاوا

برای کاهش هسته به نام هسته تعریف شده در filename .rs ، سه روش در ScriptC_ filename ارائه شده است:

کاتلین

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

جاوا

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

در اینجا چند نمونه از تماس با هسته افزودنی آورده شده است:

کاتلین

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

جاوا

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 برای هر آرگومان ورودی در عملکرد باتری هسته یک آرگومان Allocation ورودی دارد. Renderscript Runtime بررسی می کند تا اطمینان حاصل شود که همه تخصیص ورودی دارای ابعاد یکسانی هستند و نوع Element هر یک از تخصیص ورودی با آرگومان ورودی مربوطه نمونه اولیه عملکرد جمع کننده مطابقت دارد. در صورت عدم موفقیت هر یک از این چک ها ، Renderscript یک استثنا را پرتاب می کند. هسته بیش از هر مختصات در آن ابعاد اجرا می کند.

روش 2 همان روش 1 است به جز اینکه روش 2 یک آرگومان اضافی sc را می گیرد که می تواند برای محدود کردن اجرای هسته به زیر مجموعه ای از مختصات استفاده شود.

روش 3 همان روش 1 است به جز اینکه به جای اینکه ورودی های تخصیصی را در نظر بگیرند ، ورودی های آرایه جاوا را می گیرد. این یک راحتی است که شما را از نوشتن کد برای ایجاد صریح تخصیص و کپی کردن داده ها از یک آرایه جاوا نجات می دهد. با این حال ، استفاده از روش 3 به جای روش 1 عملکرد کد را افزایش نمی دهد . برای هر آرایه ورودی ، روش 3 یک تخصیص موقت 1 بعدی با نوع Element مناسب و setAutoPadding(boolean) ایجاد می کند و آرایه را به تخصیص کپی می کند که گویی با روش copyFrom() Allocation . سپس با استفاده از آن تخصیص موقت ، روش 1 را می خواند.

توجه: اگر برنامه شما چندین تماس هسته را با همان آرایه یا با آرایه های مختلف با ابعاد مشابه و نوع عنصر انجام می دهد ، ممکن است به جای استفاده از روش 3 ، عملکرد خود را با ایجاد صریح ، جمع آوری و استفاده مجدد از تخصیص خود بهبود بخشید.

javafuturetype ، نوع بازگشت روش های کاهش منعکس شده ، یک کلاس تو در تو استاتیک منعکس شده در کلاس ScriptC_ filename است. این نشان دهنده نتیجه آینده اجرای هسته کاهش یافته است. برای به دست آوردن نتیجه واقعی اجرای ، با روش get() آن کلاس تماس بگیرید ، که مقدار نوع javaresulttype را برمی گرداند. get() همزمان است.

کاتلین

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

جاوا

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

JavaresultType از نتیجه عملکرد عملکرد OutConverter تعیین می شود. مگر در مواردی که ResultType یک نوع بدون امضا (مقیاس ، بردار یا آرایه) باشد ، JavaresultType نوع جاوا به طور مستقیم مربوطه است. اگر ResultType یک نوع امضا نشده است و یک نوع بزرگتر از جاوا امضا شده است ، پس JavaresultType نوع دیگری از جاوا امضا شده است. در غیر این صورت ، این نوع جاوا به طور مستقیم مربوطه است. به عنوان مثال:

  • اگر نتیجه int ، int2 یا int[15] باشد ، سپس javaresulttype int ، Int2 یا int[] است. تمام مقادیر ResultType را می توان توسط JavaresultType نشان داد.
  • اگر ResultType uint ، uint2 یا uint[15] باشد ، سپس javaresulttype long ، Long2 یا long[] . تمام مقادیر ResultType را می توان توسط JavaresultType نشان داد.
  • اگر ResultType ulong ، ulong2 یا ulong[15] باشد ، JavaresultType long ، Long2 یا long[] . مقادیر خاصی از ResultType وجود دارد که نمی توانند توسط JavaresultType نشان داده شوند.

javafuturetype نوع نتیجه آینده است که مطابق با نتیجه عملکرد عملکرد OutConverter است .

  • اگر ResultType یک نوع آرایه نباشد ، javafuturetype result_ resultType است.
  • اگر ResultType آرایه ای از تعداد طول با اعضای Type Type باشد ، سپس JavafutureType resultArray Count _ memberType است.

به عنوان مثال:

کاتلین

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

جاوا

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 نمی تواند تمام مقادیر نوع نتیجه را نشان دهد ، و یک هسته کاهش یک مقدار غیر قابل ارائه تولید می کند ، سپس javaFutureType .get() یک استثنا را پرتاب می کند.

روش 3 و devecsiinxtype

devecsiinxtype نوع جاوا است که مربوط به inxtype آرگومان مربوط به عملکرد باتری است. مگر اینکه Inxtype یک نوع بدون امضا یا نوع بردار باشد ، Devecsiinxtype نوع جاوا مستقیماً مربوطه است. اگر Inxtype یک نوع مقیاس بدون امضا باشد ، پس از آن Devecsiinxtype نوع جاوا است که مستقیماً با نوع مقیاس امضا شده با همان اندازه مطابقت دارد. اگر inxtype یک نوع بردار امضا شده است ، سپس devecsiinxtype نوع جاوا است که مستقیماً مطابق با نوع جزء بردار است. اگر inxtype یک نوع بردار بدون امضا است ، سپس devecsiinxtype نوع جاوا است که به طور مستقیم با نوع مقیاس امضا شده با اندازه یک نوع جزء بردار مطابقت دارد. به عنوان مثال:

  • اگر inxtype int باشد ، پس از آن devecsiinxtype int است.
  • اگر inxtype int2 باشد ، پس از آن devecsiinxtype int است. آرایه یک نمایش مسطح است: دو برابر عناصر مقیاس پذیر است زیرا تخصیص دارای عناصر بردار 2 جزء است. این به همان روشی است که روشهای copyFrom() برای Allocation کار می کنند.
  • اگر inxtype uint باشد ، سپس دستگاه های inxtype int است. یک مقدار امضا شده در آرایه جاوا به عنوان یک مقدار امضا نشده از همان بیت پاتر در تخصیص تعبیر می شود. این به همان روشی است که روشهای copyFrom() برای Allocation کار می کنند.
  • اگر inxtype uint2 باشد ، سپس دستگاه های inxtype int است. این ترکیبی از نحوه برخورد با int2 و uint است: آرایه یک نمایش مسطح است و مقادیر امضا شده آرایه جاوا به عنوان مقادیر عناصر امضا نشده 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 ، RenderscriptIntrinsic و Hello Compute بیشتر استفاده از API های تحت پوشش در این صفحه را نشان می دهد.

،

Renderscript چارچوبی برای انجام کارهای محاسباتی فشرده با عملکرد بالا در اندروید است. Renderscript در درجه اول برای استفاده با محاسبات داده موازی قرار دارد ، اگرچه بارهای سریال نیز می تواند سود ببرد. Renderscript Runtime کار را در سراسر پردازنده های موجود در یک دستگاه مانند CPU های چند هسته ای و GPU موازی می کند. این به شما امکان می دهد به جای برنامه ریزی کار ، روی بیان الگوریتم ها تمرکز کنید. Renderscript به ویژه برای برنامه های انجام پردازش تصویر ، عکاسی محاسباتی یا دید رایانه مفید است.

برای شروع با Renderscript ، دو مفهوم اصلی وجود دارد که باید آنها را درک کنید:

  • این زبان خود یک زبان مشتق از C99 برای نوشتن کد محاسبات با کارایی بالا است. نوشتن هسته Renderscript نحوه استفاده از آن برای نوشتن هسته های محاسباتی را شرح می دهد.
  • API کنترل برای مدیریت طول عمر منابع Renderscript و کنترل اجرای هسته استفاده می شود. این در سه زبان مختلف موجود است: جاوا ، C ++ در Android NDK و خود زبان هسته C99 مشتق شده. با استفاده از RenderScript از کد جاوا و تک منبع Renderscript به ترتیب گزینه های اول و سوم را شرح دهید.

نوشتن هسته Renderscript

یک هسته Renderscript به طور معمول در یک پرونده .rs در دایرکتوری <project_root>/src/rs ساکن است. هر پرونده .rs اسکریپت نامیده می شود. هر اسکریپت شامل مجموعه ای از هسته ها ، توابع و متغیرهای خاص خود است. یک اسکریپت می تواند حاوی:

  • اعلامیه Pragma ( #pragma version(1) ) که نسخه زبان هسته Renderscript را که در این اسکریپت استفاده می شود ، اعلام می کند. در حال حاضر ، 1 تنها مقدار معتبر است.
  • اعلامیه pragma ( #pragma rs java_package_name(com.example.app) ) که نام بسته کلاس های جاوا را که از این اسکریپت منعکس شده است ، اعلام می کند. توجه داشته باشید که پرونده .rs شما باید بخشی از بسته برنامه شما باشد و نه در یک پروژه کتابخانه.
  • عملکردهای صفر یا بیشتر قابل ذکر است . یک عملکرد قابل توصیف یک تابع Renderscript تک رشته ای است که می توانید با آرگومان های دلخواه از کد جاوا خود تماس بگیرید. اینها اغلب برای تنظیم اولیه یا محاسبات سریال در یک خط لوله پردازش بزرگتر مفید هستند.
  • گلوبال های اسکریپت صفر یا بیشتر. یک اسکریپت جهانی شبیه به یک متغیر جهانی در C است. شما می توانید از کد جاوا به Globals Script دسترسی پیدا کنید ، و اینها اغلب برای پارامترهای منتقل شده به هسته های Renderscript استفاده می شوند. گلوبال های اسکریپت در اینجا با جزئیات بیشتری توضیح داده شده است.

  • هسته های محاسبه صفر یا بیشتر. هسته محاسباتی یک تابع یا مجموعه ای از توابع است که می توانید زمان اجرا Renderscript را هدایت کنید تا به طور موازی در مجموعه داده ها اجرا شود. دو نوع هسته محاسباتی وجود دارد: نقشه برداری هسته (که به آن هسته های مناسب نیز گفته می شود) و هسته های کاهش .

    هسته نقشه برداری یک عملکرد موازی است که بر روی مجموعه ای از Allocations با ابعاد مشابه عمل می کند. به طور پیش فرض ، آن را برای هر مختصات در آن ابعاد یک بار اجرا می کند. به طور معمول (اما نه منحصراً) برای تبدیل مجموعه ای از Allocations ورودی به یک Allocation خروجی یک Element در یک زمان استفاده می شود.

    • در اینجا نمونه ای از یک هسته نقشه برداری ساده آورده شده است:

      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 ورودی منتقل شده به پرتاب هسته پر می شود. استدلال های x و y در زیر مورد بحث قرار گرفته است. مقدار برگردانده شده از هسته به طور خودکار در Allocation خروجی به مکان مناسب نوشته می شود. به طور پیش فرض ، این هسته در کل Allocation ورودی خود اجرا می شود ، با یک اجرای عملکرد هسته در هر Element در Allocation .

      یک هسته نقشه برداری ممکن است یک یا چند Allocations ورودی ، یک Allocation خروجی واحد یا هر دو داشته باشد. Renderscript Runtime بررسی می کند تا اطمینان حاصل شود که کلیه تخصیص ورودی و خروجی ابعاد یکسانی دارند و انواع Element تخصیص ورودی و خروجی با نمونه اولیه هسته مطابقت دارند. در صورت عدم موفقیت هر یک از این چک ها ، Renderscript یک استثنا را پرتاب می کند.

      توجه: قبل از Android 6.0 (API سطح 23) ، یک هسته نقشه برداری ممکن است بیش از یک Allocation ورودی نداشته باشد.

      اگر به Allocations ورودی یا خروجی بیشتری نسبت به هسته نیاز دارید ، آن اشیاء باید به Globals اسکریپت rs_allocation وصل شوند و از یک هسته یا عملکرد قابل دعوت از طریق rsGetElementAt_ type () یا rsSetElementAt_ type () دسترسی پیدا کنند.

      توجه: RS_KERNEL یک کلان است که به طور خودکار توسط Renderscript برای راحتی شما تعریف می شود:

      #define RS_KERNEL __attribute__((kernel))
      

    هسته کاهش ، خانواده ای از کارکردها است که بر اساس مجموعه ای از Allocations ورودی با همان ابعاد کار می کند. به طور پیش فرض ، عملکرد باتری آن یک بار برای هر مختصات در آن ابعاد اجرا می شود. به طور معمول (اما نه به طور انحصاری) برای "کاهش" مجموعه ای از 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 نیاز دارد. بسته به آنچه شما می خواهید هسته انجام دهد ، ممکن است عملکردهای دیگری نیز داشته باشد.

      یک عملکرد باتری از هسته هسته باید void برگردد و حداقل باید دو استدلال داشته باشد. accum اول (در این مثال) یک اشاره گر به یک مورد داده باتری است و val (در این مثال) بر اساس Allocation ورودی منتقل شده به پرتاب هسته ، به طور خودکار پر می شود. مورد داده باتری توسط Renderscript Runtime ایجاد می شود. به طور پیش فرض ، آن را به صفر آغاز می شود. به طور پیش فرض ، این هسته در کل Allocation ورودی خود اجرا می شود ، با یک اجرای عملکرد باتری در هر Element در Allocation . به طور پیش فرض ، مقدار نهایی مورد داده باتری به عنوان نتیجه کاهش درمان می شود و به جاوا بازگردانده می شود. Renderscript Runtime بررسی می کند تا اطمینان حاصل شود که نوع Element تخصیص ورودی با نمونه اولیه عملکرد باتری مطابقت دارد. اگر مطابقت نداشته باشد ، Renderscript یک استثنا را پرتاب می کند.

      یک هسته کاهش دارای یک یا چند Allocations ورودی است اما هیچ Allocations خروجی ندارد.

      هسته های کاهش در اینجا با جزئیات بیشتری توضیح داده شده است.

      هسته های کاهش در Android 7.0 (API سطح 24) و بعد از آن پشتیبانی می شوند.

    یک تابع هسته نقشه برداری یا یک تابع باتری کاهش دهنده هسته ممکن است با استفاده از آرگومان های ویژه x ، y و z ، به مختصات اجرای فعلی دسترسی پیدا کند ، که باید از نوع int یا uint32_t باشد. این آرگومان ها اختیاری هستند.

    یک عملکرد هسته نقشه برداری یا عملکرد باتری از ترکیب هسته هسته نیز ممکن است context آرگومان ویژه اختیاری از نوع RS_KERNEL_CONTEXT را در نظر بگیرد. توسط خانواده ای از API های زمان اجرا که برای پرس و جو خاصیت خاص اجرای فعلی استفاده می شود - به عنوان مثال RSGetDimx مورد نیاز است. (استدلال context در Android 6.0 (سطح API 23) و بعد از آن موجود است.)

  • یک تابع اختیاری init() . عملکرد init() نوع خاصی از عملکرد قابل توصیف است که هنگام اولین بار اسکریپت ، Renderscript اجرا می شود. این اجازه می دهد تا برخی از محاسبات به طور خودکار در ایجاد اسکریپت اتفاق بیفتد.
  • گلوبال ها و توابع اسکریپت استاتیک صفر یا بیشتر. یک اسکریپت استاتیک جهانی معادل یک اسکریپت جهانی است به جز اینکه از کد جاوا قابل دسترسی نیست. یک عملکرد استاتیک یک عملکرد C استاندارد است که از هر هسته یا عملکرد قابل ذکر در اسکریپت می توان آن را فراخوانی کرد اما در معرض API جاوا قرار نمی گیرد. اگر یک اسکریپت جهانی یا عملکرد نیازی به دسترسی به کد جاوا نداشته باشد ، بسیار توصیه می شود که static اعلام شود.

تنظیم دقت نقطه شناور

می توانید سطح مورد نیاز دقت نقطه شناور را در یک اسکریپت کنترل کنید. این مفید است اگر استاندارد کامل IEEE 754-2008 (به طور پیش فرض استفاده شود) لازم نیست. pragmas زیر می تواند سطح متفاوتی از دقت نقطه شناور را تعیین کند:

  • #pragma rs_fp_full (پیش فرض اگر هیچ چیز مشخص نشده باشد): برای برنامه هایی که نیاز به دقت نقطه شناور دارند همانطور که توسط استاندارد IEEE 754-2008 بیان شده است.
  • #pragma rs_fp_relaxed : برای برنامه هایی که به انطباق دقیق IEEE 754-2008 احتیاج ندارند و می توانند دقت کمتری را تحمل کنند. این حالت Flush-to-Zero را برای deNorms و Round-Towards-Zero امکان پذیر می کند.
  • #pragma rs_fp_imprecise : برای برنامه هایی که نیازهای دقیق دقیق ندارند. این حالت همه چیز را در rs_fp_relaxed همراه با موارد زیر امکان پذیر می کند:
    • عملیات حاصل از -0.0 می تواند به جای آن +0.0 برگردد.
    • عملیات در INF و NAN تعریف نشده است.

بیشتر برنامه ها می توانند از rs_fp_relaxed بدون هیچ گونه عوارض جانبی استفاده کنند. این ممکن است به دلیل بهینه سازی های اضافی که فقط با دقت آرام (مانند دستورالعمل های CPU SIMD) در دسترس است ، در برخی از معماری ها بسیار مفید باشد.

دسترسی به API های Renderscript از جاوا

هنگام تهیه یک برنامه Android که از Renderscript استفاده می کند ، می توانید از یکی از دو روش به API آن از جاوا دسترسی پیدا کنید:

  • android.renderscript - API در این بسته کلاس در دستگاه هایی که Android 3.0 (API سطح 11) و بالاتر دارند در دسترس هستند.
  • android.support.v8.renderscript - API در این بسته از طریق یک کتابخانه پشتیبانی در دسترس است ، که به شما امکان می دهد از آنها در دستگاه هایی که Android 2.3 (API سطح 9) و بالاتر دارند استفاده کنید.

در اینجا تجارت وجود دارد:

  • اگر از API های کتابخانه پشتیبانی استفاده می کنید ، بخش Renderscript برنامه شما با دستگاه هایی که Android 2.3 (API سطح 9) را اجرا می کنند ، سازگار خواهد بود ، صرف نظر از اینکه ویژگی های شما استفاده می کنید. این به برنامه شما اجازه می دهد تا روی دستگاه های بیشتری کار کند تا اینکه از API های بومی ( android.renderscript ) استفاده کنید.
  • برخی از ویژگی های Renderscript از طریق API های کتابخانه پشتیبانی در دسترس نیست.
  • اگر از API های کتابخانه پشتیبانی استفاده می کنید ، APK های بزرگتر (احتمالاً به طور قابل توجهی) از API های بومی ( android.renderscript ) استفاده می کنید.

با استفاده از API های کتابخانه پشتیبانی Renderscript

به منظور استفاده از API های Renderscript Renderscript ، باید محیط توسعه خود را پیکربندی کنید تا بتوانید به آنها دسترسی پیدا کنید. ابزارهای SDK Android زیر برای استفاده از این API ها مورد نیاز است:

  • Android SDK Tools Revision 22.2 یا بالاتر
  • Android SDK Build-Tools Revision 18.1.0 یا بالاتر

توجه داشته باشید که از Android SDK Build-Tools 24.0.0 ، Android 2.2 (API سطح 8) دیگر پشتیبانی نمی شود.

می توانید نسخه نصب شده این ابزارها را در Android SDK Manager بررسی و به روز کنید.

برای استفاده از API های Renderscript کتابخانه پشتیبانی:

  1. اطمینان حاصل کنید که نسخه SDK Android مورد نیاز را نصب کرده اید.
  2. تنظیمات فرآیند ساخت Android را به روز کنید تا تنظیمات Renderscript را شامل شود:
    • فایل build.gradle را در پوشه برنامه ماژول برنامه خود باز کنید.
    • تنظیمات Renderscript زیر را به پرونده اضافه کنید:

      شیار

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

      کاتلین

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

      تنظیمات ذکر شده در بالا رفتار خاص را در فرآیند ساخت اندروید کنترل می کند:

      • renderscriptTargetApi - نسخه Bytecode را که باید تولید شود مشخص می کند. توصیه می کنیم این مقدار را بر روی پایین ترین سطح API تنظیم کنید که بتوانید تمام عملکردهای مورد استفاده خود را ارائه دهید و renderscriptSupportModeEnabled را در true تنظیم کنید. مقادیر معتبر برای این تنظیم هر مقدار عدد صحیح از 11 تا سطح API اخیراً منتشر شده است. اگر حداقل نسخه SDK شما که در برنامه برنامه شما مشخص شده است ، به یک مقدار متفاوت تنظیم شده است ، این مقدار نادیده گرفته می شود و از مقدار هدف در فایل ساخت برای تنظیم حداقل نسخه SDK استفاده می شود.
      • renderscriptSupportModeEnabled - مشخص می کند که اگر دستگاهی که روی آن کار می کند ، باید از نسخه هدف پشتیبانی کند ، باید به نسخه سازگار برگردد.
  3. در کلاسهای برنامه خود که از Renderscript استفاده می کنند ، واردات کلاسهای کتابخانه پشتیبانی را اضافه کنید:

    کاتلین

    import android.support.v8.renderscript.*
    

    جاوا

    import android.support.v8.renderscript.*;
    

با استفاده از Renderscript از کد جاوا یا کوتلین

استفاده از Renderscript از Java یا Kotlin Code به کلاسهای API واقع در android.renderscript یا android.support.v8.renderscript متکی است. بیشتر برنامه ها از همان الگوی استفاده اساسی پیروی می کنند:

  1. زمینه Renderscript را اولیه کنید. متن RenderScript ، ایجاد شده با create(Context) ، تضمین می کند که می توان از Renderscript استفاده کرد و یک شیء برای کنترل طول عمر همه اشیاء Renderscript بعدی فراهم می کند. شما باید ایجاد زمینه را به عنوان یک عملیات بالقوه طولانی مدت در نظر بگیرید ، زیرا ممکن است منابع سخت افزاری مختلف را ایجاد کند. در صورت امکان نباید در مسیر بحرانی یک برنامه باشد. به طور معمول ، یک برنامه فقط یک متن Renderscript را در یک زمان خواهد داشت.
  2. حداقل یک Allocation ایجاد کنید تا به یک فیلمنامه منتقل شود. Allocation یک شیء Renderscript است که مقدار ثابت داده را ذخیره می کند. هسته های موجود در اسکریپت ها اشیاء Allocation را به عنوان ورودی و خروجی خود می گیرند ، و به اشیاء Allocation می توان با استفاده از rsGetElementAt_ type () و rsSetElementAt_ type () در هنگام اتصال به عنوان گلوبال های اسکریپت ، در هسته ها دسترسی پیدا کرد. اشیاء Allocation اجازه می دهند آرایه ها از کد جاوا به کد کد و برعکس منتقل شوند. اشیاء Allocation به طور معمول با استفاده از createTyped() یا createFromBitmap() ایجاد می شوند.
  3. هر اسکریپت لازم را ایجاد کنید. هنگام استفاده از Renderscript دو ​​نوع اسکریپت در دسترس شما است:
    • Scriptc : این اسکریپت های تعریف شده توسط کاربر است که در نوشتن یک هسته Renderscript در بالا توضیح داده شده است. هر اسکریپت دارای یک کلاس جاوا است که توسط کامپایلر Renderscript منعکس شده است تا دسترسی به اسکریپت از کد جاوا را آسان کند. این کلاس دارای نام ScriptC_ filename است. به عنوان مثال ، اگر هسته نقشه برداری فوق در invert.rs قرار داشته باشد و یک متن Renderscript قبلاً در mRenderScript قرار داشته باشد ، کد جاوا یا کوتلین برای فوری اسکریپت خواهد بود:

      کاتلین

      val invert = ScriptC_invert(renderScript)
      

      جاوا

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic : اینها هسته های ارائه شده در نسخه های متداول برای عملیات مشترک ، مانند Blur Gaussian ، Convolution و ترکیب تصویر هستند. برای اطلاعات بیشتر ، به زیر کلاسهای ScriptIntrinsic مراجعه کنید.
  4. تخصیص جمع آوری با داده ها. به جز تخصیص های ایجاد شده با createFromBitmap() ، در هنگام ایجاد اولین بار ، یک تخصیص با داده های خالی جمع می شود. برای جمع آوری تخصیص ، از یکی از روشهای "کپی" در Allocation استفاده کنید. روشهای "کپی" همزمان هستند.
  5. هرگونه گلوبال های اسکریپت لازم را تنظیم کنید. ممکن است گلوبال ها را با استفاده از روش ها در همان کلاس ScriptC_ filename به نام set_ globalname تنظیم کنید. به عنوان مثال ، به منظور تنظیم یک متغیر int به نام threshold ، از روش جاوا set_threshold(int) استفاده کنید. و به منظور تنظیم متغیر rs_allocation به نام lookup ، از روش Java set_lookup(Allocation) استفاده کنید. روشهای set ناهمزمان هستند.
  6. هسته های مناسب و عملکردهای قابل ذکر را راه اندازی کنید.

    روشهای راه اندازی یک هسته معین در همان کلاس ScriptC_ filename با روش هایی به نام forEach_ mappingKernelName () یا reduce_ reductionKernelName () منعکس می شوند. این پرتاب ها ناهمزمان هستند. بسته به استدلال به هسته ، این روش یک یا چند تخصیص می گیرد که همه آنها باید ابعاد یکسانی داشته باشند. به طور پیش فرض ، یک هسته در هر مختصات در آن ابعاد اجرا می شود. برای اجرای یک هسته بر روی زیر مجموعه ای از این مختصات ، یک Script.LaunchOptions مناسب را منتقل کنید. LaunchOptions به عنوان آخرین استدلال برای forEach یا reduce روش.

    توابع قابل دعوت را با استفاده از روش های invoke_ functionName منعکس شده در همان کلاس ScriptC_ filename . این پرتاب ها ناهمزمان هستند.

  7. داده ها را از اشیاء Allocation و اشیاء javafuturetype بازیابی کنید. برای دسترسی به داده ها از Allocation از کد جاوا ، باید با استفاده از یکی از روشهای "کپی" در Allocation ، آن داده ها را به جاوا کپی کنید. برای به دست آوردن نتیجه هسته کاهش ، شما باید از روش javaFutureType .get() استفاده کنید. روشهای "کپی" و get() همزمان هستند.
  8. زمینه Renderscript را پاره کنید. شما می توانید متن Renderscript را با destroy() یا با اجازه دادن به شیء متن Renderscript که زباله جمع آوری شده است ، از بین ببرید. این امر باعث می شود هرگونه استفاده بیشتر از هر شیء متعلق به آن زمینه ، یک استثنا را پرتاب کند.

مدل اجرای ناهمزمان

forEach منعکس شده ، invoke ، reduce و روشهای set ناهمزمان هستند - هر یک ممکن است قبل از اتمام عمل درخواست شده به جاوا برگردند. با این حال ، اقدامات فردی به ترتیب راه اندازی آنها سریال می شوند.

کلاس Allocation روشهای "کپی" را برای کپی کردن داده ها به و از تخصیص ارائه می دهد. یک روش "کپی" همزمان است و با توجه به هر یک از اقدامات ناهمزمان در بالا که همان تخصیص را لمس می کند ، سریال می شود.

کلاسهای منعکس شده JavafutureType یک روش get() برای به دست آوردن نتیجه کاهش ارائه می دهند. get() همزمان است و با توجه به کاهش (که ناهمزمان است) سریال می شود.

نسخه تک منبع

Android 7.0 (API سطح 24) یک ویژگی برنامه نویسی جدید به نام Single-Source Renderscript را معرفی می کند ، که در آن هسته ها از فیلمنامه ای که در آن تعریف شده اند ، به جای جاوا راه اندازی می شوند. این رویکرد در حال حاضر محدود به نقشه برداری هسته است ، که به سادگی در این بخش به عنوان "هسته" گفته می شود. این ویژگی جدید همچنین از ایجاد تخصیص نوع rs_allocation از داخل اسکریپت پشتیبانی می کند. اکنون می توان یک الگوریتم کامل را صرفاً در یک اسکریپت پیاده سازی کرد ، حتی در صورت نیاز به پرتاب چندین هسته. سود دو برابر است: کد قابل خواندن تر ، زیرا اجرای الگوریتم را به یک زبان حفظ می کند. و کد بالقوه سریعتر ، به دلیل انتقال کمتری بین جاوا و Renderscript در چندین پرتاب هسته.

در Renderscript تک منبع ، شما هسته را می نویسید که در نوشتن یک هسته Renderscript شرح داده شده است. سپس شما یک تابع قابل ذکر را می نویسید که برای راه اندازی آنها rsForEach() تماس می گیرد. این API یک عملکرد هسته را به عنوان اولین پارامتر می گیرد و به دنبال آن تخصیص ورودی و خروجی است. یک API rsForEachWithOptions() یک آرگومان اضافی از نوع rs_script_call_t را می گیرد ، که زیر مجموعه ای از عناصر را از تخصیص ورودی و خروجی برای عملکرد هسته برای پردازش مشخص می کند.

برای شروع محاسبات RenderScript ، شما عملکرد قابل ذکر از جاوا را صدا می کنید. مراحل استفاده از Renderscript را از کد جاوا دنبال کنید. در مرحله راه اندازی هسته های مناسب ، با استفاده از invoke_ function_name () ، با عملکرد قابل استفاده تماس بگیرید ، که کل محاسبات را آغاز می کند ، از جمله راه اندازی هسته.

تخصیص اغلب برای صرفه جویی و عبور از نتایج میانی از یک هسته به دیگری مورد نیاز است. می توانید آنها را با استفاده از rscreatealLocation () ایجاد کنید. یک شکل آسان برای استفاده از آن API rsCreateAllocation_<T><W>(…) است ، جایی که T نوع داده برای یک عنصر است ، و W عرض بردار برای عنصر است. API اندازه ها را در ابعاد x ، y و z به عنوان استدلال می گیرد. برای تخصیص 1D یا 2D ، اندازه برای ابعاد y یا z می تواند حذف شود. به عنوان مثال ، rsCreateAllocation_uchar4(16384) تخصیص 1D از 16384 عنصر ایجاد می کند که هر یک از آنها از نوع uchar4 است.

تخصیص توسط سیستم به طور خودکار مدیریت می شود. لازم نیست صریحاً آنها را آزاد کنید یا آنها را آزاد کنید. با این حال ، می توانید با rsClearObject(rs_allocation* alloc) تماس بگیرید تا نشان دهید دیگر نیازی به alloc دسته به تخصیص اساسی ندارید ، تا سیستم بتواند در اسرع وقت منابع را آزاد کند.

بخش نوشتن یک هسته Renderscript شامل یک هسته مثال است که یک تصویر را معکوس می کند. مثال زیر گسترش می دهد که برای استفاده بیش از یک اثر در یک تصویر ، با استفاده از تک منبع Renderscript. این شامل هسته دیگری ، greyscale است که یک تصویر رنگی را به سیاه و سفید تبدیل می کند. یک process() سپس آن دو هسته را به طور متوالی در یک تصویر ورودی اعمال می کند و یک تصویر خروجی تولید می کند. تخصیص برای ورودی و خروجی به عنوان آرگومان نوع 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);
}

شما می توانید process() را از جاوا یا کوتلین به شرح زیر بنامند:

کاتلین

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)

جاوا

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

This example shows how an algorithm that involves two kernel launches can be implemented completely in the RenderScript language itself. Without Single-Source RenderScript, you would have to launch both kernels from the Java code, separating kernel launches from kernel definitions and making it harder to understand the whole algorithm. Not only is the Single-Source RenderScript code easier to read, it also eliminates the transitioning between Java and the script across kernel launches. Some iterative algorithms may launch kernels hundreds of times, making the overhead of such transitioning considerable.

Script Globals

A script global is an ordinary non- static global variable in a script ( .rs ) file. For a script global named var defined in the file filename .rs , there will be a method get_ var reflected in the class ScriptC_ filename . Unless the global is const , there will also be a method set_ var .

A given script global has two separate values -- a Java value and a script value. These values behave as follows:

  • If var has a static initializer in the script, it specifies the initial value of var in both Java and the script. Otherwise, that initial value is zero.
  • Accesses to var within the script read and write its script value.
  • The get_ var method reads the Java value.
  • The set_ var method (if it exists) writes the Java value immediately, and writes the script value asynchronously .

NOTE: This means that except for any static initializer in the script, values written to a global from within a script are not visible to Java.

Reduction Kernels in Depth

Reduction is the process of combining a collection of data into a single value. This is a useful primitive in parallel programming, with applications such as the following:

  • computing the sum or product over all the data
  • computing logical operations ( and , or , xor ) over all the data
  • finding the minimum or maximum value within the data
  • searching for a specific value or for the coordinate of a specific value within the data

In Android 7.0 (API level 24) and later, RenderScript supports reduction kernels to allow efficient user-written reduction algorithms. You may launch reduction kernels on inputs with 1, 2, or 3 dimensions.

An example above shows a simple addint reduction kernel. Here is a more complicated findMinAndMax reduction kernel that finds the locations of the minimum and maximum long values in a 1-dimensional Allocation :

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

NOTE: There are more example reduction kernels here .

In order to run a reduction kernel, the RenderScript runtime creates one or more variables called accumulator data items to hold the state of the reduction process. The RenderScript runtime picks the number of accumulator data items in such a way as to maximize performance. The type of the accumulator data items ( accumType ) is determined by the kernel's accumulator function -- the first argument to that function is a pointer to an accumulator data item. By default, every accumulator data item is initialized to zero (as if by memset ); however, you may write an initializer function to do something different.

Example: In the addint kernel, the accumulator data items (of type int ) are used to add up input values. There is no initializer function, so each accumulator data item is initialized to zero.

Example: In the findMinAndMax kernel, the accumulator data items (of type MinAndMax ) are used to keep track of the minimum and maximum values found so far. There is an initializer function to set these to LONG_MAX and LONG_MIN , respectively; and to set the locations of these values to -1, indicating that the values are not actually present in the (empty) portion of the input that has been processed.

RenderScript calls your accumulator function once for every coordinate in the input(s). Typically, your function should update the accumulator data item in some way according to the input.

Example: In the addint kernel, the accumulator function adds the value of an input Element to the accumulator data item.

Example: In the findMinAndMax kernel, the accumulator function checks to see whether the value of an input Element is less than or equal to the minimum value recorded in the accumulator data item and/or greater than or equal to the maximum value recorded in the accumulator data item, and updates the accumulator data item accordingly.

After the accumulator function has been called once for every coordinate in the input(s), RenderScript must combine the accumulator data items together into a single accumulator data item. You may write a combiner function to do this. If the accumulator function has a single input and no special arguments , then you do not need to write a combiner function; RenderScript will use the accumulator function to combine the accumulator data items. (You may still write a combiner function if this default behavior is not what you want.)

Example: In the addint kernel, there is no combiner function, so the accumulator function will be used. This is the correct behavior, because if we split a collection of values into two pieces, and we add up the values in those two pieces separately, adding up those two sums is the same as adding up the entire collection.

Example: In the findMinAndMax kernel, the combiner function checks to see whether the minimum value recorded in the "source" accumulator data item *val is less than the minimum value recorded in the "destination" accumulator data item *accum , and updates *accum بر این اساس. It does similar work for the maximum value. This updates *accum to the state it would have had if all of the input values had been accumulated into *accum rather than some into *accum and some into *val .

After all of the accumulator data items have been combined, RenderScript determines the result of the reduction to return to Java. You may write an outconverter function to do this. You do not need to write an outconverter function if you want the final value of the combined accumulator data items to be the result of the reduction.

Example: In the addint kernel, there is no outconverter function. The final value of the combined data items is the sum of all Elements of the input, which is the value we want to return.

Example: In the findMinAndMax kernel, the outconverter function initializes an int2 result value to hold the locations of the minimum and maximum values resulting from the combination of all of the accumulator data items.

Writing a reduction kernel

#pragma rs reduce defines a reduction kernel by specifying its name and the names and roles of the functions that make up the kernel. All such functions must be static . A reduction kernel always requires an accumulator function; you can omit some or all of the other functions, depending on what you want the kernel to do.

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

The meaning of the items in the #pragma is as follows:

  • reduce( kernelName ) (mandatory): Specifies that a reduction kernel is being defined. A reflected Java method reduce_ kernelName will launch the kernel.
  • initializer( initializerName ) (optional): Specifies the name of the initializer function for this reduction kernel. When you launch the kernel, RenderScript calls this function once for each accumulator data item . The function must be defined like this:

    static void initializerName(accumType *accum) { … }

    accum is a pointer to an accumulator data item for this function to initialize.

    If you do not provide an initializer function, RenderScript initializes every accumulator data item to zero (as if by memset ), behaving as if there were an initializer function that looks like this:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator( accumulatorName ) (mandatory): Specifies the name of the accumulator function for this reduction kernel. When you launch the kernel, RenderScript calls this function once for every coordinate in the input(s), to update an accumulator data item in some way according to the input(s). The function must be defined like this:

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

    accum is a pointer to an accumulator data item for this function to modify. in1 through in N are one or more arguments that are automatically filled in based on the inputs passed to the kernel launch, one argument per input. The accumulator function may optionally take any of the special arguments .

    An example kernel with multiple inputs is dotProduct .

  • combiner( combinerName )

    (optional): Specifies the name of the combiner function for this reduction kernel. After RenderScript calls the accumulator function once for every coordinate in the input(s), it calls this function as many times as necessary to combine all accumulator data items into a single accumulator data item. The function must be defined like this:

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

    accum is a pointer to a "destination" accumulator data item for this function to modify. other is a pointer to a "source" accumulator data item for this function to "combine" into *accum .

    NOTE: It is possible that *accum , *other , or both have been initialized but have never been passed to the accumulator function; that is, one or both have never been updated according to any input data. For example, in the findMinAndMax kernel, the combiner function fMMCombiner explicitly checks for idx < 0 because that indicates such an accumulator data item, whose value is INITVAL .

    If you do not provide a combiner function, RenderScript uses the accumulator function in its place, behaving as if there were a combiner function that looks like this:

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

    A combiner function is mandatory if the kernel has more than one input, if the input data type is not the same as the accumulator data type, or if the accumulator function takes one or more special arguments .

  • outconverter( outconverterName ) (optional): Specifies the name of the outconverter function for this reduction kernel. After RenderScript combines all of the accumulator data items, it calls this function to determine the result of the reduction to return to Java. The function must be defined like this:

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

    result is a pointer to a result data item (allocated but not initialized by the RenderScript runtime) for this function to initialize with the result of the reduction. resultType is the type of that data item, which need not be the same as accumType . accum is a pointer to the final accumulator data item computed by the combiner function .

    If you do not provide an outconverter function, RenderScript copies the final accumulator data item to the result data item, behaving as if there were an outconverter function that looks like this:

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

    If you want a different result type than the accumulator data type, then the outconverter function is mandatory.

Note that a kernel has input types, an accumulator data item type, and a result type, none of which need to be the same. For example, in the findMinAndMax kernel, the input type long , accumulator data item type MinAndMax , and result type int2 are all different.

What can't you assume?

You must not rely on the number of accumulator data items created by RenderScript for a given kernel launch. There is no guarantee that two launches of the same kernel with the same input(s) will create the same number of accumulator data items.

You must not rely on the order in which RenderScript calls the initializer, accumulator, and combiner functions; it may even call some of them in parallel. There is no guarantee that two launches of the same kernel with the same input will follow the same order. The only guarantee is that only the initializer function will ever see an uninitialized accumulator data item. به عنوان مثال:

  • There is no guarantee that all accumulator data items will be initialized before the accumulator function is called, although it will only be called on an initialized accumulator data item.
  • There is no guarantee on the order in which input Elements are passed to the accumulator function.
  • There is no guarantee that the accumulator function has been called for all input Elements before the combiner function is called.

One consequence of this is that the findMinAndMax kernel is not deterministic: If the input contains more than one occurrence of the same minimum or maximum value, you have no way of knowing which occurrence the kernel will find.

What must you guarantee?

Because the RenderScript system can choose to execute a kernel in many different ways , you must follow certain rules to ensure that your kernel behaves the way you want. If you do not follow these rules, you may get incorrect results, nondeterministic behavior, or runtime errors.

The rules below often say that two accumulator data items must have " the same value" . این به چه معناست؟ That depends on what you want the kernel to do. For a mathematical reduction such as addint , it usually makes sense for "the same" to mean mathematical equality. For a "pick any" search such as findMinAndMax ("find the location of minimum and maximum input values") where there might be more than one occurrence of identical input values, all locations of a given input value must be considered "the same" . You could write a similar kernel to "find the location of leftmost minimum and maximum input values" where (say) a minimum value at location 100 is preferred over an identical minimum value at location 200; for this kernel, "the same" would mean identical location , not merely identical value , and the accumulator and combiner functions would have to be different than those for findMinAndMax .

The initializer function must create an identity value . That is, if I and A are accumulator data items initialized by the initializer function, and I has never been passed to the accumulator function (but A may have been), then
  • combinerName (& A , & I ) must leave A the same
  • combinerName (& I , & A ) must leave I the same as A

Example: In the addint kernel, an accumulator data item is initialized to zero. The combiner function for this kernel performs addition; zero is the identity value for addition.

Example: In the findMinAndMax kernel, an accumulator data item is initialized to INITVAL .

  • fMMCombiner(& A , & I ) leaves A the same, because I is INITVAL .
  • fMMCombiner(& I , & A ) sets I to A , because I is INITVAL .

Therefore, INITVAL is indeed an identity value.

The combiner function must be commutative . That is, if A and B are accumulator data items initialized by the initializer function, and that may have been passed to the accumulator function zero or more times, then combinerName (& A , & B ) must set A to the same value that combinerName (& B , & A ) sets B .

Example: In the addint kernel, the combiner function adds the two accumulator data item values; addition is commutative.

Example: In the findMinAndMax kernel, fMMCombiner(& A , & B ) is the same as A = minmax( A , B ) , and minmax is commutative, so fMMCombiner is also.

The combiner function must be associative . That is, if A , B , and C are accumulator data items initialized by the initializer function, and that may have been passed to the accumulator function zero or more times, then the following two code sequences must set A to the same value :

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

Example: In the addint kernel, the combiner function adds the two accumulator data item values:

  • 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
    

Addition is associative, and so the combiner function is also.

Example: In the findMinAndMax kernel,

fMMCombiner(&A, &B)
همان است که
A = minmax(A, B)
So the two sequences are
  • 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 is associative, and so fMMCombiner is also.

The accumulator function and combiner function together must obey the basic folding rule . That is, if A and B are accumulator data items, A has been initialized by the initializer function and may have been passed to the accumulator function zero or more times, B has not been initialized, and args is the list of input arguments and special arguments for a particular call to the accumulator function, then the following two code sequences must set A to the same value :

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

Example: In the addint kernel, for an input value V :

  • Statement 1 is the same as A += V
  • Statement 2 is the same as B = 0
  • Statement 3 is the same as B += V , which is the same as B = V
  • Statement 4 is the same as A += B , which is the same as A += V

Statements 1 and 4 set A to the same value, and so this kernel obeys the basic folding rule.

Example: In the findMinAndMax kernel, for an input value V at coordinate X :

  • Statement 1 is the same as A = minmax(A, IndexedVal( V , X ))
  • Statement 2 is the same as B = INITVAL
  • Statement 3 is the same as
    B = minmax(B, IndexedVal(V, X))
    
    which, because B is the initial value, is the same as
    B = IndexedVal(V, X)
    
  • Statement 4 is the same as
    A = minmax(A, B)
    
    که همان است
    A = minmax(A, IndexedVal(V, X))
    

Statements 1 and 4 set A to the same value, and so this kernel obeys the basic folding rule.

Calling a reduction kernel from Java code

For a reduction kernel named kernelName defined in the file filename .rs , there are three methods reflected in the class ScriptC_ filename :

کاتلین

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

جاوا

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

Here are some examples of calling the addint kernel:

کاتلین

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

جاوا

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

Method 1 has one input Allocation argument for every input argument in the kernel's accumulator function . The RenderScript runtime checks to ensure that all of the input Allocations have the same dimensions and that the Element type of each of the input Allocations matches that of the corresponding input argument of the accumulator function's prototype. If any of these checks fail, RenderScript throws an exception. The kernel executes over every coordinate in those dimensions.

Method 2 is the same as Method 1 except that Method 2 takes an additional argument sc that can be used to limit the kernel execution to a subset of the coordinates.

Method 3 is the same as Method 1 except that instead of taking Allocation inputs it takes Java array inputs. This is a convenience that saves you from having to write code to explicitly create an Allocation and copy data to it from a Java array. However, using Method 3 instead of Method 1 does not increase the performance of the code . For each input array, Method 3 creates a temporary 1-dimensional Allocation with the appropriate Element type and setAutoPadding(boolean) enabled, and copies the array to the Allocation as if by the appropriate copyFrom() method of Allocation . It then calls Method 1, passing those temporary Allocations.

NOTE: If your application will make multiple kernel calls with the same array, or with different arrays of the same dimensions and Element type, you may improve performance by explicitly creating, populating, and reusing Allocations yourself, instead of by using Method 3.

javaFutureType , the return type of the reflected reduction methods, is a reflected static nested class within the ScriptC_ filename class. It represents the future result of a reduction kernel run. To obtain the actual result of the run, call the get() method of that class, which returns a value of type javaResultType . get() is synchronous .

کاتلین

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

جاوا

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

javaResultType is determined from the resultType of the outconverter function . Unless resultType is an unsigned type (scalar, vector, or array), javaResultType is the directly corresponding Java type. If resultType is an unsigned type and there is a larger Java signed type, then javaResultType is that larger Java signed type; otherwise, it is the directly corresponding Java type. به عنوان مثال:

  • If resultType is int , int2 , or int[15] , then javaResultType is int , Int2 , or int[] . All values of resultType can be represented by javaResultType .
  • If resultType is uint , uint2 , or uint[15] , then javaResultType is long , Long2 , or long[] . All values of resultType can be represented by javaResultType .
  • If resultType is ulong , ulong2 , or ulong[15] , then javaResultType is long , Long2 , or long[] . There are certain values of resultType that cannot be represented by javaResultType .

javaFutureType is the future result type corresponding to the resultType of the outconverter function .

  • If resultType is not an array type, then javaFutureType is result_ resultType .
  • If resultType is an array of length Count with members of type memberType , then javaFutureType is resultArray Count _ memberType .

به عنوان مثال:

کاتلین

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

جاوا

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

If javaResultType is an object type (including an array type), each call to javaFutureType .get() on the same instance will return the same object.

If javaResultType cannot represent all values of type resultType , and a reduction kernel produces an unrepresentible value, then javaFutureType .get() throws an exception.

Method 3 and devecSiInXType

devecSiInXType is the Java type corresponding to the inXType of the corresponding argument of the accumulator function . Unless inXType is an unsigned type or a vector type, devecSiInXType is the directly corresponding Java type. If inXType is an unsigned scalar type, then devecSiInXType is the Java type directly corresponding to the signed scalar type of the same size. If inXType is a signed vector type, then devecSiInXType is the Java type directly corresponding to the vector component type. If inXType is an unsigned vector type, then devecSiInXType is the Java type directly corresponding to the signed scalar type of the same size as the vector component type. به عنوان مثال:

  • If inXType is int , then devecSiInXType is int .
  • If inXType is int2 , then devecSiInXType is int . The array is a flattened representation: It has twice as many scalar Elements as the Allocation has 2-component vector Elements. This is the same way that the copyFrom() methods of Allocation work.
  • If inXType is uint , then deviceSiInXType is int . A signed value in the Java array is interpreted as an unsigned value of the same bitpattern in the Allocation. This is the same way that the copyFrom() methods of Allocation work.
  • If inXType is uint2 , then deviceSiInXType is int . This is a combination of the way int2 and uint are handled: The array is a flattened representation, and Java array signed values are interpreted as RenderScript unsigned Element values.

Note that for Method 3 , input types are handled differently than result types:

  • A script's vector input is flattened on the Java side, whereas a script's vector result is not.
  • A script's unsigned input is represented as a signed input of the same size on the Java side, whereas a script's unsigned result is represented as a widened signed type on the Java side (except in the case of ulong ).

More example reduction kernels

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

Additional code samples

The BasicRenderScript , RenderScriptIntrinsic , and Hello Compute samples further demonstrate the use of the APIs covered on this page.

،

RenderScript is a framework for running computationally intensive tasks at high performance on Android. RenderScript is primarily oriented for use with data-parallel computation, although serial workloads can benefit as well. The RenderScript runtime parallelizes work across processors available on a device, such as multi-core CPUs and GPUs. This allows you to focus on expressing algorithms rather than scheduling work. RenderScript is especially useful for applications performing image processing, computational photography, or computer vision.

To begin with RenderScript, there are two main concepts you should understand:

  • The language itself is a C99-derived language for writing high-performance compute code. Writing a RenderScript Kernel describes how to use it to write compute kernels.
  • The control API is used for managing the lifetime of RenderScript resources and controlling kernel execution. It is available in three different languages: Java, C++ in Android NDK, and the C99-derived kernel language itself. Using RenderScript from Java Code and Single-Source RenderScript describe the first and the third options, respectively.

Writing a RenderScript Kernel

A RenderScript kernel typically resides in a .rs file in the <project_root>/src/rs directory; each .rs file is called a script . Every script contains its own set of kernels, functions, and variables. A script can contain:

  • A pragma declaration ( #pragma version(1) ) that declares the version of the RenderScript kernel language used in this script. Currently, 1 is the only valid value.
  • A pragma declaration ( #pragma rs java_package_name(com.example.app) ) that declares the package name of the Java classes reflected from this script. Note that your .rs file must be part of your application package, and not in a library project.
  • Zero or more invokable functions . An invokable function is a single-threaded RenderScript function that you can call from your Java code with arbitrary arguments. These are often useful for initial setup or serial computations within a larger processing pipeline.
  • Zero or more script globals . A script global is similar to a global variable in C. You can access script globals from Java code, and these are often used for parameter passing to RenderScript kernels. Script globals are explained in more detail here .

  • Zero or more compute kernels . A compute kernel is a function or collection of functions that you can direct the RenderScript runtime to execute in parallel across a collection of data. There are two kinds of compute kernels: mapping kernels (also called foreach kernels) and reduction kernels.

    A mapping kernel is a parallel function that operates on a collection of Allocations of the same dimensions. By default, it executes once for every coordinate in those dimensions. It is typically (but not exclusively) used to transform a collection of input Allocations to an output Allocation one Element at a time.

    • Here is an example of a simple mapping kernel :

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

      In most respects, this is identical to a standard C function. The RS_KERNEL property applied to the function prototype specifies that the function is a RenderScript mapping kernel instead of an invokable function. The in argument is automatically filled in based on the input Allocation passed to the kernel launch. The arguments x and y are discussed below . The value returned from the kernel is automatically written to the appropriate location in the output Allocation . By default, this kernel is run across its entire input Allocation , with one execution of the kernel function per Element in the Allocation .

      A mapping kernel may have one or more input Allocations , a single output Allocation , or both. The RenderScript runtime checks to ensure that all input and output Allocations have the same dimensions, and that the Element types of the input and output Allocations match the kernel's prototype; if either of these checks fails, RenderScript throws an exception.

      NOTE: Before Android 6.0 (API level 23), a mapping kernel may not have more than one input Allocation .

      If you need more input or output Allocations than the kernel has, those objects should be bound to rs_allocation script globals and accessed from a kernel or invokable function via rsGetElementAt_ type () or rsSetElementAt_ type () .

      NOTE: RS_KERNEL is a macro defined automatically by RenderScript for your convenience:

      #define RS_KERNEL __attribute__((kernel))
      

    A reduction kernel is a family of functions that operates on a collection of input Allocations of the same dimensions. By default, its accumulator function executes once for every coordinate in those dimensions. It is typically (but not exclusively) used to "reduce" a collection of input Allocations to a single value.

    • Here is an example of a simple reduction kernel that adds up the Elements of its input:

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

      A reduction kernel consists of one or more user-written functions. #pragma rs reduce is used to define the kernel by specifying its name ( addint , in this example) and the names and roles of the functions that make up the kernel (an accumulator function addintAccum , in this example). All such functions must be static . A reduction kernel always requires an accumulator function; it may also have other functions, depending on what you want the kernel to do.

      A reduction kernel accumulator function must return void and must have at least two arguments. The first argument ( accum , in this example) is a pointer to an accumulator data item and the second ( val , in this example) is automatically filled in based on the input Allocation passed to the kernel launch. The accumulator data item is created by the RenderScript runtime; by default, it is initialized to zero. By default, this kernel is run across its entire input Allocation , with one execution of the accumulator function per Element in the Allocation . By default, the final value of the accumulator data item is treated as the result of the reduction, and is returned to Java. The RenderScript runtime checks to ensure that the Element type of the input Allocation matches the accumulator function's prototype; if it does not match, RenderScript throws an exception.

      A reduction kernel has one or more input Allocations but no output Allocations .

      Reduction kernels are explained in more detail here .

      Reduction kernels are supported in Android 7.0 (API level 24) and later.

    A mapping kernel function or a reduction kernel accumulator function may access the coordinates of the current execution using the special arguments x , y , and z , which must be of type int or uint32_t . These arguments are optional.

    A mapping kernel function or a reduction kernel accumulator function may also take the optional special argument context of type rs_kernel_context . It is needed by a family of runtime APIs that are used to query certain properties of the current execution -- for example, rsGetDimX . (The context argument is available in Android 6.0 (API level 23) and later.)

  • An optional init() function. The init() function is a special type of invokable function that RenderScript runs when the script is first instantiated. This allows for some computation to occur automatically at script creation.
  • Zero or more static script globals and functions . A static script global is equivalent to a script global except that it cannot be accessed from Java code. A static function is a standard C function that can be called from any kernel or invokable function in the script but is not exposed to the Java API. If a script global or function does not need to be accessed from Java code, it is highly recommended that it be declared static .

Setting floating point precision

You can control the required level of floating point precision in a script. This is useful if full IEEE 754-2008 standard (used by default) is not required. The following pragmas can set a different level of floating point precision:

  • #pragma rs_fp_full (default if nothing is specified): For apps that require floating point precision as outlined by the IEEE 754-2008 standard.
  • #pragma rs_fp_relaxed : For apps that don't require strict IEEE 754-2008 compliance and can tolerate less precision. This mode enables flush-to-zero for denorms and round-towards-zero.
  • #pragma rs_fp_imprecise : For apps that don't have stringent precision requirements. This mode enables everything in rs_fp_relaxed along with the following:
    • Operations resulting in -0.0 can return +0.0 instead.
    • Operations on INF and NAN are undefined.

Most applications can use rs_fp_relaxed without any side effects. This may be very beneficial on some architectures due to additional optimizations only available with relaxed precision (such as SIMD CPU instructions).

Accessing RenderScript APIs from Java

When developing an Android application that uses RenderScript, you can access its API from Java in one of two ways:

Here are the tradeoffs:

  • If you use the Support Library APIs, the RenderScript portion of your application will be compatible with devices running Android 2.3 (API level 9) and higher, regardless of which RenderScript features you use. This allows your application to work on more devices than if you use the native ( android.renderscript ) APIs.
  • Certain RenderScript features are not available through the Support Library APIs.
  • If you use the Support Library APIs, you will get (possibly significantly) larger APKs than if you use the native ( android.renderscript ) APIs.

Using the RenderScript Support Library APIs

In order to use the Support Library RenderScript APIs, you must configure your development environment to be able to access them. The following Android SDK tools are required for using these APIs:

  • Android SDK Tools revision 22.2 or higher
  • Android SDK Build-tools revision 18.1.0 or higher

Note that starting from Android SDK Build-tools 24.0.0, Android 2.2 (API level 8) is no longer supported.

You can check and update the installed version of these tools in the Android SDK Manager .

To use the Support Library RenderScript APIs:

  1. Make sure you have the required Android SDK version installed.
  2. Update the settings for the Android build process to include the RenderScript settings:
    • Open the build.gradle file in the app folder of your application module.
    • Add the following RenderScript settings to the file:

      شیار

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

      کاتلین

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

      The settings listed above control specific behavior in the Android build process:

      • renderscriptTargetApi - Specifies the bytecode version to be generated. We recommend you set this value to the lowest API level able to provide all the functionality you are using and set renderscriptSupportModeEnabled to true . Valid values for this setting are any integer value from 11 to the most recently released API level. If your minimum SDK version specified in your application manifest is set to a different value, that value is ignored and the target value in the build file is used to set the minimum SDK version.
      • renderscriptSupportModeEnabled - Specifies that the generated bytecode should fall back to a compatible version if the device it is running on does not support the target version.
  3. In your application classes that use RenderScript, add an import for the Support Library classes:

    کاتلین

    import android.support.v8.renderscript.*
    

    جاوا

    import android.support.v8.renderscript.*;
    

Using RenderScript from Java or Kotlin Code

Using RenderScript from Java or Kotlin code relies on the API classes located in the android.renderscript or the android.support.v8.renderscript package. Most applications follow the same basic usage pattern:

  1. Initialize a RenderScript context. The RenderScript context, created with create(Context) , ensures that RenderScript can be used and provides an object to control the lifetime of all subsequent RenderScript objects. You should consider context creation to be a potentially long-running operation, since it may create resources on different pieces of hardware; it should not be in an application's critical path if at all possible. Typically, an application will have only a single RenderScript context at a time.
  2. Create at least one Allocation to be passed to a script. An Allocation is a RenderScript object that provides storage for a fixed amount of data. Kernels in scripts take Allocation objects as their input and output, and Allocation objects can be accessed in kernels using rsGetElementAt_ type () and rsSetElementAt_ type () when bound as script globals. Allocation objects allow arrays to be passed from Java code to RenderScript code and vice-versa. Allocation objects are typically created using createTyped() or createFromBitmap() .
  3. Create whatever scripts are necessary. There are two types of scripts available to you when using RenderScript:
    • ScriptC : These are the user-defined scripts as described in Writing a RenderScript Kernel above. Every script has a Java class reflected by the RenderScript compiler in order to make it easy to access the script from Java code; this class has the name ScriptC_ filename . For example, if the mapping kernel above were located in invert.rs and a RenderScript context were already located in mRenderScript , the Java or Kotlin code to instantiate the script would be:

      کاتلین

      val invert = ScriptC_invert(renderScript)
      

      جاوا

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic : These are built-in RenderScript kernels for common operations, such as Gaussian blur, convolution, and image blending. For more information, see the subclasses of ScriptIntrinsic .
  4. Populate Allocations with data. Except for Allocations created with createFromBitmap() , an Allocation is populated with empty data when it is first created. To populate an Allocation, use one of the "copy" methods in Allocation . The "copy" methods are synchronous .
  5. Set any necessary script globals . You may set globals using methods in the same ScriptC_ filename class named set_ globalname . For example, in order to set an int variable named threshold , use the Java method set_threshold(int) ; and in order to set an rs_allocation variable named lookup , use the Java method set_lookup(Allocation) . The set methods are asynchronous .
  6. Launch the appropriate kernels and invokable functions.

    Methods to launch a given kernel are reflected in the same ScriptC_ filename class with methods named forEach_ mappingKernelName () or reduce_ reductionKernelName () . These launches are asynchronous . Depending on the arguments to the kernel, the method takes one or more Allocations, all of which must have the same dimensions. By default, a kernel executes over every coordinate in those dimensions; to execute a kernel over a subset of those coordinates, pass an appropriate Script.LaunchOptions as the last argument to the forEach or reduce method.

    Launch invokable functions using the invoke_ functionName methods reflected in the same ScriptC_ filename class. These launches are asynchronous .

  7. Retrieve data from Allocation objects and javaFutureType objects. In order to access data from an Allocation from Java code, you must copy that data back to Java using one of the "copy" methods in Allocation . In order to obtain the result of a reduction kernel, you must use the javaFutureType .get() method. The "copy" and get() methods are synchronous .
  8. Tear down the RenderScript context. You can destroy the RenderScript context with destroy() or by allowing the RenderScript context object to be garbage collected. This causes any further use of any object belonging to that context to throw an exception.

Asynchronous execution model

The reflected forEach , invoke , reduce , and set methods are asynchronous -- each may return to Java before completing the requested action. However, the individual actions are serialized in the order in which they are launched.

The Allocation class provides "copy" methods to copy data to and from Allocations. A "copy" method is synchronous, and is serialized with respect to any of the asynchronous actions above that touch the same Allocation.

The reflected javaFutureType classes provide a get() method to obtain the result of a reduction. get() is synchronous, and is serialized with respect to the reduction (which is asynchronous).

Single-Source RenderScript

Android 7.0 (API level 24) introduces a new programming feature called Single-Source RenderScript , in which kernels are launched from the script where they are defined, rather than from Java. This approach is currently limited to mapping kernels, which are simply referred to as "kernels" in this section for conciseness. This new feature also supports creating allocations of type rs_allocation from inside the script. It is now possible to implement a whole algorithm solely within a script, even if multiple kernel launches are required. The benefit is twofold: more readable code, because it keeps the implementation of an algorithm in one language; and potentially faster code, because of fewer transitions between Java and RenderScript across multiple kernel launches.

In Single-Source RenderScript, you write kernels as described in Writing a RenderScript Kernel . You then write an invokable function that calls rsForEach() to launch them. That API takes a kernel function as the first parameter, followed by input and output allocations. A similar API rsForEachWithOptions() takes an extra argument of type rs_script_call_t , which specifies a subset of the elements from the input and output allocations for the kernel function to process.

To start RenderScript computation, you call the invokable function from Java. Follow the steps in Using RenderScript from Java Code . In the step launch the appropriate kernels , call the invokable function using invoke_ function_name () , which will start the whole computation, including launching kernels.

Allocations are often needed to save and pass intermediate results from one kernel launch to another. You can create them using rsCreateAllocation() . One easy-to-use form of that API is rsCreateAllocation_<T><W>(…) , where T is the data type for an element, and W is the vector width for the element. The API takes the sizes in dimensions X, Y, and Z as arguments. For 1D or 2D allocations, the size for dimension Y or Z can be omitted. For example, rsCreateAllocation_uchar4(16384) creates a 1D allocation of 16384 elements, each of which is of type uchar4 .

Allocations are managed by the system automatically. You do not have to explicitly release or free them. However, you can call rsClearObject(rs_allocation* alloc) to indicate you no longer need the handle alloc to the underlying allocation, so that the system can free up resources as early as possible.

The Writing a RenderScript Kernel section contains an example kernel that inverts an image. The example below expands that to apply more than one effect to an image, using Single-Source RenderScript. It includes another kernel, greyscale , which turns a color image into black-and-white. An invokable function process() then applies those two kernels consecutively to an input image, and produces an output image. Allocations for both the input and the output are passed in as arguments of type 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);
}

You can call the process() function from Java or Kotlin as follows:

کاتلین

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)

جاوا

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

This example shows how an algorithm that involves two kernel launches can be implemented completely in the RenderScript language itself. Without Single-Source RenderScript, you would have to launch both kernels from the Java code, separating kernel launches from kernel definitions and making it harder to understand the whole algorithm. Not only is the Single-Source RenderScript code easier to read, it also eliminates the transitioning between Java and the script across kernel launches. Some iterative algorithms may launch kernels hundreds of times, making the overhead of such transitioning considerable.

Script Globals

A script global is an ordinary non- static global variable in a script ( .rs ) file. For a script global named var defined in the file filename .rs , there will be a method get_ var reflected in the class ScriptC_ filename . Unless the global is const , there will also be a method set_ var .

A given script global has two separate values -- a Java value and a script value. These values behave as follows:

  • If var has a static initializer in the script, it specifies the initial value of var in both Java and the script. Otherwise, that initial value is zero.
  • Accesses to var within the script read and write its script value.
  • The get_ var method reads the Java value.
  • The set_ var method (if it exists) writes the Java value immediately, and writes the script value asynchronously .

NOTE: This means that except for any static initializer in the script, values written to a global from within a script are not visible to Java.

Reduction Kernels in Depth

Reduction is the process of combining a collection of data into a single value. This is a useful primitive in parallel programming, with applications such as the following:

  • computing the sum or product over all the data
  • computing logical operations ( and , or , xor ) over all the data
  • finding the minimum or maximum value within the data
  • searching for a specific value or for the coordinate of a specific value within the data

In Android 7.0 (API level 24) and later, RenderScript supports reduction kernels to allow efficient user-written reduction algorithms. You may launch reduction kernels on inputs with 1, 2, or 3 dimensions.

An example above shows a simple addint reduction kernel. Here is a more complicated findMinAndMax reduction kernel that finds the locations of the minimum and maximum long values in a 1-dimensional Allocation :

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

NOTE: There are more example reduction kernels here .

In order to run a reduction kernel, the RenderScript runtime creates one or more variables called accumulator data items to hold the state of the reduction process. The RenderScript runtime picks the number of accumulator data items in such a way as to maximize performance. The type of the accumulator data items ( accumType ) is determined by the kernel's accumulator function -- the first argument to that function is a pointer to an accumulator data item. By default, every accumulator data item is initialized to zero (as if by memset ); however, you may write an initializer function to do something different.

Example: In the addint kernel, the accumulator data items (of type int ) are used to add up input values. There is no initializer function, so each accumulator data item is initialized to zero.

Example: In the findMinAndMax kernel, the accumulator data items (of type MinAndMax ) are used to keep track of the minimum and maximum values found so far. There is an initializer function to set these to LONG_MAX and LONG_MIN , respectively; and to set the locations of these values to -1, indicating that the values are not actually present in the (empty) portion of the input that has been processed.

RenderScript calls your accumulator function once for every coordinate in the input(s). Typically, your function should update the accumulator data item in some way according to the input.

Example: In the addint kernel, the accumulator function adds the value of an input Element to the accumulator data item.

Example: In the findMinAndMax kernel, the accumulator function checks to see whether the value of an input Element is less than or equal to the minimum value recorded in the accumulator data item and/or greater than or equal to the maximum value recorded in the accumulator data item, and updates the accumulator data item accordingly.

After the accumulator function has been called once for every coordinate in the input(s), RenderScript must combine the accumulator data items together into a single accumulator data item. You may write a combiner function to do this. If the accumulator function has a single input and no special arguments , then you do not need to write a combiner function; RenderScript will use the accumulator function to combine the accumulator data items. (You may still write a combiner function if this default behavior is not what you want.)

Example: In the addint kernel, there is no combiner function, so the accumulator function will be used. This is the correct behavior, because if we split a collection of values into two pieces, and we add up the values in those two pieces separately, adding up those two sums is the same as adding up the entire collection.

Example: In the findMinAndMax kernel, the combiner function checks to see whether the minimum value recorded in the "source" accumulator data item *val is less than the minimum value recorded in the "destination" accumulator data item *accum , and updates *accum بر این اساس. It does similar work for the maximum value. This updates *accum to the state it would have had if all of the input values had been accumulated into *accum rather than some into *accum and some into *val .

After all of the accumulator data items have been combined, RenderScript determines the result of the reduction to return to Java. You may write an outconverter function to do this. You do not need to write an outconverter function if you want the final value of the combined accumulator data items to be the result of the reduction.

Example: In the addint kernel, there is no outconverter function. The final value of the combined data items is the sum of all Elements of the input, which is the value we want to return.

Example: In the findMinAndMax kernel, the outconverter function initializes an int2 result value to hold the locations of the minimum and maximum values resulting from the combination of all of the accumulator data items.

Writing a reduction kernel

#pragma rs reduce defines a reduction kernel by specifying its name and the names and roles of the functions that make up the kernel. All such functions must be static . A reduction kernel always requires an accumulator function; you can omit some or all of the other functions, depending on what you want the kernel to do.

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

The meaning of the items in the #pragma is as follows:

  • reduce( kernelName ) (mandatory): Specifies that a reduction kernel is being defined. A reflected Java method reduce_ kernelName will launch the kernel.
  • initializer( initializerName ) (optional): Specifies the name of the initializer function for this reduction kernel. When you launch the kernel, RenderScript calls this function once for each accumulator data item . The function must be defined like this:

    static void initializerName(accumType *accum) { … }

    accum is a pointer to an accumulator data item for this function to initialize.

    If you do not provide an initializer function, RenderScript initializes every accumulator data item to zero (as if by memset ), behaving as if there were an initializer function that looks like this:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator( accumulatorName ) (mandatory): Specifies the name of the accumulator function for this reduction kernel. When you launch the kernel, RenderScript calls this function once for every coordinate in the input(s), to update an accumulator data item in some way according to the input(s). The function must be defined like this:

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

    accum is a pointer to an accumulator data item for this function to modify. in1 through in N are one or more arguments that are automatically filled in based on the inputs passed to the kernel launch, one argument per input. The accumulator function may optionally take any of the special arguments .

    An example kernel with multiple inputs is dotProduct .

  • combiner( combinerName )

    (optional): Specifies the name of the combiner function for this reduction kernel. After RenderScript calls the accumulator function once for every coordinate in the input(s), it calls this function as many times as necessary to combine all accumulator data items into a single accumulator data item. The function must be defined like this:

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

    accum is a pointer to a "destination" accumulator data item for this function to modify. other is a pointer to a "source" accumulator data item for this function to "combine" into *accum .

    NOTE: It is possible that *accum , *other , or both have been initialized but have never been passed to the accumulator function; that is, one or both have never been updated according to any input data. For example, in the findMinAndMax kernel, the combiner function fMMCombiner explicitly checks for idx < 0 because that indicates such an accumulator data item, whose value is INITVAL .

    If you do not provide a combiner function, RenderScript uses the accumulator function in its place, behaving as if there were a combiner function that looks like this:

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

    A combiner function is mandatory if the kernel has more than one input, if the input data type is not the same as the accumulator data type, or if the accumulator function takes one or more special arguments .

  • outconverter( outconverterName ) (optional): Specifies the name of the outconverter function for this reduction kernel. After RenderScript combines all of the accumulator data items, it calls this function to determine the result of the reduction to return to Java. The function must be defined like this:

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

    result is a pointer to a result data item (allocated but not initialized by the RenderScript runtime) for this function to initialize with the result of the reduction. resultType is the type of that data item, which need not be the same as accumType . accum is a pointer to the final accumulator data item computed by the combiner function .

    If you do not provide an outconverter function, RenderScript copies the final accumulator data item to the result data item, behaving as if there were an outconverter function that looks like this:

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

    If you want a different result type than the accumulator data type, then the outconverter function is mandatory.

Note that a kernel has input types, an accumulator data item type, and a result type, none of which need to be the same. For example, in the findMinAndMax kernel, the input type long , accumulator data item type MinAndMax , and result type int2 are all different.

What can't you assume?

You must not rely on the number of accumulator data items created by RenderScript for a given kernel launch. There is no guarantee that two launches of the same kernel with the same input(s) will create the same number of accumulator data items.

You must not rely on the order in which RenderScript calls the initializer, accumulator, and combiner functions; it may even call some of them in parallel. There is no guarantee that two launches of the same kernel with the same input will follow the same order. The only guarantee is that only the initializer function will ever see an uninitialized accumulator data item. به عنوان مثال:

  • There is no guarantee that all accumulator data items will be initialized before the accumulator function is called, although it will only be called on an initialized accumulator data item.
  • There is no guarantee on the order in which input Elements are passed to the accumulator function.
  • There is no guarantee that the accumulator function has been called for all input Elements before the combiner function is called.

One consequence of this is that the findMinAndMax kernel is not deterministic: If the input contains more than one occurrence of the same minimum or maximum value, you have no way of knowing which occurrence the kernel will find.

What must you guarantee?

Because the RenderScript system can choose to execute a kernel in many different ways , you must follow certain rules to ensure that your kernel behaves the way you want. If you do not follow these rules, you may get incorrect results, nondeterministic behavior, or runtime errors.

The rules below often say that two accumulator data items must have " the same value" . این به چه معناست؟ That depends on what you want the kernel to do. For a mathematical reduction such as addint , it usually makes sense for "the same" to mean mathematical equality. For a "pick any" search such as findMinAndMax ("find the location of minimum and maximum input values") where there might be more than one occurrence of identical input values, all locations of a given input value must be considered "the same" . You could write a similar kernel to "find the location of leftmost minimum and maximum input values" where (say) a minimum value at location 100 is preferred over an identical minimum value at location 200; for this kernel, "the same" would mean identical location , not merely identical value , and the accumulator and combiner functions would have to be different than those for findMinAndMax .

The initializer function must create an identity value . That is, if I and A are accumulator data items initialized by the initializer function, and I has never been passed to the accumulator function (but A may have been), then
  • combinerName (& A , & I ) must leave A the same
  • combinerName (& I , & A ) must leave I the same as A

Example: In the addint kernel, an accumulator data item is initialized to zero. The combiner function for this kernel performs addition; zero is the identity value for addition.

Example: In the findMinAndMax kernel, an accumulator data item is initialized to INITVAL .

  • fMMCombiner(& A , & I ) leaves A the same, because I is INITVAL .
  • fMMCombiner(& I , & A ) sets I to A , because I is INITVAL .

Therefore, INITVAL is indeed an identity value.

The combiner function must be commutative . That is, if A and B are accumulator data items initialized by the initializer function, and that may have been passed to the accumulator function zero or more times, then combinerName (& A , & B ) must set A to the same value that combinerName (& B , & A ) sets B .

Example: In the addint kernel, the combiner function adds the two accumulator data item values; addition is commutative.

Example: In the findMinAndMax kernel, fMMCombiner(& A , & B ) is the same as A = minmax( A , B ) , and minmax is commutative, so fMMCombiner is also.

The combiner function must be associative . That is, if A , B , and C are accumulator data items initialized by the initializer function, and that may have been passed to the accumulator function zero or more times, then the following two code sequences must set A to the same value :

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

Example: In the addint kernel, the combiner function adds the two accumulator data item values:

  • 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
    

Addition is associative, and so the combiner function is also.

Example: In the findMinAndMax kernel,

fMMCombiner(&A, &B)
همان است که
A = minmax(A, B)
So the two sequences are
  • 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 is associative, and so fMMCombiner is also.

The accumulator function and combiner function together must obey the basic folding rule . That is, if A and B are accumulator data items, A has been initialized by the initializer function and may have been passed to the accumulator function zero or more times, B has not been initialized, and args is the list of input arguments and special arguments for a particular call to the accumulator function, then the following two code sequences must set A to the same value :

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

Example: In the addint kernel, for an input value V :

  • Statement 1 is the same as A += V
  • Statement 2 is the same as B = 0
  • Statement 3 is the same as B += V , which is the same as B = V
  • Statement 4 is the same as A += B , which is the same as A += V

Statements 1 and 4 set A to the same value, and so this kernel obeys the basic folding rule.

Example: In the findMinAndMax kernel, for an input value V at coordinate X :

  • Statement 1 is the same as A = minmax(A, IndexedVal( V , X ))
  • Statement 2 is the same as B = INITVAL
  • Statement 3 is the same as
    B = minmax(B, IndexedVal(V, X))
    
    which, because B is the initial value, is the same as
    B = IndexedVal(V, X)
    
  • Statement 4 is the same as
    A = minmax(A, B)
    
    که همان است
    A = minmax(A, IndexedVal(V, X))
    

Statements 1 and 4 set A to the same value, and so this kernel obeys the basic folding rule.

Calling a reduction kernel from Java code

For a reduction kernel named kernelName defined in the file filename .rs , there are three methods reflected in the class ScriptC_ filename :

کاتلین

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

جاوا

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

Here are some examples of calling the addint kernel:

کاتلین

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

جاوا

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

Method 1 has one input Allocation argument for every input argument in the kernel's accumulator function . The RenderScript runtime checks to ensure that all of the input Allocations have the same dimensions and that the Element type of each of the input Allocations matches that of the corresponding input argument of the accumulator function's prototype. If any of these checks fail, RenderScript throws an exception. The kernel executes over every coordinate in those dimensions.

Method 2 is the same as Method 1 except that Method 2 takes an additional argument sc that can be used to limit the kernel execution to a subset of the coordinates.

Method 3 is the same as Method 1 except that instead of taking Allocation inputs it takes Java array inputs. This is a convenience that saves you from having to write code to explicitly create an Allocation and copy data to it from a Java array. However, using Method 3 instead of Method 1 does not increase the performance of the code . For each input array, Method 3 creates a temporary 1-dimensional Allocation with the appropriate Element type and setAutoPadding(boolean) enabled, and copies the array to the Allocation as if by the appropriate copyFrom() method of Allocation . It then calls Method 1, passing those temporary Allocations.

NOTE: If your application will make multiple kernel calls with the same array, or with different arrays of the same dimensions and Element type, you may improve performance by explicitly creating, populating, and reusing Allocations yourself, instead of by using Method 3.

javaFutureType , the return type of the reflected reduction methods, is a reflected static nested class within the ScriptC_ filename class. It represents the future result of a reduction kernel run. To obtain the actual result of the run, call the get() method of that class, which returns a value of type javaResultType . get() is synchronous .

کاتلین

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

جاوا

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

javaResultType is determined from the resultType of the outconverter function . Unless resultType is an unsigned type (scalar, vector, or array), javaResultType is the directly corresponding Java type. If resultType is an unsigned type and there is a larger Java signed type, then javaResultType is that larger Java signed type; otherwise, it is the directly corresponding Java type. به عنوان مثال:

  • If resultType is int , int2 , or int[15] , then javaResultType is int , Int2 , or int[] . All values of resultType can be represented by javaResultType .
  • If resultType is uint , uint2 , or uint[15] , then javaResultType is long , Long2 , or long[] . All values of resultType can be represented by javaResultType .
  • If resultType is ulong , ulong2 , or ulong[15] , then javaResultType is long , Long2 , or long[] . There are certain values of resultType that cannot be represented by javaResultType .

javaFutureType is the future result type corresponding to the resultType of the outconverter function .

  • If resultType is not an array type, then javaFutureType is result_ resultType .
  • If resultType is an array of length Count with members of type memberType , then javaFutureType is resultArray Count _ memberType .

به عنوان مثال:

کاتلین

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

جاوا

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

If javaResultType is an object type (including an array type), each call to javaFutureType .get() on the same instance will return the same object.

If javaResultType cannot represent all values of type resultType , and a reduction kernel produces an unrepresentible value, then javaFutureType .get() throws an exception.

Method 3 and devecSiInXType

devecSiInXType is the Java type corresponding to the inXType of the corresponding argument of the accumulator function . Unless inXType is an unsigned type or a vector type, devecSiInXType is the directly corresponding Java type. If inXType is an unsigned scalar type, then devecSiInXType is the Java type directly corresponding to the signed scalar type of the same size. If inXType is a signed vector type, then devecSiInXType is the Java type directly corresponding to the vector component type. If inXType is an unsigned vector type, then devecSiInXType is the Java type directly corresponding to the signed scalar type of the same size as the vector component type. به عنوان مثال:

  • If inXType is int , then devecSiInXType is int .
  • If inXType is int2 , then devecSiInXType is int . The array is a flattened representation: It has twice as many scalar Elements as the Allocation has 2-component vector Elements. This is the same way that the copyFrom() methods of Allocation work.
  • If inXType is uint , then deviceSiInXType is int . A signed value in the Java array is interpreted as an unsigned value of the same bitpattern in the Allocation. This is the same way that the copyFrom() methods of Allocation work.
  • If inXType is uint2 , then deviceSiInXType is int . This is a combination of the way int2 and uint are handled: The array is a flattened representation, and Java array signed values are interpreted as RenderScript unsigned Element values.

Note that for Method 3 , input types are handled differently than result types:

  • A script's vector input is flattened on the Java side, whereas a script's vector result is not.
  • A script's unsigned input is represented as a signed input of the same size on the Java side, whereas a script's unsigned result is represented as a widened signed type on the Java side (except in the case of ulong ).

More example reduction kernels

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

Additional code samples

The BasicRenderScript , RenderScriptIntrinsic , and Hello Compute samples further demonstrate the use of the APIs covered on this page.

،

RenderScript is a framework for running computationally intensive tasks at high performance on Android. RenderScript is primarily oriented for use with data-parallel computation, although serial workloads can benefit as well. The RenderScript runtime parallelizes work across processors available on a device, such as multi-core CPUs and GPUs. This allows you to focus on expressing algorithms rather than scheduling work. RenderScript is especially useful for applications performing image processing, computational photography, or computer vision.

To begin with RenderScript, there are two main concepts you should understand:

  • The language itself is a C99-derived language for writing high-performance compute code. Writing a RenderScript Kernel describes how to use it to write compute kernels.
  • The control API is used for managing the lifetime of RenderScript resources and controlling kernel execution. It is available in three different languages: Java, C++ in Android NDK, and the C99-derived kernel language itself. Using RenderScript from Java Code and Single-Source RenderScript describe the first and the third options, respectively.

Writing a RenderScript Kernel

A RenderScript kernel typically resides in a .rs file in the <project_root>/src/rs directory; each .rs file is called a script . Every script contains its own set of kernels, functions, and variables. A script can contain:

  • A pragma declaration ( #pragma version(1) ) that declares the version of the RenderScript kernel language used in this script. Currently, 1 is the only valid value.
  • A pragma declaration ( #pragma rs java_package_name(com.example.app) ) that declares the package name of the Java classes reflected from this script. Note that your .rs file must be part of your application package, and not in a library project.
  • Zero or more invokable functions . An invokable function is a single-threaded RenderScript function that you can call from your Java code with arbitrary arguments. These are often useful for initial setup or serial computations within a larger processing pipeline.
  • Zero or more script globals . A script global is similar to a global variable in C. You can access script globals from Java code, and these are often used for parameter passing to RenderScript kernels. Script globals are explained in more detail here .

  • Zero or more compute kernels . A compute kernel is a function or collection of functions that you can direct the RenderScript runtime to execute in parallel across a collection of data. There are two kinds of compute kernels: mapping kernels (also called foreach kernels) and reduction kernels.

    A mapping kernel is a parallel function that operates on a collection of Allocations of the same dimensions. By default, it executes once for every coordinate in those dimensions. It is typically (but not exclusively) used to transform a collection of input Allocations to an output Allocation one Element at a time.

    • Here is an example of a simple mapping kernel :

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

      In most respects, this is identical to a standard C function. The RS_KERNEL property applied to the function prototype specifies that the function is a RenderScript mapping kernel instead of an invokable function. The in argument is automatically filled in based on the input Allocation passed to the kernel launch. The arguments x and y are discussed below . The value returned from the kernel is automatically written to the appropriate location in the output Allocation . By default, this kernel is run across its entire input Allocation , with one execution of the kernel function per Element in the Allocation .

      A mapping kernel may have one or more input Allocations , a single output Allocation , or both. The RenderScript runtime checks to ensure that all input and output Allocations have the same dimensions, and that the Element types of the input and output Allocations match the kernel's prototype; if either of these checks fails, RenderScript throws an exception.

      NOTE: Before Android 6.0 (API level 23), a mapping kernel may not have more than one input Allocation .

      If you need more input or output Allocations than the kernel has, those objects should be bound to rs_allocation script globals and accessed from a kernel or invokable function via rsGetElementAt_ type () or rsSetElementAt_ type () .

      NOTE: RS_KERNEL is a macro defined automatically by RenderScript for your convenience:

      #define RS_KERNEL __attribute__((kernel))
      

    A reduction kernel is a family of functions that operates on a collection of input Allocations of the same dimensions. By default, its accumulator function executes once for every coordinate in those dimensions. It is typically (but not exclusively) used to "reduce" a collection of input Allocations to a single value.

    • Here is an example of a simple reduction kernel that adds up the Elements of its input:

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

      A reduction kernel consists of one or more user-written functions. #pragma rs reduce is used to define the kernel by specifying its name ( addint , in this example) and the names and roles of the functions that make up the kernel (an accumulator function addintAccum , in this example). All such functions must be static . A reduction kernel always requires an accumulator function; it may also have other functions, depending on what you want the kernel to do.

      A reduction kernel accumulator function must return void and must have at least two arguments. The first argument ( accum , in this example) is a pointer to an accumulator data item and the second ( val , in this example) is automatically filled in based on the input Allocation passed to the kernel launch. The accumulator data item is created by the RenderScript runtime; by default, it is initialized to zero. By default, this kernel is run across its entire input Allocation , with one execution of the accumulator function per Element in the Allocation . By default, the final value of the accumulator data item is treated as the result of the reduction, and is returned to Java. The RenderScript runtime checks to ensure that the Element type of the input Allocation matches the accumulator function's prototype; if it does not match, RenderScript throws an exception.

      A reduction kernel has one or more input Allocations but no output Allocations .

      Reduction kernels are explained in more detail here .

      Reduction kernels are supported in Android 7.0 (API level 24) and later.

    A mapping kernel function or a reduction kernel accumulator function may access the coordinates of the current execution using the special arguments x , y , and z , which must be of type int or uint32_t . These arguments are optional.

    A mapping kernel function or a reduction kernel accumulator function may also take the optional special argument context of type rs_kernel_context . It is needed by a family of runtime APIs that are used to query certain properties of the current execution -- for example, rsGetDimX . (The context argument is available in Android 6.0 (API level 23) and later.)

  • An optional init() function. The init() function is a special type of invokable function that RenderScript runs when the script is first instantiated. This allows for some computation to occur automatically at script creation.
  • Zero or more static script globals and functions . A static script global is equivalent to a script global except that it cannot be accessed from Java code. A static function is a standard C function that can be called from any kernel or invokable function in the script but is not exposed to the Java API. If a script global or function does not need to be accessed from Java code, it is highly recommended that it be declared static .

Setting floating point precision

You can control the required level of floating point precision in a script. This is useful if full IEEE 754-2008 standard (used by default) is not required. The following pragmas can set a different level of floating point precision:

  • #pragma rs_fp_full (default if nothing is specified): For apps that require floating point precision as outlined by the IEEE 754-2008 standard.
  • #pragma rs_fp_relaxed : For apps that don't require strict IEEE 754-2008 compliance and can tolerate less precision. This mode enables flush-to-zero for denorms and round-towards-zero.
  • #pragma rs_fp_imprecise : For apps that don't have stringent precision requirements. This mode enables everything in rs_fp_relaxed along with the following:
    • Operations resulting in -0.0 can return +0.0 instead.
    • Operations on INF and NAN are undefined.

Most applications can use rs_fp_relaxed without any side effects. This may be very beneficial on some architectures due to additional optimizations only available with relaxed precision (such as SIMD CPU instructions).

Accessing RenderScript APIs from Java

When developing an Android application that uses RenderScript, you can access its API from Java in one of two ways:

Here are the tradeoffs:

  • If you use the Support Library APIs, the RenderScript portion of your application will be compatible with devices running Android 2.3 (API level 9) and higher, regardless of which RenderScript features you use. This allows your application to work on more devices than if you use the native ( android.renderscript ) APIs.
  • Certain RenderScript features are not available through the Support Library APIs.
  • If you use the Support Library APIs, you will get (possibly significantly) larger APKs than if you use the native ( android.renderscript ) APIs.

Using the RenderScript Support Library APIs

In order to use the Support Library RenderScript APIs, you must configure your development environment to be able to access them. The following Android SDK tools are required for using these APIs:

  • Android SDK Tools revision 22.2 or higher
  • Android SDK Build-tools revision 18.1.0 or higher

Note that starting from Android SDK Build-tools 24.0.0, Android 2.2 (API level 8) is no longer supported.

You can check and update the installed version of these tools in the Android SDK Manager .

To use the Support Library RenderScript APIs:

  1. Make sure you have the required Android SDK version installed.
  2. Update the settings for the Android build process to include the RenderScript settings:
    • Open the build.gradle file in the app folder of your application module.
    • Add the following RenderScript settings to the file:

      شیار

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

      کاتلین

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

      The settings listed above control specific behavior in the Android build process:

      • renderscriptTargetApi - Specifies the bytecode version to be generated. We recommend you set this value to the lowest API level able to provide all the functionality you are using and set renderscriptSupportModeEnabled to true . Valid values for this setting are any integer value from 11 to the most recently released API level. If your minimum SDK version specified in your application manifest is set to a different value, that value is ignored and the target value in the build file is used to set the minimum SDK version.
      • renderscriptSupportModeEnabled - Specifies that the generated bytecode should fall back to a compatible version if the device it is running on does not support the target version.
  3. In your application classes that use RenderScript, add an import for the Support Library classes:

    کاتلین

    import android.support.v8.renderscript.*
    

    جاوا

    import android.support.v8.renderscript.*;
    

Using RenderScript from Java or Kotlin Code

Using RenderScript from Java or Kotlin code relies on the API classes located in the android.renderscript or the android.support.v8.renderscript package. Most applications follow the same basic usage pattern:

  1. Initialize a RenderScript context. The RenderScript context, created with create(Context) , ensures that RenderScript can be used and provides an object to control the lifetime of all subsequent RenderScript objects. You should consider context creation to be a potentially long-running operation, since it may create resources on different pieces of hardware; it should not be in an application's critical path if at all possible. Typically, an application will have only a single RenderScript context at a time.
  2. Create at least one Allocation to be passed to a script. An Allocation is a RenderScript object that provides storage for a fixed amount of data. Kernels in scripts take Allocation objects as their input and output, and Allocation objects can be accessed in kernels using rsGetElementAt_ type () and rsSetElementAt_ type () when bound as script globals. Allocation objects allow arrays to be passed from Java code to RenderScript code and vice-versa. Allocation objects are typically created using createTyped() or createFromBitmap() .
  3. Create whatever scripts are necessary. There are two types of scripts available to you when using RenderScript:
    • ScriptC : These are the user-defined scripts as described in Writing a RenderScript Kernel above. Every script has a Java class reflected by the RenderScript compiler in order to make it easy to access the script from Java code; this class has the name ScriptC_ filename . For example, if the mapping kernel above were located in invert.rs and a RenderScript context were already located in mRenderScript , the Java or Kotlin code to instantiate the script would be:

      کاتلین

      val invert = ScriptC_invert(renderScript)
      

      جاوا

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic : These are built-in RenderScript kernels for common operations, such as Gaussian blur, convolution, and image blending. For more information, see the subclasses of ScriptIntrinsic .
  4. Populate Allocations with data. Except for Allocations created with createFromBitmap() , an Allocation is populated with empty data when it is first created. To populate an Allocation, use one of the "copy" methods in Allocation . The "copy" methods are synchronous .
  5. Set any necessary script globals . You may set globals using methods in the same ScriptC_ filename class named set_ globalname . For example, in order to set an int variable named threshold , use the Java method set_threshold(int) ; and in order to set an rs_allocation variable named lookup , use the Java method set_lookup(Allocation) . The set methods are asynchronous .
  6. Launch the appropriate kernels and invokable functions.

    Methods to launch a given kernel are reflected in the same ScriptC_ filename class with methods named forEach_ mappingKernelName () or reduce_ reductionKernelName () . These launches are asynchronous . Depending on the arguments to the kernel, the method takes one or more Allocations, all of which must have the same dimensions. By default, a kernel executes over every coordinate in those dimensions; to execute a kernel over a subset of those coordinates, pass an appropriate Script.LaunchOptions as the last argument to the forEach or reduce method.

    Launch invokable functions using the invoke_ functionName methods reflected in the same ScriptC_ filename class. These launches are asynchronous .

  7. Retrieve data from Allocation objects and javaFutureType objects. In order to access data from an Allocation from Java code, you must copy that data back to Java using one of the "copy" methods in Allocation . In order to obtain the result of a reduction kernel, you must use the javaFutureType .get() method. The "copy" and get() methods are synchronous .
  8. Tear down the RenderScript context. You can destroy the RenderScript context with destroy() or by allowing the RenderScript context object to be garbage collected. This causes any further use of any object belonging to that context to throw an exception.

Asynchronous execution model

The reflected forEach , invoke , reduce , and set methods are asynchronous -- each may return to Java before completing the requested action. However, the individual actions are serialized in the order in which they are launched.

The Allocation class provides "copy" methods to copy data to and from Allocations. A "copy" method is synchronous, and is serialized with respect to any of the asynchronous actions above that touch the same Allocation.

The reflected javaFutureType classes provide a get() method to obtain the result of a reduction. get() is synchronous, and is serialized with respect to the reduction (which is asynchronous).

Single-Source RenderScript

Android 7.0 (API level 24) introduces a new programming feature called Single-Source RenderScript , in which kernels are launched from the script where they are defined, rather than from Java. This approach is currently limited to mapping kernels, which are simply referred to as "kernels" in this section for conciseness. This new feature also supports creating allocations of type rs_allocation from inside the script. It is now possible to implement a whole algorithm solely within a script, even if multiple kernel launches are required. The benefit is twofold: more readable code, because it keeps the implementation of an algorithm in one language; and potentially faster code, because of fewer transitions between Java and RenderScript across multiple kernel launches.

In Single-Source RenderScript, you write kernels as described in Writing a RenderScript Kernel . You then write an invokable function that calls rsForEach() to launch them. That API takes a kernel function as the first parameter, followed by input and output allocations. A similar API rsForEachWithOptions() takes an extra argument of type rs_script_call_t , which specifies a subset of the elements from the input and output allocations for the kernel function to process.

To start RenderScript computation, you call the invokable function from Java. Follow the steps in Using RenderScript from Java Code . In the step launch the appropriate kernels , call the invokable function using invoke_ function_name () , which will start the whole computation, including launching kernels.

Allocations are often needed to save and pass intermediate results from one kernel launch to another. You can create them using rsCreateAllocation() . One easy-to-use form of that API is rsCreateAllocation_<T><W>(…) , where T is the data type for an element, and W is the vector width for the element. The API takes the sizes in dimensions X, Y, and Z as arguments. For 1D or 2D allocations, the size for dimension Y or Z can be omitted. For example, rsCreateAllocation_uchar4(16384) creates a 1D allocation of 16384 elements, each of which is of type uchar4 .

Allocations are managed by the system automatically. You do not have to explicitly release or free them. However, you can call rsClearObject(rs_allocation* alloc) to indicate you no longer need the handle alloc to the underlying allocation, so that the system can free up resources as early as possible.

The Writing a RenderScript Kernel section contains an example kernel that inverts an image. The example below expands that to apply more than one effect to an image, using Single-Source RenderScript. It includes another kernel, greyscale , which turns a color image into black-and-white. An invokable function process() then applies those two kernels consecutively to an input image, and produces an output image. Allocations for both the input and the output are passed in as arguments of type 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);
}

You can call the process() function from Java or Kotlin as follows:

کاتلین

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)

جاوا

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

This example shows how an algorithm that involves two kernel launches can be implemented completely in the RenderScript language itself. Without Single-Source RenderScript, you would have to launch both kernels from the Java code, separating kernel launches from kernel definitions and making it harder to understand the whole algorithm. Not only is the Single-Source RenderScript code easier to read, it also eliminates the transitioning between Java and the script across kernel launches. Some iterative algorithms may launch kernels hundreds of times, making the overhead of such transitioning considerable.

Script Globals

A script global is an ordinary non- static global variable in a script ( .rs ) file. For a script global named var defined in the file filename .rs , there will be a method get_ var reflected in the class ScriptC_ filename . Unless the global is const , there will also be a method set_ var .

A given script global has two separate values -- a Java value and a script value. These values behave as follows:

  • If var has a static initializer in the script, it specifies the initial value of var in both Java and the script. Otherwise, that initial value is zero.
  • Accesses to var within the script read and write its script value.
  • The get_ var method reads the Java value.
  • The set_ var method (if it exists) writes the Java value immediately, and writes the script value asynchronously .

NOTE: This means that except for any static initializer in the script, values written to a global from within a script are not visible to Java.

Reduction Kernels in Depth

Reduction is the process of combining a collection of data into a single value. This is a useful primitive in parallel programming, with applications such as the following:

  • computing the sum or product over all the data
  • computing logical operations ( and , or , xor ) over all the data
  • finding the minimum or maximum value within the data
  • searching for a specific value or for the coordinate of a specific value within the data

In Android 7.0 (API level 24) and later, RenderScript supports reduction kernels to allow efficient user-written reduction algorithms. You may launch reduction kernels on inputs with 1, 2, or 3 dimensions.

An example above shows a simple addint reduction kernel. Here is a more complicated findMinAndMax reduction kernel that finds the locations of the minimum and maximum long values in a 1-dimensional Allocation :

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

NOTE: There are more example reduction kernels here .

In order to run a reduction kernel, the RenderScript runtime creates one or more variables called accumulator data items to hold the state of the reduction process. The RenderScript runtime picks the number of accumulator data items in such a way as to maximize performance. The type of the accumulator data items ( accumType ) is determined by the kernel's accumulator function -- the first argument to that function is a pointer to an accumulator data item. By default, every accumulator data item is initialized to zero (as if by memset ); however, you may write an initializer function to do something different.

Example: In the addint kernel, the accumulator data items (of type int ) are used to add up input values. There is no initializer function, so each accumulator data item is initialized to zero.

Example: In the findMinAndMax kernel, the accumulator data items (of type MinAndMax ) are used to keep track of the minimum and maximum values found so far. There is an initializer function to set these to LONG_MAX and LONG_MIN , respectively; and to set the locations of these values to -1, indicating that the values are not actually present in the (empty) portion of the input that has been processed.

RenderScript calls your accumulator function once for every coordinate in the input(s). Typically, your function should update the accumulator data item in some way according to the input.

Example: In the addint kernel, the accumulator function adds the value of an input Element to the accumulator data item.

Example: In the findMinAndMax kernel, the accumulator function checks to see whether the value of an input Element is less than or equal to the minimum value recorded in the accumulator data item and/or greater than or equal to the maximum value recorded in the accumulator data item, and updates the accumulator data item accordingly.

After the accumulator function has been called once for every coordinate in the input(s), RenderScript must combine the accumulator data items together into a single accumulator data item. You may write a combiner function to do this. If the accumulator function has a single input and no special arguments , then you do not need to write a combiner function; RenderScript will use the accumulator function to combine the accumulator data items. (You may still write a combiner function if this default behavior is not what you want.)

Example: In the addint kernel, there is no combiner function, so the accumulator function will be used. This is the correct behavior, because if we split a collection of values into two pieces, and we add up the values in those two pieces separately, adding up those two sums is the same as adding up the entire collection.

Example: In the findMinAndMax kernel, the combiner function checks to see whether the minimum value recorded in the "source" accumulator data item *val is less than the minimum value recorded in the "destination" accumulator data item *accum , and updates *accum بر این اساس. It does similar work for the maximum value. This updates *accum to the state it would have had if all of the input values had been accumulated into *accum rather than some into *accum and some into *val .

After all of the accumulator data items have been combined, RenderScript determines the result of the reduction to return to Java. You may write an outconverter function to do this. You do not need to write an outconverter function if you want the final value of the combined accumulator data items to be the result of the reduction.

Example: In the addint kernel, there is no outconverter function. The final value of the combined data items is the sum of all Elements of the input, which is the value we want to return.

Example: In the findMinAndMax kernel, the outconverter function initializes an int2 result value to hold the locations of the minimum and maximum values resulting from the combination of all of the accumulator data items.

Writing a reduction kernel

#pragma rs reduce defines a reduction kernel by specifying its name and the names and roles of the functions that make up the kernel. All such functions must be static . A reduction kernel always requires an accumulator function; you can omit some or all of the other functions, depending on what you want the kernel to do.

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

The meaning of the items in the #pragma is as follows:

  • reduce( kernelName ) (mandatory): Specifies that a reduction kernel is being defined. A reflected Java method reduce_ kernelName will launch the kernel.
  • initializer( initializerName ) (optional): Specifies the name of the initializer function for this reduction kernel. When you launch the kernel, RenderScript calls this function once for each accumulator data item . The function must be defined like this:

    static void initializerName(accumType *accum) { … }

    accum is a pointer to an accumulator data item for this function to initialize.

    If you do not provide an initializer function, RenderScript initializes every accumulator data item to zero (as if by memset ), behaving as if there were an initializer function that looks like this:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator( accumulatorName ) (mandatory): Specifies the name of the accumulator function for this reduction kernel. When you launch the kernel, RenderScript calls this function once for every coordinate in the input(s), to update an accumulator data item in some way according to the input(s). The function must be defined like this:

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

    accum is a pointer to an accumulator data item for this function to modify. in1 through in N are one or more arguments that are automatically filled in based on the inputs passed to the kernel launch, one argument per input. The accumulator function may optionally take any of the special arguments .

    An example kernel with multiple inputs is dotProduct .

  • combiner( combinerName )

    (optional): Specifies the name of the combiner function for this reduction kernel. After RenderScript calls the accumulator function once for every coordinate in the input(s), it calls this function as many times as necessary to combine all accumulator data items into a single accumulator data item. The function must be defined like this:

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

    accum is a pointer to a "destination" accumulator data item for this function to modify. other is a pointer to a "source" accumulator data item for this function to "combine" into *accum .

    NOTE: It is possible that *accum , *other , or both have been initialized but have never been passed to the accumulator function; that is, one or both have never been updated according to any input data. For example, in the findMinAndMax kernel, the combiner function fMMCombiner explicitly checks for idx < 0 because that indicates such an accumulator data item, whose value is INITVAL .

    If you do not provide a combiner function, RenderScript uses the accumulator function in its place, behaving as if there were a combiner function that looks like this:

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

    A combiner function is mandatory if the kernel has more than one input, if the input data type is not the same as the accumulator data type, or if the accumulator function takes one or more special arguments .

  • outconverter( outconverterName ) (optional): Specifies the name of the outconverter function for this reduction kernel. After RenderScript combines all of the accumulator data items, it calls this function to determine the result of the reduction to return to Java. The function must be defined like this:

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

    result is a pointer to a result data item (allocated but not initialized by the RenderScript runtime) for this function to initialize with the result of the reduction. resultType is the type of that data item, which need not be the same as accumType . accum is a pointer to the final accumulator data item computed by the combiner function .

    If you do not provide an outconverter function, RenderScript copies the final accumulator data item to the result data item, behaving as if there were an outconverter function that looks like this:

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

    If you want a different result type than the accumulator data type, then the outconverter function is mandatory.

Note that a kernel has input types, an accumulator data item type, and a result type, none of which need to be the same. For example, in the findMinAndMax kernel, the input type long , accumulator data item type MinAndMax , and result type int2 are all different.

What can't you assume?

You must not rely on the number of accumulator data items created by RenderScript for a given kernel launch. There is no guarantee that two launches of the same kernel with the same input(s) will create the same number of accumulator data items.

You must not rely on the order in which RenderScript calls the initializer, accumulator, and combiner functions; it may even call some of them in parallel. There is no guarantee that two launches of the same kernel with the same input will follow the same order. The only guarantee is that only the initializer function will ever see an uninitialized accumulator data item. به عنوان مثال:

  • There is no guarantee that all accumulator data items will be initialized before the accumulator function is called, although it will only be called on an initialized accumulator data item.
  • There is no guarantee on the order in which input Elements are passed to the accumulator function.
  • There is no guarantee that the accumulator function has been called for all input Elements before the combiner function is called.

One consequence of this is that the findMinAndMax kernel is not deterministic: If the input contains more than one occurrence of the same minimum or maximum value, you have no way of knowing which occurrence the kernel will find.

What must you guarantee?

Because the RenderScript system can choose to execute a kernel in many different ways , you must follow certain rules to ensure that your kernel behaves the way you want. If you do not follow these rules, you may get incorrect results, nondeterministic behavior, or runtime errors.

The rules below often say that two accumulator data items must have " the same value" . این به چه معناست؟ That depends on what you want the kernel to do. For a mathematical reduction such as addint , it usually makes sense for "the same" to mean mathematical equality. For a "pick any" search such as findMinAndMax ("find the location of minimum and maximum input values") where there might be more than one occurrence of identical input values, all locations of a given input value must be considered "the same" . You could write a similar kernel to "find the location of leftmost minimum and maximum input values" where (say) a minimum value at location 100 is preferred over an identical minimum value at location 200; for this kernel, "the same" would mean identical location , not merely identical value , and the accumulator and combiner functions would have to be different than those for findMinAndMax .

The initializer function must create an identity value . That is, if I and A are accumulator data items initialized by the initializer function, and I has never been passed to the accumulator function (but A may have been), then
  • combinerName (& A , & I ) must leave A the same
  • combinerName (& I , & A ) must leave I the same as A

Example: In the addint kernel, an accumulator data item is initialized to zero. The combiner function for this kernel performs addition; zero is the identity value for addition.

Example: In the findMinAndMax kernel, an accumulator data item is initialized to INITVAL .

  • fMMCombiner(& A , & I ) leaves A the same, because I is INITVAL .
  • fMMCombiner(& I , & A ) sets I to A , because I is INITVAL .

Therefore, INITVAL is indeed an identity value.

The combiner function must be commutative . That is, if A and B are accumulator data items initialized by the initializer function, and that may have been passed to the accumulator function zero or more times, then combinerName (& A , & B ) must set A to the same value that combinerName (& B , & A ) sets B .

Example: In the addint kernel, the combiner function adds the two accumulator data item values; addition is commutative.

Example: In the findMinAndMax kernel, fMMCombiner(& A , & B ) is the same as A = minmax( A , B ) , and minmax is commutative, so fMMCombiner is also.

The combiner function must be associative . That is, if A , B , and C are accumulator data items initialized by the initializer function, and that may have been passed to the accumulator function zero or more times, then the following two code sequences must set A to the same value :

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

Example: In the addint kernel, the combiner function adds the two accumulator data item values:

  • 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
    

Addition is associative, and so the combiner function is also.

Example: In the findMinAndMax kernel,

fMMCombiner(&A, &B)
همان است که
A = minmax(A, B)
So the two sequences are
  • 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 is associative, and so fMMCombiner is also.

The accumulator function and combiner function together must obey the basic folding rule . That is, if A and B are accumulator data items, A has been initialized by the initializer function and may have been passed to the accumulator function zero or more times, B has not been initialized, and args is the list of input arguments and special arguments for a particular call to the accumulator function, then the following two code sequences must set A to the same value :

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

Example: In the addint kernel, for an input value V :

  • Statement 1 is the same as A += V
  • Statement 2 is the same as B = 0
  • Statement 3 is the same as B += V , which is the same as B = V
  • Statement 4 is the same as A += B , which is the same as A += V

Statements 1 and 4 set A to the same value, and so this kernel obeys the basic folding rule.

Example: In the findMinAndMax kernel, for an input value V at coordinate X :

  • Statement 1 is the same as A = minmax(A, IndexedVal( V , X ))
  • Statement 2 is the same as B = INITVAL
  • Statement 3 is the same as
    B = minmax(B, IndexedVal(V, X))
    
    which, because B is the initial value, is the same as
    B = IndexedVal(V, X)
    
  • Statement 4 is the same as
    A = minmax(A, B)
    
    که همان است
    A = minmax(A, IndexedVal(V, X))
    

Statements 1 and 4 set A to the same value, and so this kernel obeys the basic folding rule.

Calling a reduction kernel from Java code

For a reduction kernel named kernelName defined in the file filename .rs , there are three methods reflected in the class ScriptC_ filename :

کاتلین

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

جاوا

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

Here are some examples of calling the addint kernel:

کاتلین

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

جاوا

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

Method 1 has one input Allocation argument for every input argument in the kernel's accumulator function . The RenderScript runtime checks to ensure that all of the input Allocations have the same dimensions and that the Element type of each of the input Allocations matches that of the corresponding input argument of the accumulator function's prototype. If any of these checks fail, RenderScript throws an exception. The kernel executes over every coordinate in those dimensions.

Method 2 is the same as Method 1 except that Method 2 takes an additional argument sc that can be used to limit the kernel execution to a subset of the coordinates.

Method 3 is the same as Method 1 except that instead of taking Allocation inputs it takes Java array inputs. This is a convenience that saves you from having to write code to explicitly create an Allocation and copy data to it from a Java array. However, using Method 3 instead of Method 1 does not increase the performance of the code . For each input array, Method 3 creates a temporary 1-dimensional Allocation with the appropriate Element type and setAutoPadding(boolean) enabled, and copies the array to the Allocation as if by the appropriate copyFrom() method of Allocation . It then calls Method 1, passing those temporary Allocations.

NOTE: If your application will make multiple kernel calls with the same array, or with different arrays of the same dimensions and Element type, you may improve performance by explicitly creating, populating, and reusing Allocations yourself, instead of by using Method 3.

javaFutureType , the return type of the reflected reduction methods, is a reflected static nested class within the ScriptC_ filename class. It represents the future result of a reduction kernel run. To obtain the actual result of the run, call the get() method of that class, which returns a value of type javaResultType . get() is synchronous .

کاتلین

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

جاوا

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

javaResultType is determined from the resultType of the outconverter function . Unless resultType is an unsigned type (scalar, vector, or array), javaResultType is the directly corresponding Java type. If resultType is an unsigned type and there is a larger Java signed type, then javaResultType is that larger Java signed type; otherwise, it is the directly corresponding Java type. به عنوان مثال:

  • If resultType is int , int2 , or int[15] , then javaResultType is int , Int2 , or int[] . All values of resultType can be represented by javaResultType .
  • If resultType is uint , uint2 , or uint[15] , then javaResultType is long , Long2 , or long[] . All values of resultType can be represented by javaResultType .
  • If resultType is ulong , ulong2 , or ulong[15] , then javaResultType is long , Long2 , or long[] . There are certain values of resultType that cannot be represented by javaResultType .

javaFutureType is the future result type corresponding to the resultType of the outconverter function .

  • If resultType is not an array type, then javaFutureType is result_ resultType .
  • If resultType is an array of length Count with members of type memberType , then javaFutureType is resultArray Count _ memberType .

به عنوان مثال:

کاتلین

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

جاوا

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

If javaResultType is an object type (including an array type), each call to javaFutureType .get() on the same instance will return the same object.

If javaResultType cannot represent all values of type resultType , and a reduction kernel produces an unrepresentible value, then javaFutureType .get() throws an exception.

Method 3 and devecSiInXType

devecSiInXType is the Java type corresponding to the inXType of the corresponding argument of the accumulator function . Unless inXType is an unsigned type or a vector type, devecSiInXType is the directly corresponding Java type. If inXType is an unsigned scalar type, then devecSiInXType is the Java type directly corresponding to the signed scalar type of the same size. If inXType is a signed vector type, then devecSiInXType is the Java type directly corresponding to the vector component type. If inXType is an unsigned vector type, then devecSiInXType is the Java type directly corresponding to the signed scalar type of the same size as the vector component type. به عنوان مثال:

  • If inXType is int , then devecSiInXType is int .
  • If inXType is int2 , then devecSiInXType is int . The array is a flattened representation: It has twice as many scalar Elements as the Allocation has 2-component vector Elements. This is the same way that the copyFrom() methods of Allocation work.
  • If inXType is uint , then deviceSiInXType is int . A signed value in the Java array is interpreted as an unsigned value of the same bitpattern in the Allocation. This is the same way that the copyFrom() methods of Allocation work.
  • If inXType is uint2 , then deviceSiInXType is int . This is a combination of the way int2 and uint are handled: The array is a flattened representation, and Java array signed values are interpreted as RenderScript unsigned Element values.

Note that for Method 3 , input types are handled differently than result types:

  • A script's vector input is flattened on the Java side, whereas a script's vector result is not.
  • A script's unsigned input is represented as a signed input of the same size on the Java side, whereas a script's unsigned result is represented as a widened signed type on the Java side (except in the case of ulong ).

More example reduction kernels

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

Additional code samples

The BasicRenderScript , RenderScriptIntrinsic , and Hello Compute samples further demonstrate the use of the APIs covered on this page.