סקירה כללית של RenderScript

‫RenderScript הוא פלטפורמה להרצת משימות עתירות חישוב בביצועים גבוהים ב-Android. ‫RenderScript מיועד בעיקר לשימוש בחישובים מקבילים, אבל אפשר להשתמש בו גם לעומסי עבודה סדרתיים. זמן הריצה של RenderScript מבצע עבודה במקביל במעבדים שזמינים במכשיר, כמו מעבדי CPU מרובי ליבות ומעבדי GPU. כך תוכלו להתמקד בהגדרת אלגוריתמים במקום בתזמון עבודה. ‫RenderScript שימושי במיוחד לאפליקציות שמבצעות עיבוד תמונה, צילום חישובי או ראייה ממוחשבת.

כדי להתחיל להשתמש ב-RenderScript, צריך להבין שני מושגים עיקריים:

  • השפה עצמה היא שפה שנגזרת מ-C99 לכתיבת קוד של מחשוב עתיר ביצועים (HPC). במאמר Writing a RenderScript Kernel מוסבר איך להשתמש בו כדי לכתוב ליבות חישוב.
  • Control API משמש לניהול משך החיים של משאבי RenderScript ולשליטה בהפעלת ליבת המערכת. היא זמינה בשלוש שפות שונות: Java,‏ C++‎ ב-Android NDK ושפת הליבה עצמה שמבוססת על C99. במאמרים Using RenderScript from Java Code ו-Single-Source RenderScript מוסבר על האפשרות הראשונה והשלישית, בהתאמה.

כתיבת ליבת RenderScript

