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

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

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

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

      הערה: לפני Android 6.0 (רמת API‏ 23), יכול להיות שלקרנל מיפוי לא היה יותר מקלט Allocation אחד.

      אם אתם צריכים יותר קלט או פלט של Allocations ממה שיש לקרנל, צריך לקשר את האובייקטים האלה למשתנים גלובליים של סקריפט rs_allocation ולגשת אליהם מקרנל או מפונקציה שניתן להפעיל באמצעות rsGetElementAt_type() או rsSetElementAt_type().

      הערה: RS_KERNEL הוא פקודת מאקרו שמוגדרת באופן אוטומטי על ידי RenderScript לנוחותכם:

      #define RS_KERNEL __attribute__((kernel))

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

    • הנה דוגמה לקרנל צמצום פשוט שמסכם את ה-Elements של הקלט שלו:

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

      קרנל צמצום מורכב מפונקציה אחת או יותר שנכתבו על ידי המשתמש. הפונקציה #pragma rs reduce משמשת להגדרת הליבה על ידי ציון השם שלה (addint, בדוגמה הזו) והשמות והתפקידים של הפונקציות שמרכיבות את הליבה (פונקציית accumulator addintAccum, בדוגמה הזו). כל הפונקציות האלה חייבות להיות static. ליבת צמצום תמיד דורשת פונקציית accumulator, ויכולות להיות לה גם פונקציות אחרות, בהתאם לפעולה שרוצים שהליבה תבצע.

      פונקציית צבירה של ליבת צמצום חייבת להחזיר void ולכלול לפחות שני ארגומנטים. הארגומנט הראשון (accum בדוגמה הזו) הוא מצביע לפריט נתונים של צובר, והארגומנט השני (val בדוגמה הזו) מתמלא באופן אוטומטי על סמך הקלט Allocation שמועבר להפעלת הליבה. פריט הנתונים של המצבר נוצר על ידי זמן הריצה של RenderScript. כברירת מחדל, הוא מאותחל לאפס. כברירת מחדל, הליבה הזו פועלת על כל הקלט Allocation, עם הפעלה אחת של פונקציית הצבירה לכל Element ב-Allocation. כברירת מחדל, הערך הסופי של פריט הנתונים של המצטבר נחשב לתוצאה של ההפחתה, ומוחזר ל-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 (ברירת המחדל אם לא צוין ערך): לאפליקציות שדורשות דיוק של נקודה צפה (floating point) כפי שמתואר בתקן IEEE 754-2008.
  • ‫#pragma rs_fp_relaxed: לאפליקציות שלא דורשות תאימות מחמירה ל-IEEE 754-2008 ויכולות לפעול עם דיוק נמוך יותר. המצב הזה מאפשר להשתמש ב-flush-to-zero לערכים לא מנורמלים וב-round-towards-zero.
  • ‫#pragma rs_fp_imprecise: לאפליקציות שאין להן דרישות דיוק מחמירות. במצב הזה אפשר לעשות את כל מה שאפשר לעשות ב-rs_fp_relaxed, וגם:
    • פעולות שהתוצאה שלהן היא ‎-0.0 יכולות להחזיר ‎+0.0 במקום זאת.
    • פעולות ב-INF וב-NAN לא מוגדרות.

רוב האפליקציות יכולות להשתמש ב-rs_fp_relaxed בלי תופעות לוואי. זה יכול להועיל מאוד בארכיטקטורות מסוימות, כי יש אופטימיזציות נוספות שזמינות רק עם דיוק מופחת (כמו הוראות SIMD CPU).

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

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

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

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

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

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

כדי להשתמש בממשקי ה-API של RenderScript בספריית התמיכה:

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

      מגניב

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

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

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

    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 method‏ set_threshold(int), וכדי להגדיר משתנה rs_allocation בשם lookup, משתמשים ב-Java method‏ set_lookup(Allocation). ה-methods של set הן אסינכרוניות.
  6. מפעילים את הליבות המתאימות ואת הפונקציות שניתן להפעיל.

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

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

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

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

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

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

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

‫RenderScript ממקור יחיד

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

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

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

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

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

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

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

הסבר מעמיק על קרנלים של צמצום

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

כתיבת קרנל של הפחתה

‫#pragma rs reduce מגדיר ליבת צמצום על ידי ציון השם שלה, והשמות והתפקידים של הפונקציות שמרכיבות את הליבה. כל הפונקציות האלה חייבות להיות static. קרנל צמצום תמיד דורש פונקציית accumulator. אפשר להשמיט חלק מהפונקציות האחרות או את כולן, בהתאם לפעולה שרוצים שהקרנל יבצע.

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

המשמעות של הפריטים ב-#pragma היא:

  • ‫reduce(kernelName) (חובה): מציין שמוגדר ליבה של הפחתה. השיטה reduce_kernelName של Java תפעיל את הליבה.
  • ‫initializer(initializerName) (אופציונלי): מציין את השם של פונקציית האתחול של ליבת ההפחתה הזו. כשמפעילים את הליבה, RenderScript קורא לפונקציה הזו פעם אחת לכל פריט נתונים של צובר. צריך להגדיר את הפונקציה כך:

    static void initializerName(accumType *accum) { … }

    ‫accum הוא מצביע לפריט נתונים של צובר (accumulator) שהפונקציה הזו מאתחלת.

    אם לא תספקו פונקציית אתחול, 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)

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

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

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

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

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

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

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

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

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

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

מה אתם צריכים להבטיח?

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

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

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

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

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

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

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

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

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

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

פונקציית השילוב חייבת להיות אסוציאטיבית. כלומר, אם A,‏ B ו-C הם פריטי נתונים של accumulator שעברו אתחול על ידי פונקציית האתחול, ויכול להיות שהם הועברו לפונקציית ה-accumulator אפס פעמים או יותר, אז שני רצפי הקוד הבאים חייבים להגדיר את 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
  • הצהרה 2 זהה להצהרה B = 0
  • הצהרה 3 זהה ל-B += V, שזהה ל-B = V
  • הצהרה 4 זהה ל-A += B, שזהה ל-A += V

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

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

  • הצהרה 1 זהה ל-A = minmax(A, IndexedVal(V, X))
  • הצהרה 2 זהה לB = INITVAL
  • הצהרה 3 זהה ל-
    B = minmax(B, IndexedVal(V, X))
    מכיוון ש-B הוא הערך ההתחלתי, זהה ל-
    B = IndexedVal(V, X)
  • הצהרה 4 זהה ל-
    A = minmax(A, B)
    שזהה ל-
    A = minmax(A, IndexedVal(V, X))

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

קריאה לקרנל צמצום מקוד 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 זורק חריגה. הקרנל פועל על כל קואורדינטה במאפיינים האלה.

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

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

אם אי אפשר לייצג את כל הערכים של resultType באמצעות javaResultType, וקרנל של הפחתה מייצר ערך שלא ניתן לייצוג, הפונקציה 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. המערך הוא ייצוג שטוח: יש בו פי שניים רכיבי סקלר מאשר רכיבי וקטור עם שני רכיבים בהקצאה. השיטות 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 שמופיעים בדף הזה.