نمای کلی RenderScript

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

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

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

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

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

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

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

    یک هسته نگاشت ، یک تابع موازی است که روی مجموعه‌ای از 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 یک استثنا ایجاد می‌کند.

      نکته: قبل از اندروید ۶.۰ (سطح API ۲۳)، یک هسته نگاشت نمی‌توانست بیش از یک 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 ورودی Allocation با نمونه اولیه تابع انباشتگر مطابقت دارد. اگر مطابقت نداشته باشد، RenderScript یک استثنا ایجاد می‌کند.

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

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

      هسته‌های کاهشی در اندروید ۷.۰ (سطح API ۲۴) و بالاتر پشتیبانی می‌شوند.

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

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

  • یک تابع init() اختیاری. تابع init() نوع خاصی از تابع قابل فراخوانی است که RenderScript هنگام اولین نمونه‌سازی اسکریپت اجرا می‌کند. این امر امکان انجام خودکار برخی محاسبات را در هنگام ایجاد اسکریپت فراهم می‌کند.
  • صفر یا چند تابع و گلوبال اسکریپت استاتیک . یک گلوبال اسکریپت استاتیک معادل یک گلوبال اسکریپت است با این تفاوت که از کد جاوا قابل دسترسی نیست. یک تابع استاتیک یک تابع استاندارد C است که می‌تواند از هر هسته یا تابع قابل فراخوانی در اسکریپت فراخوانی شود اما در معرض 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 استفاده کنند. این امر می‌تواند در برخی معماری‌ها به دلیل بهینه‌سازی‌های اضافی که فقط با دقت آرام (مانند دستورالعمل‌های SIMD CPU) در دسترس هستند، بسیار مفید باشد.

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

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

  • android.renderscript - رابط‌های برنامه‌نویسی کاربردی (API) موجود در این پکیج کلاس، در دستگاه‌هایی که اندروید ۳.۰ (سطح API 11) و بالاتر را اجرا می‌کنند، در دسترس هستند.
  • android.support.v8.renderscript - رابط‌های برنامه‌نویسی کاربردی (API) موجود در این بسته از طریق یک کتابخانه پشتیبانی (Support Library) در دسترس هستند که به شما امکان می‌دهد از آنها در دستگاه‌هایی که اندروید ۲.۳ (API level 9) و بالاتر دارند استفاده کنید.

در اینجا به این موارد اشاره می‌کنیم:

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

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

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

  • ابزارهای SDK اندروید نسخه ۲۲.۲ یا بالاتر
  • ابزارهای ساخت SDK اندروید نسخه ۱۸.۱.۰ یا بالاتر

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

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

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

  1. مطمئن شوید که نسخه مورد نیاز Android SDK را نصب کرده‌اید.
  2. تنظیمات فرآیند ساخت اندروید را به‌روزرسانی کنید تا تنظیمات RenderScript را نیز شامل شود:
    • فایل build.gradle را در پوشه app ماژول اپلیکیشن خود باز کنید.
    • تنظیمات 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 تنظیم کنید. مقادیر معتبر برای این تنظیم، هر مقدار صحیحی از ۱۱ تا آخرین سطح API منتشر شده است. اگر حداقل نسخه SDK شما که در مانیفست برنامه‌تان مشخص شده است، روی مقدار متفاوتی تنظیم شده باشد، آن مقدار نادیده گرفته می‌شود و از مقدار target در فایل ساخت برای تنظیم حداقل نسخه SDK استفاده می‌شود.
      • renderscriptSupportModeEnabled - مشخص می‌کند که اگر دستگاهی که بایت‌کد روی آن اجرا می‌شود از نسخه هدف پشتیبانی نمی‌کند، باید به یک نسخه سازگار بازگردد.
  3. در کلاس‌های برنامه‌تان که از RenderScript استفاده می‌کنند، یک import برای کلاس‌های Support Library اضافه کنید:

    کاتلین

    import android.support.v8.renderscript.*

    جاوا

    import android.support.v8.renderscript.*;

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

استفاده از RenderScript از کد جاوا یا کاتلین به کلاس‌های 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» در بالا توضیح داده شده است. هر اسکریپت دارای یک کلاس جاوا است که توسط کامپایلر RenderScript منعکس می‌شود تا دسترسی به اسکریپت از کد جاوا آسان شود. این کلاس دارای نام ScriptC_ filename است. برای مثال، اگر هسته نگاشت بالا در invert.rs قرار داشته باشد و یک زمینه RenderScript از قبل در mRenderScript قرار داشته باشد، کد جاوا یا کاتلین برای نمونه‌سازی اسکریپت به صورت زیر خواهد بود:

      کاتلین

      val invert = ScriptC_invert(renderScript)

      جاوا

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

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

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

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

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

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

کلاس Allocation متدهای "کپی" را برای کپی کردن داده‌ها به و از Allocations ارائه می‌دهد. یک متد "کپی" همزمان است و نسبت به هر یک از اقدامات غیرهمزمان بالا که با همان Allocation تماس دارند، سریالی می‌شود.

کلاس‌های javaFutureType منعکس‌شده، یک متد get() برای دریافت نتیجه‌ی یک کاهش ارائه می‌دهند. get() همگام است و نسبت به کاهش (که ناهمگام است) سریالی می‌شود.