קרנל של RenderScript נמצא בדרך כלל בקובץ .rs בספרייה <project_root>/src/rs. כל קובץ .rs נקרא סקריפט. כל סקריפט מכיל קבוצה משלו של ליבות, פונקציות ומשתנים. סקריפט יכול להכיל:

  • הצהרת pragma ‏ (#pragma version(1)) שמצהירה על הגרסה של שפת הליבה של RenderScript שמשמשת בסקריפט הזה. בשלב הזה, הערך התקין היחיד הוא 1.
  • הצהרת pragma ‏ (#pragma rs java_package_name(com.example.app)) שמצהירה על שם החבילה של מחלקות Java שמשתקפות מהסקריפט הזה. שימו לב שקובץ .rs צריך להיות חלק מחבילת האפליקציה, ולא בפרויקט של ספרייה.
  • אפס או יותר פונקציות שאפשר להפעיל. פונקציה שאפשר להפעיל היא פונקציית RenderScript עם שרשור יחיד שאפשר לקרוא לה מקוד Java עם ארגומנטים שרירותיים. הן שימושיות בדרך כלל להגדרה ראשונית או לחישובים סדרתיים בתוך צינור עיבוד גדול יותר.
  • אפס או יותר משתנים גלובליים של סקריפט. משתנה גלובלי בסקריפט דומה למשתנה גלובלי ב-C. אפשר לגשת למשתנים גלובליים של סקריפט מקוד Java, והם משמשים לעיתים קרובות להעברת פרמטרים לליבות של 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 מבצעת בדיקות כדי לוודא שלכל הקצאות הקלט והפלט יש את אותם ממדים, ושהסוגים של הקצאות הקלט והפלט תואמים לאב-טיפוס של הליבה. אם אחת מהבדיקות האלה נכשלת, RenderScript זורק חריגה.Element

      הערה: לפני 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. כברירת מחדל, הערך הסופי של פריט הנתונים של המצבר נחשב לתוצאה של הצמצום ומוחזר ל-Java. סביבת זמן הריצה של RenderScript בודקת כדי לוודא שהסוג 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 מריץ אותה כשהסקריפט מופעל בפעם הראשונה. כך אפשר לבצע חישובים מסוימים באופן אוטומטי בזמן יצירת הסקריפט.
  • אפס או יותר פונקציות ומשתנים גלובליים סטטיים של סקריפט. משתנה גלובלי סטטי בסקריפט שווה למשתנה גלובלי בסקריפט, אלא שלא ניתן לגשת אליו מקוד Java. פונקציה סטטית היא פונקציית C רגילה שאפשר להפעיל מכל פונקציית ליבה או פונקציה שאפשר להפעיל בסקריפט, אבל היא לא חשופה ל-Java API. אם אין צורך לגשת לסקריפט גלובלי או לפונקציה מקוד Java, מומלץ מאוד להצהיר עליהם כ-static.

הגדרת רמת הדיוק של נקודה צפה (floating-point)

אתם יכולים לשלוט ברמת הדיוק הנדרשת של נקודה צפה בסקריפט. האפשרות הזו שימושית אם לא נדרש תקן 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).

גישה לממשקי RenderScript API מ-Java

כשמפתחים אפליקציית Android שמשתמשת ב-RenderScript, אפשר לגשת ל-API שלה מ-Java באחת משתי דרכים:

  • 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 הזה מאפשר לאפליקציה לפעול ביותר מכשירים מאשר אם משתמשים בממשקי ה-API המקוריים (android.renderscript).
  • חלק מהתכונות של RenderScript לא זמינות דרך ממשקי ה-API של ספריית התמיכה.
  • אם משתמשים בממשקי Support Library APIs, חבילות ה-APK שמתקבלות גדולות יותר (יכול להיות שהרבה יותר) מאלה שמתקבלות אם משתמשים בממשקי API מקוריים (android.renderscript).

שימוש בממשקי API של RenderScript Support Library

כדי להשתמש בממשקי API של RenderScript בספריית התמיכה, צריך להגדיר את סביבת הפיתוח כך שתהיה לה גישה אליהם. כדי להשתמש בממשקי ה-API האלה, צריך את כלי Android SDK הבאים:

  • ‫Android SDK Tools מגרסה 22.2 ואילך
  • ‫Android SDK Build-tools מגרסה 18.1.0 ואילך

הערה: החל מ-Android SDK Build-tools 24.0.0, אין יותר תמיכה ב-Android 2.2 (רמת API‏ 8).

אפשר לבדוק ולעדכן את הגרסה המותקנת של הכלים האלה בAndroid SDK Manager.

כדי להשתמש ב-Support Library RenderScript APIs:

  1. מוודאים שמותקנת הגרסה הנדרשת של Android SDK.
  2. מעדכנים את ההגדרות של תהליך הבנייה של Android כדי לכלול את ההגדרות של RenderScript:
    • פותחים את הקובץ build.gradle בתיקיית האפליקציה של מודול האפליקציה.
    • מוסיפים לקובץ את הגדרות RenderScript הבאות:

      Groovy

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

      Kotlin

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

      ההגדרות שמופיעות למעלה שולטות בהתנהגות ספציפית בתהליך הבנייה של Android:

      • renderscriptTargetApi – מציין את גרסת ה-bytecode שתיצור. מומלץ להגדיר את הערך הזה לרמת ה-API הנמוכה ביותר שיכולה לספק את כל הפונקציונליות שבה אתם משתמשים, ולהגדיר את renderscriptSupportModeEnabled ל-true. הערכים התקינים להגדרה הזו הם כל ערך של מספר שלם מ-11 ועד לרמת ה-API העדכנית ביותר. אם גרסת ה-SDK המינימלית שצוינה במניפסט האפליקציה מוגדרת לערך אחר, המערכת מתעלמת מהערך הזה ומשתמשת בערך היעד בקובץ ה-build כדי להגדיר את גרסת ה-SDK המינימלית.
      • renderscriptSupportModeEnabled – מציין שאם המכשיר שבו פועל קוד הבייט שנוצר לא תומך בגרסת היעד, המערכת תחזור לגרסה תואמת.
  3. במחלקות האפליקציה שמשתמשות ב-RenderScript, מוסיפים ייבוא למחלקות Support Library:

    Kotlin

    import android.support.v8.renderscript.*

    Java

    import android.support.v8.renderscript.*;

שימוש ב-RenderScript מקוד Java או Kotlin

השימוש ב-RenderScript מקוד Java או 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 מאפשרים להעביר מערכים מקוד Java לקוד RenderScript ולהיפך. בדרך כלל יוצרים אובייקטים מסוג Allocation באמצעות createTyped() או createFromBitmap().
  3. יוצרים את כל הסקריפטים שצריך. יש שני סוגים של סקריפטים שזמינים לכם כשאתם משתמשים ב-RenderScript:
    • ScriptC: אלה סקריפטים שהוגדרו על ידי המשתמש, כפי שמתואר בקטע כתיבת ליבת RenderScript למעלה. לכל סקריפט יש מחלקה ב-Java שמשתקפת על ידי קומפיילר RenderScript כדי לאפשר גישה קלה לסקריפט מקוד Java; שם המחלקה הוא ScriptC_filename. לדוגמה, אם ליבת המיפוי שלמעלה נמצאת ב-invert.rs והקשר של RenderScript כבר נמצא ב-mRenderScript, קוד ה-Java או ה-Kotlin ליצירת מופע של הסקריפט יהיה:

      Kotlin

      val invert = ScriptC_invert(renderScript)

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
    • ScriptIntrinsic: אלה ליבות RenderScript מובנות לפעולות נפוצות, כמו טשטוש גאוסיאני, קונבולוציה ומיזוג תמונות. מידע נוסף זמין במאמר בנושא מחלקות משנה של ScriptIntrinsic.
  4. מאכלסים את הכרטיסייה 'הקצאות' בנתונים. מלבד הקצאות שנוצרו באמצעות createFromBitmap(), הקצאה מאוכלסת בנתונים ריקים כשהיא נוצרת לראשונה. כדי לאכלס הקצאה, משתמשים באחת משיטות ההעתקה ב-Allocation. השיטות של 'העתקה' הן סינכרוניות.
  5. מגדירים את המשתנים הגלובליים של הסקריפט שנדרשים. אפשר להגדיר משתנים גלובליים באמצעות שיטות באותה מחלקה ScriptC_filename שנקראת set_globalname. לדוגמה, כדי להגדיר משתנה int בשם threshold, משתמשים בשיטת Java‏ set_threshold(int). כדי להגדיר משתנה rs_allocation בשם lookup, משתמשים בשיטת Java‏ set_lookup(Allocation). השיטות של set הן אסינכרוניות.
  6. מפעילים את ליבות הקרנל המתאימות ואת הפונקציות שאפשר להפעיל.

    השיטות להפעלת ליבה נתונה משתקפות באותה מחלקה ScriptC_filename עם שיטות בשם forEach_mappingKernelName() או reduce_reductionKernelName(). ההשקות האלה הן אסינכרוניות. בהתאם לארגומנטים של הליבה, השיטה מקבלת הקצאה אחת או יותר, שלכולן צריכים להיות אותם ממדים. כברירת מחדל, ליבת (kernel) מופעלת על כל קואורדינטה במאפיינים האלה. כדי להפעיל ליבה על קבוצת משנה של הקואורדינטות האלה, צריך להעביר Script.LaunchOptions מתאים כארגומנט האחרון לשיטה forEach או reduce.

    מפעילים פונקציות שאפשר להפעיל באמצעות השיטות invoke_functionName שמשתקפות באותה מחלקה ScriptC_filename. ההשקות האלה הן אסינכרוניות.

  7. אחזור נתונים מאובייקטים מסוג Allocation ומאובייקטים מסוג javaFutureType. כדי לגשת לנתונים מ-Allocation מקוד Java, צריך להעתיק את הנתונים האלה בחזרה ל-Java באמצעות אחת משיטות ההעתקה ב-Allocation. כדי לקבל את התוצאה של ליבת צמצום, צריך להשתמש בשיטה javaFutureType.get(). השיטות 'copy' ו-get() הן סינכרוניות.
  8. מפרקים את ההקשר של RenderScript. אפשר להרוס את ההקשר של RenderScript באמצעות destroy() או על ידי מתן אפשרות לאובייקט ההקשר של RenderScript לעבור איסוף אשפה. כתוצאה מכך, כל שימוש נוסף באובייקט ששייך להקשר הזה יגרום להחזרת חריגה.

מודל ביצוע אסינכרוני

השיטות forEach,‏ invoke,‏ reduce ו-set הן אסינכרוניות – כל אחת מהן עשויה לחזור ל-Java לפני השלמת הפעולה המבוקשת. עם זאת, הפעולות הבודדות מבוצעות בסדר שבו הן מופעלות.

המחלקות Allocation מספקות שיטות 'העתקה' להעתקת נתונים אל Allocations וממנו. השיטה 'copy' היא סינכרונית, והיא עוברת סריאליזציה ביחס לכל אחת מהפעולות האסינכרוניות שלמעלה שמשפיעות על אותה הקצאה.

המחלקות javaFutureType שמשקפות את התוצאה מספקות שיטה get() לקבלת התוצאה של צמצום. ‫get() הוא סינכרוני, והוא עובר סריאליזציה ביחס לצמצום (שהוא אסינכרוני).

‫Single-Source RenderScript

ב-Android 7.0 (רמת API‏ 24) הושקה תכונת תכנות חדשה בשם Single-Source RenderScript (מקור יחיד של RenderScript), שבה ליבות מופעלות מהסקריפט שבו הן מוגדרות, ולא מ-Java. הגישה הזו מוגבלת כרגע למיפוי ליבות (kernels), שנקראות בפשטות 'ליבות' בקטע הזה כדי לשמור על תמציתיות. התכונה החדשה הזו תומכת גם ביצירת הקצאות מהסוג rs_allocation מתוך הסקריפט. עכשיו אפשר להטמיע אלגוריתם שלם רק בסקריפט, גם אם נדרשות כמה הפעלות של ליבת המערכת. היתרון הוא כפול: קוד קריא יותר, כי הוא שומר על ההטמעה של אלגוריתם בשפה אחת, וקוד מהיר יותר, כי יש פחות מעברים בין Java ל-RenderScript בכמה הפעלות של ליבת המערכת.

ב-RenderScript עם מקור יחיד, כותבים ליבות כמו שמתואר במאמר כתיבת ליבת RenderScript. אחר כך כותבים פונקציה שאפשר להפעיל ולקרוא לה rsForEach() כדי להפעיל אותן. ה-API הזה מקבל פונקציית ליבה כפרמטר הראשון, ואחריו הקצאות של קלט ופלט. ‫API דומה rsForEachWithOptions() מקבל ארגומנט נוסף מסוג rs_script_call_t, שמציין קבוצת משנה של הרכיבים מהקצאות הקלט והפלט לעיבוד של פונקציית הליבה.

כדי להתחיל חישוב של RenderScript, מפעילים את הפונקציה הניתנת להפעלה מ-Java. פועלים לפי השלבים במאמר שימוש ב-RenderScript מקוד Java. בשלב הפעלת ליבות מתאימות, קוראים לפונקציה שאפשר להפעיל באמצעות invoke_function_name(), מה שיפעיל את כל החישוב, כולל הפעלת ליבות.

לרוב, צריך הקצאות כדי לשמור ולהעביר תוצאות ביניים מהפעלת ליבה אחת להפעלת ליבה אחרת. אפשר ליצור אותם באמצעות rsCreateAllocation(). אחת מהצורות הקלות לשימוש של ה-API הזה היא rsCreateAllocation_<T><W>(…), כאשר T הוא סוג הנתונים של רכיב, ו-W הוא רוחב הווקטור של הרכיב. ה-API מקבל את הגדלים בממדים X,‏ Y ו-Z כארגומנטים. במקרה של הקצאות חד-ממדיות או דו-ממדיות, אפשר להשמיט את הגודל של המאפיין Y או Z. לדוגמה, rsCreateAllocation_uchar4(16384) יוצר הקצאה חד-ממדית של 16,384 רכיבים, שכל אחד מהם הוא מסוג uchar4.

המערכת מנהלת את ההקצאות באופן אוטומטי. אין צורך לשחרר אותם באופן מפורש. עם זאת, אפשר לקרוא ל- rsClearObject(rs_allocation* alloc) כדי לציין שאין יותר צורך בטיפול alloc בהקצאה הבסיסית, כדי שהמערכת תוכל לפנות משאבים מוקדם ככל האפשר.

בקטע Writing a RenderScript Kernel יש דוגמה לליבת מערכת שמבצעת היפוך של תמונה. בדוגמה הבאה מוסבר איך להחיל יותר מאפקט אחד על תמונה באמצעות 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() מ-Java או מ-Kotlin באופן הבא:

Kotlin

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

Java

// File SingleSource.java

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

בדוגמה הזו אפשר לראות איך אפשר להטמיע אלגוריתם שכולל שני הפעלות של ליבת המערכת, באופן מלא בשפת RenderScript עצמה. בלי Single-Source RenderScript, תצטרכו להפעיל את שני הגרעינים מקוד Java, להפריד בין הפעלות הגרעינים לבין הגדרות הגרעינים, ויהיה קשה יותר להבין את האלגוריתם כולו. קוד ה-RenderScript ממקור יחיד לא רק קל יותר לקריאה, אלא גם מבטל את המעבר בין Java לבין הסקריפט בהפעלות של ליבת המערכת. אלגוריתמים איטרטיביים מסוימים עשויים להפעיל ליבות מאות פעמים, ולכן התקורה של מעבר כזה היא משמעותית.

משתנים גלובליים בסקריפט

משתנה גלובלי בסקריפט הוא משתנה גלובלי רגיל שאינו static בסקריפט (קובץ .rs). למשתנה גלובלי של סקריפט בשם var שמוגדר בקובץ filename.rs, תהיה שיטה get_var שמשתקפת במחלקה ScriptC_filename. אלא אם הערך של global הוא const, תהיה גם שיטה set_var.

למשתנה גלובלי נתון בסקריפט יש שני ערכים נפרדים – ערך Java וערך סקריפט. הערכים האלה מתנהגים באופן הבא:

  • אם ל-var יש מאתחל סטטי בסקריפט, הוא מציין את הערך ההתחלתי של var גם ב-Java וגם בסקריפט. אחרת, הערך הראשוני הוא אפס.
  • גישות אל var בתוך הסקריפט קוראות וכותבות את ערך הסקריפט שלו.
  • השיטה get_var קוראת את הערך של Java.
  • השיטה set_var (אם היא קיימת) כותבת את הערך של Java באופן מיידי, ואת הערך של הסקריפט באופן אסינכרוני.

הערה: המשמעות היא שערכים שנכתבים לגלובל מתוך סקריפט לא גלויים ל-Java, למעט מאתחלים סטטיים בסקריפט.

הסבר מפורט על ליבות להפחתת מימדים

צמצום הוא תהליך של שילוב אוסף נתונים לערך יחיד. זוהי פרימיטיב שימושית בתכנות מקבילי, עם יישומים כמו:

  • חישוב הסכום או המכפלה של כל הנתונים
  • חישוב פעולות לוגיות (and, or, xor) על כל הנתונים
  • חיפוש הערך המינימלי או המקסימלי בנתונים
  • חיפוש ערך ספציפי או הקואורדינטה של ערך ספציפי בנתונים

ב-Android 7.0 (רמת API‏ 24) ואילך, RenderScript תומך בליבות של צמצום כדי לאפשר אלגוריתמים יעילים של צמצום שנכתבו על ידי המשתמש. אפשר להפעיל ליבות של הפחתה על קלט עם מימד אחד, שניים או שלושה.

בדוגמה שלמעלה מוצג ליבת צמצום פשוטה של addint. הנה ליבת צמצום מסובכת יותר של 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 יוצרת משתנה אחד או יותר בשם accumulator data items כדי לשמור את המצב של תהליך הצמצום. סביבת זמן הריצה של RenderScript בוחרת את מספר פריטי הנתונים של המצבר באופן שממקסם את הביצועים. הסוג של פריטי הנתונים של המצבר (accumType) נקבע על ידי פונקציית המצבר של ליבת המערכת – הארגומנט הראשון של הפונקציה הזו הוא מצביע לפריט נתונים של המצבר. כברירת מחדל, כל פריט נתונים של צובר מאותחל לאפס (כמו ב-memset), אבל אפשר לכתוב פונקציית אתחול כדי לבצע פעולה אחרת.

דוגמה: בגרעין addint, פריטי הנתונים של האוגר (מסוג int) משמשים לסיכום ערכי הקלט. אין פונקציית אתחול, ולכן כל פריט נתונים של צובר מאותחל לאפס.

דוגמה: בגרעין findMinAndMax, פריטי הנתונים של המצבר (מהסוג MinAndMax) משמשים למעקב אחרי הערכים המינימליים והמקסימליים שנמצאו עד כה. יש פונקציית אתחול להגדרת הערכים האלה ל-LONG_MAX ול-LONG_MIN, בהתאמה, ולהגדרת המיקומים של הערכים האלה ל-1-, מה שמציין שהערכים לא נמצאים בפועל בחלק (הריק) של הקלט שעבר עיבוד.

‫RenderScript קורא לפונקציית האקומולטור פעם אחת לכל קואורדינטה בקלט. בדרך כלל, הפונקציה צריכה לעדכן את פריט הנתונים של המצבר בצורה כלשהי בהתאם לקלט.

דוגמה: בגרעין addint, פונקציית המצבר מוסיפה את הערך של רכיב קלט לפריט נתוני המצבר.

דוגמה: בגרעין findMinAndMax, פונקציית המצבר בודקת אם הערך של רכיב קלט קטן מהערך המינימלי שמתועד בפריט הנתונים של המצבר או שווה לו, ו/או גדול מהערך המקסימלי שמתועד בפריט הנתונים של המצבר או שווה לו, ומעדכנת את פריט הנתונים של המצבר בהתאם.

אחרי שקוראים לפונקציית המצבר פעם אחת לכל קואורדינטה בקלט, RenderScript צריך לשלב את פריטי הנתונים של המצבר לפריט נתונים אחד של המצבר. אפשר לכתוב פונקציית combiner כדי לעשות את זה. אם לפונקציית המצבר יש קלט יחיד ואין לה ארגומנטים מיוחדים, לא צריך לכתוב פונקציית שילוב. RenderScript ישתמש בפונקציית המצבר כדי לשלב את פריטי הנתונים של המצבר. (אפשר עדיין לכתוב פונקציית שילוב אם התנהגות ברירת המחדל הזו לא מתאימה לכם).

דוגמה: בגרעין addint אין פונקציית צירוף, ולכן נעשה שימוש בפונקציית האקומולטור. זהו ההתנהגות הנכונה, כי אם מפצלים אוסף של ערכים לשני חלקים, וסוכמים את הערכים בשני החלקים בנפרד, סכום שני הסכומים שווה לסכום של כל הערכים באוסף.

דוגמה: בגרעין findMinAndMax, פונקציית ה-combiner בודקת אם הערך המינימלי שמתועד בפריט הנתונים של המצבר source‏ *val קטן מהערך המינימלי שמתועד בפריט הנתונים של המצבר destination‏ *accum, ומעדכנת את *accum בהתאם. הוא עושה עבודה דומה גם לגבי הערך המקסימלי. הפעולה הזו מעדכנת את *accum למצב שהיה מתקבל אם כל ערכי הקלט היו מצטברים ב-*accum ולא אם חלק מהם היו מצטברים ב-*accum וחלק ב-*val.

אחרי שכל פריטי הנתונים של המצבר משולבים, RenderScript קובע את התוצאה של הצמצום כדי להחזיר ל-Java. אפשר לכתוב פונקציית 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) (חובה): מציין שמוגדרת ליבת צמצום. שיטת Java משוקפת 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)‫ (חובה): מציין את השם של פונקציית האקומולטור עבור ליבת הרדוקציה הזו. כשמפעילים את הליבה, RenderScript קורא לפונקציה הזו פעם אחת לכל קואורדינטה בקלט, כדי לעדכן פריט נתונים של צובר בדרך כלשהי בהתאם לקלט. הפונקציה צריכה להיות מוגדרת כך:

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

    accum הוא מצביע לפריט נתונים של צובר שהפונקציה הזו יכולה לשנות. ‫in1 עד inN הם ארגומנט אחד או יותר שמולאו באופן אוטומטי על סמך הקלט שהועבר להפעלת הליבה, ארגומנט אחד לכל קלט. פונקציית האקומולטור יכולה לקבל גם ארגומנטים מיוחדים.

    דוגמה לליבה עם כמה קלטים היא dotProduct.

  • combiner(combinerName)

    ‫(optional): מציין את השם של פונקציית השילוב עבור הליבה של הפחתת הערכים הזו. אחרי ש-RenderScript קורא לפונקציית האקומולטור פעם אחת לכל קואורדינטה בקלט, הוא קורא לפונקציה הזו כמה פעמים שצריך כדי לשלב את כל פריטי הנתונים של האקומולטור לפריט נתונים אחד של האקומולטור. הפונקציה צריכה להיות מוגדרת כך:

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

    accum הוא מצביע לפריט נתונים של צובר 'יעד' שהפונקציה הזו תשנה. ‫other הוא מצביע לפריט נתונים של צובר 'מקור' שהפונקציה הזו 'משלבת' ב-*accum.

    הערה: יכול להיות שהמשתנים *accum, *other או שניהם אותחלו אבל אף פעם לא הועברו לפונקציית האקומולטור. כלומר, אחד מהם או שניהם אף פעם לא עודכנו בהתאם לנתוני הקלט. לדוגמה, בגרעין findMinAndMax, פונקציית ה-combiner‏ fMMCombiner בודקת במפורש אם הערך הוא idx < 0, כי זה מציין פריט נתונים של מצבר כזה, שהערך שלו הוא INITVAL.

    אם לא מספקים פונקציית שילוב, RenderScript משתמש בפונקציית האקומולטור במקומה, כאילו הייתה פונקציית שילוב שנראית כך:

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

    פונקציית שילוב היא חובה אם לליבה יש יותר מקלט אחד, אם סוג נתוני הקלט לא זהה לסוג נתוני המצבר, או אם פונקציית המצבר מקבלת ארגומנטים מיוחדים אחד או יותר.

  • outconverter(outconverterName) (אופציונלי): מציין את השם של פונקציית ה-outconverter עבור ליבת הצמצום הזו. אחרי ש-RenderScript משלב את כל פריטי הנתונים של המצבר, הוא קורא לפונקציה הזו כדי לקבוע את תוצאת הצמצום שצריך להחזיר ל-Java. הפונקציה צריכה להיות מוגדרת כך:

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

    result הוא מצביע לפריט נתונים של תוצאה (שהוקצה אבל לא אותחל על ידי זמן הריצה של RenderScript) שהפונקציה הזו מאתחלת עם התוצאה של הצמצום. resultType הוא הסוג של פריט הנתונים הזה, שלא חייב להיות זהה ל-accumType. ‫accum הוא מצביע לפריט הנתונים הסופי של המצבר שמחושב על ידי פונקציית הצירוף.

    אם לא מספקים פונקציית outconverter, ‏ RenderScript מעתיקה את פריט הנתונים הסופי של המצבר לפריט הנתונים של התוצאה, כאילו הייתה פונקציית outconverter שנראית כך:

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

    אם רוצים סוג תוצאה שונה מסוג הנתונים של המצבר, חובה להשתמש בפונקציה outconverter.

