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

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

שימוש בממשקי API של ספריית התמיכה של RenderScript

כדי להשתמש בממשקי RenderScript API של Support Library, צריך להגדיר את סביבת הפיתוח כך שתהיה לה גישה אליהם. כדי להשתמש בממשקי ה-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.

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

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

      מגניב

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

      Kotlin

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

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

      • renderscriptTargetApi – מציין את גרסת ה-bytecode שתיצור. מומלץ להגדיר את הערך הזה לרמת ה-API הנמוכה ביותר שיכולה לספק את כל הפונקציונליות שבה אתם משתמשים, ולהגדיר את renderscriptSupportModeEnabled ל-true. הערכים התקינים להגדרה הזו הם כל ערך של מספר שלם מ-11 ועד לרמת ה-API העדכנית ביותר. אם גרסת ה-SDK המינימלית שצוינה בקובץ המניפסט של האפליקציה מוגדרת לערך אחר, המערכת מתעלמת מהערך הזה ומשתמשת בערך היעד בקובץ הבנייה כדי להגדיר את גרסת ה-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(). השיטות 'העתקה' ו-get() הן סינכרוניות.
  8. מפרקים את ההקשר של RenderScript. אפשר להרוס את ההקשר של RenderScript באמצעות destroy() או על ידי מתן אפשרות לאובייקט ההקשר של RenderScript לעבור איסוף אשפה. כתוצאה מכך, כל שימוש נוסף באובייקט ששייך להקשר הזה יגרום להחזרת חריגה.

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

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

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

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

‫RenderScript ממקור יחיד

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

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

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

משתנה גלובלי בסקריפט הוא משתנה גלובלי רגיל שאינו static בסקריפט (קובץ .rs). למשתנה גלובלי של סקריפט בשם var שמוגדר בקובץ filename.rs, תהיה שיטה get_var שמשתקפת במחלקה ScriptC_filename. אלא אם הערך הגלובלי של הוא 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 יוצרת משתנה אחד או יותר בשם פריטי נתונים של צובר כדי לשמור את המצב של תהליך הצמצום. זמן הריצה של 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 הוא מצביע לפריט הנתונים הסופי של המצבר שחושב על ידי פונקציית ה-combiner.

    אם לא מספקים פונקציית 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) must leave A the same
  • combinerName(&I, &A) חייב להיות I זהה ל-A

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