رندراسکریپت تک‌منبعی

اندروید ۷.۰ (سطح API ۲۴) یک ویژگی برنامه‌نویسی جدید به نام Single-Source RenderScript را معرفی می‌کند که در آن هسته‌ها از اسکریپتی که در آن تعریف شده‌اند، به جای جاوا، اجرا می‌شوند. این رویکرد در حال حاضر به نگاشت هسته‌ها محدود است که در این بخش برای اختصار، به سادگی "هسته" نامیده می‌شوند. این ویژگی جدید همچنین از ایجاد تخصیص‌هایی از نوع rs_allocation از داخل اسکریپت پشتیبانی می‌کند. اکنون می‌توان کل یک الگوریتم را صرفاً درون یک اسکریپت پیاده‌سازی کرد، حتی اگر چندین راه‌اندازی هسته مورد نیاز باشد. این مزیت دو چندان است: کد خواناتر، زیرا پیاده‌سازی یک الگوریتم را در یک زبان نگه می‌دارد؛ و کد بالقوه سریع‌تر، به دلیل انتقال کمتر بین جاوا و RenderScript در چندین راه‌اندازی هسته.

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

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

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

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

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

اسکریپت گلوبال

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

یک اسکریپت گلوبال مشخص دو مقدار جداگانه دارد -- یک مقدار جاوا و یک مقدار اسکریپت . این مقادیر به شرح زیر رفتار می‌کنند:

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

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

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

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

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

در اندروید ۷.۰ (سطح API 24) و بالاتر، RenderScript از هسته‌های کاهشی پشتیبانی می‌کند تا الگوریتم‌های کاهشی نوشته شده توسط کاربر را به طور کارآمد اجرا کند. می‌توانید هسته‌های کاهشی را روی ورودی‌هایی با ۱، ۲ یا ۳ بعد راه‌اندازی کنید.

مثال بالا یک هسته ساده کاهش جمع‌شونده را نشان می‌دهد. در اینجا یک هسته پیچیده‌تر کاهشی findMinAndMax را مشاهده می‌کنید که مکان‌های حداقل و حداکثر مقادیر long را در یک 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;
}

توجه: نمونه‌های بیشتری از هسته‌های کاهشی در اینجا وجود دارد.

برای اجرای یک هسته کاهش، زمان اجرای RenderScript یک یا چند متغیر به نام اقلام داده انباشتگر ایجاد می‌کند تا وضعیت فرآیند کاهش را نگه دارد. زمان اجرای RenderScript تعداد اقلام داده انباشتگر را به گونه‌ای انتخاب می‌کند که عملکرد را به حداکثر برساند. نوع اقلام داده انباشتگر ( accumType ) توسط تابع انباشتگر هسته تعیین می‌شود -- اولین آرگومان برای آن تابع، اشاره‌گری به یک قلم داده انباشتگر است. به طور پیش‌فرض، هر قلم داده انباشتگر با مقدار صفر مقداردهی اولیه می‌شود (مثلاً با memset )؛ با این حال، می‌توانید یک تابع مقداردهی اولیه بنویسید تا کار متفاوتی انجام دهد.

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

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

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

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

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

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

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

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

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

مثال: در هسته 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 ) (اختیاری): نام تابع initializer را برای این هسته کاهش مشخص می‌کند. وقتی هسته را اجرا می‌کنید، RenderScript این تابع را برای هر آیتم داده انباشتگر یک بار فراخوانی می‌کند. این تابع باید به صورت زیر تعریف شود:

    static void initializerName(accumType *accum) {  }

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

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

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator( accumulatorName ) (اجباری): نام تابع accumulator را برای این هسته کاهش مشخص می‌کند. وقتی هسته را اجرا می‌کنید، RenderScript این تابع را برای هر مختصات در ورودی(ها) یک بار فراخوانی می‌کند تا یک آیتم داده accumulator را به نحوی مطابق با ورودی(ها) به‌روزرسانی کند. این تابع باید به صورت زیر تعریف شود:

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

    accum یک اشاره‌گر به یک آیتم داده‌ی accumulator برای تغییر توسط این تابع است. in1 تا in N یک یا چند آرگومان هستند که به طور خودکار بر اساس ورودی‌های ارسالی به راه‌اندازی هسته، یک آرگومان برای هر ورودی، پر می‌شوند. تابع accumulator می‌تواند به صورت اختیاری هر یک از آرگومان‌های ویژه را بپذیرد.

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

  • combiner( combinerName )

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

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

    accum یک اشاره‌گر به یک آیتم داده‌ی انباره‌ی «مقصد» برای تغییر توسط این تابع است. other یک اشاره‌گر به یک آیتم داده‌ی انباره‌ی «مبدأ» برای «ترکیب» این تابع در *accum است.

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

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

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

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

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

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

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

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

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

    اگر نوع نتیجه‌ای متفاوت از نوع داده‌ی accumulator می‌خواهید، استفاده از تابع outconverter الزامی است.

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

چی رو نمیتونی فرض کنی؟

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. For example:

  • 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" . What does this mean? 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)
is the same as
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)
    which is the same as
    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. For example:

  • 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. For example:

  • 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.