שימו לב: לליבה יש סוגי קלט, סוג של פריט נתונים מצטבר וסוג תוצאה, ואף אחד מהם לא צריך להיות זהה. לדוגמה, בגרעין findMinAndMax, סוג הקלט long, סוג פריט הנתונים של צובר MinAndMax וסוג התוצאה int2 שונים זה מזה.

מה אסור להניח?

אסור להסתמך על מספר פריטי הנתונים של המצבר שנוצרו על ידי RenderScript עבור הפעלה נתונה של ליבת המערכת. אין ערובה לכך ששתי הפעלות של אותו ליבה עם אותם נתוני קלט ייצרו את אותו מספר של פריטי נתונים של צובר.

אסור להסתמך על הסדר שבו RenderScript קורא לפונקציות initializer,‏ accumulator ו-combiner. יכול להיות שהוא אפילו יקרא לחלק מהן במקביל. אין ערובה לכך ששתי הפעלות של אותו ליבה עם אותו קלט יפעלו באותו סדר. הדבר היחיד שמובטח הוא שרק פונקציית האתחול תראה אי פעם פריט נתונים של צובר לא מאותחל. לדוגמה:

  • אין ערובה לכך שכל פריטי הנתונים של המצבר יאותחלו לפני הקריאה לפונקציית המצבר, למרות שהקריאה תתבצע רק לפריט נתונים מאותחל של המצבר.
  • אין ערובה לסדר שבו רכיבי הקלט מועברים לפונקציית המצבר.
  • אין ערובה לכך שהפונקציה המצטברת נקראה עבור כל רכיבי הקלט לפני שהפונקציה המשולבת נקראת.

אחת מההשלכות של זה היא שגרעין findMinAndMax לא דטרמיניסטי: אם הקלט מכיל יותר ממופע אחד של אותו ערך מינימלי או מקסימלי, אין לכם דרך לדעת איזה מופע הגרעין ימצא.

מה צריך להבטיח?

מערכת RenderScript יכולה לבחור לבצע ליבת מערכת בדרכים רבות ושונות, ולכן צריך לפעול לפי כללים מסוימים כדי לוודא שהליבה מתנהגת כמו שרוצים. אם לא תקפידו על הכללים האלה, יכול להיות שתקבלו תוצאות שגויות, התנהגות לא דטרמיניסטית או שגיאות בזמן ריצה.

בכללים שלמטה מצוין לעיתים קרובות ששני פריטי נתונים של צובר חייבים להיות בעלי אותו ערך. מה זה אומר? זה תלוי במה שרוצים שהליבה תעשה. לגבי צמצום מתמטי כמו addint, בדרך כלל הגיוני ש'זהה' יהיה שווה ערך לשוויון מתמטי. בחיפוש מסוג 'בחירת כל האפשרויות', כמו findMinAndMax (חיפוש המיקום של ערכי הקלט המינימליים והמקסימליים), שבו יכול להיות יותר ממופע אחד של ערכי קלט זהים, כל המיקומים של ערך קלט נתון נחשבים 'זהים'. אפשר לכתוב ליבת הפעלה דומה לזו של 'מציאת המיקום של ערכי הקלט המינימליים והמקסימליים הכי שמאליים', שבה (לדוגמה) ערך מינימלי במיקום 100 עדיף על ערך מינימלי זהה במיקום 200. בליבה הזו, 'זהה' יתייחס למיקום זהה, ולא רק לערך זהה, ופונקציות האקומולטור והקומביינר יהיו שונות מאלה של findMinAndMax.

פונקציית האתחול חייבת ליצור ערך זהות. כלומר, אם I ו-A הם פריטי נתונים של צובר שהופעלו על ידי פונקציית ההפעלה, ואם I מעולם לא הועבר לפונקציית הצובר (אבל יכול להיות ש-A הועבר), אז
  • combinerName(&A, &I) חייב להיות A זהה
  • הערך של combinerName(&I, &A) חייב להיות זהה לערך של AI

דוגמה: בגרעין addint, פריט נתונים של צובר מאותחל לאפס. הפונקציה המשולבת של ליבת ה-kernel הזו מבצעת חיבור; אפס הוא ערך הזהות של החיבור.

דוגמה: בגרעין findMinAndMax, פריט נתונים של צובר מאותחל ל-INITVAL.

  • הפונקציה fMMCombiner(&A, &I) משאירה את A ללא שינוי, כי I הוא INITVAL.
  • fMMCombiner(&I, &A) מגדיר את I לערך A, כי I הוא INITVAL.

לכן, INITVAL הוא אכן ערך זהות.

פונקציית השילוב חייבת להיות קומוטטיבית. כלומר, אם A ו-B הם פריטי נתונים של צובר שאותחלו על ידי פונקציית האתחול, ואולי הועברו לפונקציית הצובר אפס פעמים או יותר, אז combinerName(&A, &B) חייבת להגדיר את A לאותו ערך שמוגדר על ידי combinerName(&B, &A) עבור B.

דוגמה: בגרעין addint, פונקציית הצירוף מוסיפה את שני הערכים של פריטי הנתונים של המצבר. פעולת החיבור היא קומוטטיבית.

דוגמה: בגרעין findMinAndMax, fMMCombiner(&A, &B) זהה ל- A = minmax(A, B), ו-minmax הוא קומוטטיבי, ולכן גם fMMCombiner הוא קומוטטיבי.

פונקציית השילוב חייבת להיות אסוציאטיבית. כלומר, אם A, B ו-C הם פריטי נתונים של מצבר שאותחלו על ידי פונקציית האתחול, ואולי הועברו לפונקציית המצבר אפס פעמים או יותר, אז שתי רצפי הקוד הבאים חייבים להגדיר את A לאותו ערך:

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

דוגמה: בגרעין addint, פונקציית הצירוף מוסיפה את שני הערכים של פריטי הנתונים של המצבר:

  • 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

פעולת החיבור היא אסוציאטיבית, ולכן גם פונקציית הצירוף היא אסוציאטיבית.

דוגמה: בגרעין 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 היא אסוציאטיבית.

הפונקציה המצטברת והפונקציה המשולבת חייבות יחד לפעול לפי כלל הקיפול הבסיסי. כלומר, אם A ו-B הם פריטי נתונים של צובר, A אותחל על ידי פונקציית האתחול ויכול להיות שהוא הועבר לפונקציית הצובר אפס פעמים או יותר, B לא אותחל, ו-args הוא רשימת ארגומנטים של קלט וארגומנטים מיוחדים לקריאה מסוימת לפונקציית הצובר, אז שתי רצפי הקוד הבאים חייבים להגדיר את A לאותו ערך:

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

דוגמה: בגרעין addint, עבור ערך קלט V:

  • ההצהרה 1 זהה לA += V
  • ההצהרה השנייה זהה לB = 0
  • ההצהרה 3 זהה ל-B += V, שזהה ל-B = V
  • ההצהרה 4 זהה להצהרה A += B, שזהה להצהרה A += V

ההצהרות 1 ו-4 מגדירות את A לאותו ערך, ולכן ליבת ה-kernel הזו פועלת לפי כלל הקיפול הבסיסי.

דוגמה: בגרעין findMinAndMax, עבור ערך קלט V בקואורדינטה X:

  • ההצהרה 1 זהה לA = minmax(A, IndexedVal(V, X))
  • ההצהרה השנייה זהה ל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 לאותו ערך, ולכן ליבת ה-kernel הזו פועלת לפי כלל הקיפול הבסיסי.

הפעלת ליבת צמצום מקוד Java

עבור ליבת צמצום בשם kernelName שמוגדרת בקובץ filename.rs, יש שלוש שיטות שמשתקפות במחלקה ScriptC_filename:

Kotlin

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

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

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

Java

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

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

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

הנה כמה דוגמאות לקריאה לליבת addint:

Kotlin

val script = ScriptC_example(renderScript)

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

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

Java

ScriptC_example script = new ScriptC_example(renderScript);

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

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

לשיטה 1 יש ארגומנט קלט Allocation אחד לכל ארגומנט קלט בפונקציית הצבירה של ליבת המערכת. סביבת זמן הריצה של RenderScript בודקת כדי לוודא שלכל הקצאות הקלט יש את אותם ממדים, ושהסוג Element של כל אחת מהקצאות הקלט תואם לסוג של ארגומנט הקלט התואם של אב הטיפוס של פונקציית המצבר. אם אחת מהבדיקות האלה נכשלת, RenderScript זורק חריג. הליבה פועלת על כל קואורדינטה בממדים האלה.

Method 2 זהה ל-Method 1, אבל ב-Method 2 יש ארגומנט נוסף sc שאפשר להשתמש בו כדי להגביל את הביצוע של הליבה לקבוצת משנה של הקואורדינטות.

שיטה 3 זהה לשיטה 1, אלא שבמקום לקבל קלט של הקצאה היא מקבלת קלט של מערך Java. הנוחות הזו חוסכת לכם את הצורך לכתוב קוד כדי ליצור הקצאה באופן מפורש ולהעתיק אליה נתונים ממערך Java. עם זאת, שימוש בשיטה 3 במקום בשיטה 1 לא משפר את הביצועים של הקוד. לכל מערך קלט, שיטה 3 יוצרת הקצאה זמנית חד-ממדית עם סוג Element מתאים ועם setAutoPadding(boolean) מופעל, ומעתיקה את המערך להקצאה כאילו באמצעות שיטת copyFrom() המתאימה של Allocation. לאחר מכן, הוא קורא ל-Method 1 ומעביר את ההקצאות הזמניות האלה.

הערה: אם האפליקציה שלכם תבצע כמה קריאות לליבת המערכת עם אותה מערך, או עם מערכים שונים באותם ממדים ובאותו סוג רכיב, יכול להיות שתצליחו לשפר את הביצועים אם תיצרו, תאכלסו ותשתמשו מחדש בהקצאות בעצמכם, במקום להשתמש בשיטה 3.

javaFutureType, סוג ההחזרה של שיטות ההפחתה המשתקפות, הוא מחלקה סטטית מקוננת משתקפת בתוך המחלקה ScriptC_filename. הוא מייצג את התוצאה העתידית של הפעלה של ליבת הפחתה. כדי לקבל את התוצאה בפועל של ההרצה, קוראים ל-method‏ get() של המחלקה הזו, שמחזירה ערך מסוג javaResultType. ‫get() היא סינכרונית.

Kotlin

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

Java

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

הערך של javaResultType נקבע לפי הערך של resultType של הפונקציה outconverter. אלא אם resultType הוא סוג לא חתום (סקלר, וקטור או מערך), javaResultType הוא סוג Java שתואם ישירות. אם resultType הוא סוג לא חתום ויש סוג חתום גדול יותר של Java, אז javaResultType הוא סוג חתום גדול יותר של Java; אחרת, הוא סוג Java התואם ישירות. לדוגמה:

  • אם הערך של resultType הוא 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 הוא סוג התוצאה העתידי שמתאים ל-resultType של הפונקציה outconverter.

  • אם resultType הוא לא סוג מערך, אז javaFutureType הוא result_resultType.
  • אם resultType הוא מערך באורך Count עם רכיבים מהסוג memberType, אז javaFutureType הוא resultArrayCount_memberType.

לדוגמה:

Kotlin

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

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

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

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

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

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

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

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

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

Java

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

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

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

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

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

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

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

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

אם javaResultType הוא סוג אובייקט (כולל סוג מערך), כל קריאה ל-javaFutureType.get() באותו מופע תחזיר את אותו אובייקט.

אם javaResultType לא יכול לייצג את כל הערכים של הסוג resultType, וליבת צמצום מייצרת ערך שלא ניתן לייצוג, אז הפונקציה javaFutureType.get() זורקת חריגה.

שיטה 3 ו-devecSiInXType

devecSiInXType הוא סוג Java שמתאים ל-inXType של הארגומנט המתאים של פונקציית המצבר. אלא אם inXType הוא סוג לא חתום או סוג וקטורי, devecSiInXType הוא סוג Java התואם ישירות. אם inXType הוא סוג סקלרי לא חתום, אז devecSiInXType הוא סוג Java שמתאים ישירות לסוג הסקלרי החתום באותו גודל. אם inXType הוא סוג וקטור חתום, אז devecSiInXType הוא סוג Java שמתאים ישירות לסוג רכיב הווקטור. אם inXType הוא סוג וקטור לא חתום, אז devecSiInXType הוא סוג Java שתואם ישירות לסוג הסקלר החתום באותו גודל כמו סוג רכיב הווקטור. לדוגמה:

  • אם הערך של inXType הוא int, אז הערך של devecSiInXType הוא int.
  • אם הערך של inXType הוא int2, אז הערך של devecSiInXType הוא int. המערך הוא ייצוג שטוח: יש בו פי שניים רכיבי סקלר מאשר ברכיבי הווקטור של ההקצאה, שכוללים 2 רכיבים. השיטה הזו זהה לשיטות copyFrom() של Allocation.
  • אם הערך של inXType הוא uint, אז הערך של deviceSiInXType הוא int. ערך חתום במערך Java מתפרש כערך לא חתום של אותו דפוס ביטים בהקצאה. השיטה הזו זהה לשיטות copyFrom() של Allocation.
  • אם הערך של inXType הוא uint2, אז הערך של deviceSiInXType הוא int. השילוב הזה נוצר על סמך האופן שבו מטפלים ב-int2 וב-uint: המערך הוא ייצוג שטוח, וערכים חתומים של מערך Java מתפרשים כערכים לא חתומים של Element ב-RenderScript.

הערה: בשיטה 3, הטיפול בסוגי הקלט שונה מהטיפול בסוגי התוצאות:

  • קלט וקטורי של סקריפט משוטח בצד Java, אבל תוצאה וקטורית של סקריפט לא משוטחת.
  • קלט לא חתום של סקריפט מיוצג כקלט חתום באותו גודל בצד של Java, ואילו תוצאה לא חתומה של סקריפט מיוצגת כסוג חתום מורחב בצד של Java (למעט במקרה של 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 שמוסבר עליהם בדף הזה.