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

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

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

  • השפה עצמה היא שפה שמבוססת על C99, המשמשת לכתיבת קוד מחשוב עתיר ביצועים. במאמר כתיבה של ליבה של RenderScript מוסבר איך משתמשים בה כדי לכתוב ליבות מחשוב.
  • ממשק ה-API לבקרה משמש לניהול משך החיים של משאבי RenderScript ולשליטה בהרצת הליבה. הוא זמין בשלושה שפות שונות: Java,‏ C++‎ ב-Android NDK ושפת הליבה עצמה שמבוססת על C99. בשימוש ב-RenderScript מקוד Java ובRenderScript ממקור יחיד מתוארות האפשרויות הראשונה והשלישית, בהתאמה.

כתיבת ליבה של RenderScript

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

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

  • אפס או יותר ליבה (kernel) של מחשוב. ליבה של מחשוב היא פונקציה או אוסף של פונקציות שאפשר להורות להן להפעיל את סביבת זמן הריצה של 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 שהוחל על אב הטיפוס של הפונקציה מציין שהפונקציה היא ליבה (kernel) למיפוי של RenderScript במקום פונקציה שניתן להפעיל. הארגומנט in מתמלא באופן אוטומטי על סמך הקלט Allocation שמוענק להפעלת הליבה. בהמשך מוסבר על הארגומנטים x ו-y. הערך המוחזר מהליבה נכתב באופן אוטומטי במיקום המתאים בפלט Allocation. כברירת מחדל, הליבה הזו פועלת על כל הקלט שלה Allocation, עם הפעלה אחת של פונקציית הליבה לכל Element ב-Allocation.

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

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

      אם נדרשים יותר קלט או פלט Allocations ממה שיש בליבה (kernel), צריך לקשר את האובייקטים האלה אל rs_allocation הסקריפט globals ולגשת אליהם מליבה או מפונקציה שניתן להפעיל דרך 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 בדוגמה הזו) והשמות והתפקידים של הפונקציות שמרכיבות את הליבה (פונקציית accumulatoraddintAccum בדוגמה הזו). כל הפונקציות האלה חייבות להיות מסוג static. לליבה (kernel) של הפחתה תמיד נדרשת פונקציית accumulator. יכולות להיות לה גם פונקציות נוספות, בהתאם למה שרוצים שהליבה תבצע.

      פונקציית צבירה של ליבה (kernel) צריכה להחזיר void וצריכה להכיל לפחות שני ארגומנטים. הארגומנט הראשון (accum בדוגמה הזו) הוא הפניה לפריט נתונים של המצטבר, והשני (val בדוגמה הזו) מתמלא באופן אוטומטי על סמך הקלט Allocation שמוענק להפעלת הליבה. פריט הנתונים של המצטבר נוצר על ידי סביבת זמן הריצה של RenderScript, והוא מאופשר לאפס כברירת מחדל. כברירת מחדל, הליבה הזו פועלת על כל הקלט שלה Allocation, עם הפעלה אחת של פונקציית המצטבר לכל Element ב-Allocation. כברירת מחדל, הערך הסופי של פריט הנתונים של המצטבר מטופל כתוצאה מההפחתה, והוא מוחזר ל-Java. סביבת זמן הריצה של RenderScript בודקת שהסוג Element של הקצאת הקלט תואם לפרוטוטיפ של פונקציית המצטבר. אם אין התאמה, מערכת RenderScript גורמת להשלכת חריגה.

      בליבה (kernel) של הפחתה יש קלט אחד או יותר Allocations אבל אין פלט Allocations.

      כאן מוסבר בפירוט על ליבות הפחתה.

      ליבות הפחתה נתמכות ב-Android 7.0 (רמת API 24) ואילך.

    פונקציית ליבה של מיפוי או פונקציית צבירת ליבה של הפחתה יכולות לגשת לקואורדינטות של הביצוע הנוכחי באמצעות הארגומנטים המיוחדים x,‏ y ו-z, שחייבים להיות מסוג int או uint32_t. הארגומנטים האלה הם אופציונליים.

    גם פונקציית ליבה (kernel) של מיפוי או פונקציית צבירה של ליבה (kernel) עשויה להשתמש בארגומנט המיוחד context מסוג rs_kernel_context. הוא נדרש למשפחה של ממשקי API בסביבת זמן ריצה, שמשמשים לשליחת שאילתות לגבי מאפיינים מסוימים של הביצוע הנוכחי – לדוגמה, rsGetDimX. (ארגומנט context זמין ב-Android 6.0 (רמת API‏ 23) ואילך).

  • פונקציית init() אופציונלית. הפונקציה init() היא סוג מיוחד של פונקציה שניתן להפעיל, שפועלת ב-RenderScript כשהסקריפט נוצר בפעם הראשונה. כך חלק מהחישובים מתבצעים באופן אוטומטי כשיוצרים את הסקריפט.
  • אפס או יותר קטגוריות ופונקציות של סקריפט סטטי. סקריפט סטטי גלובלי מקביל לסקריפט גלובלי, אבל לא ניתן לגשת אליו מקוד Java. פונקציה סטטית היא פונקציית C רגילה שאפשר להפעיל מכל ליבה או מכל פונקציה שניתן להפעיל בסקריפט, אבל היא לא חשופה ל-Java API. אם אין צורך לגשת לפונקציה או למשתנה גלובלי של סקריפט מקוד Java, מומלץ מאוד להצהיר עליהם כ-static.

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

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

גישה לממשקי ה-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 של Support Library, חבילות ה-APK יהיו גדולות יותר (אולי באופן משמעותי) מאשר אם משתמשים בממשקי ה-API המקומיים (android.renderscript).

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

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

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

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

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

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

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

      Groovy

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

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

      • renderscriptTargetApi – מציין את גרסת ה-bytecode שיש ליצור. מומלץ להגדיר את הערך הזה לרמת ה-API הנמוכה ביותר שיכולה לספק את כל הפונקציונליות שבה אתם משתמשים, ולהגדיר את renderscriptSupportModeEnabled לערך true. הערכים החוקיים להגדרה הזו הם כל ערך שלם מ-11 ועד לרמת ה-API שפורסמה לאחרונה. אם גרסת ה-SDK המינימלית שצוינה במניפסט של האפליקציה מוגדרת לערך אחר, הערך הזה מבוטל והערך היעד בקובץ ה-build משמש להגדרת גרסת ה-SDK המינימלית.
      • renderscriptSupportModeEnabled – קובע שהקוד הבינארי שנוצר יחזור לגרסה תואמת אם המכשיר שבו הוא פועל לא תומך בגרסת היעד.
  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 בליבה (kernel) באמצעות rsGetElementAt_type() ו-rsSetElementAt_type() כשהם מחויבים לסקריפט מסוג globals. אובייקטים מסוג 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 מובנות לפעולות נפוצות, כמו טשטוש גאוסיאני, עיבוד נתונים (convolution) ושילוב תמונות. מידע נוסף זמין בקטעים הבאים על תת-הסוגים של ScriptIntrinsic.
  4. אכלוס את ההקצאות בנתונים. מלבד הקצאות שנוצרו באמצעות createFromBitmap(), ההקצאה מאוכלסת בנתונים ריקים כשהיא נוצרת לראשונה. כדי לאכלס הקצאה, משתמשים באחת מהשיטות 'copy' ב-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(). ההשקות האלה הן לא סינכרוניות. בהתאם לארגומנטים של הליבה, השיטה מקבלת Allocations אחד או יותר, וכל אחד מהם חייב להיות באותו המאפיינים. כברירת מחדל, ליבה מריצים כל קואורדינטות במאפיינים האלה. כדי להריץ ליבה על קבוצת משנה של הקואורדינטות האלה, מעבירים את Script.LaunchOptions המתאים כארגומנט האחרון ל-method forEach או reduce.

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

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

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

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

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

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

Single-Source RenderScript

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

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

הקטע כתיבת ליבה של RenderScript מכיל דוגמה ליבה (kernel) שהופכת תמונה. בדוגמה הבאה מוסבר איך להחיל יותר מאפקט אחד על תמונה באמצעות Single-Source RenderScript. הוא כולל ליבה נוספת, greyscale, שממירה תמונה צבעונית לשחור-לבן. לאחר מכן, פונקציה שניתן להפעיל process() מחילה את שני הליבות האלה ברצף על תמונה של קלט, ומפיקה תמונה של פלט. הקצאות גם לקלט וגם לפלט מועברות כארגומנטים מסוג rs_allocation.

// File: singlesource.rs

#pragma version(1)
#pragma rs java_package_name(com.android.rssample)

static const float4 weight = {0.299f, 0.587f, 0.114f, 0.0f};

uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) {
  uchar4 out = in;
  out.r = 255 - in.r;
  out.g = 255 - in.g;
  out.b = 255 - in.b;
  return out;
}

uchar4 RS_KERNEL greyscale(uchar4 in) {
  const float4 inF = rsUnpackColor8888(in);
  const float4 outF = (float4){ dot(inF, weight) };
  return rsPackColorTo8888(outF);
}

void process(rs_allocation inputImage, rs_allocation outputImage) {
  const uint32_t imageWidth = rsAllocationGetDimX(inputImage);
  const uint32_t imageHeight = rsAllocationGetDimY(inputImage);
  rs_allocation tmp = rsCreateAllocation_uchar4(imageWidth, imageHeight);
  rsForEach(invert, inputImage, tmp);
  rsForEach(greyscale, tmp, outputImage);
}

אפשר להפעיל את הפונקציה process() מ-Java או מ-Kotlin באופן הבא:

Kotlin

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

Java

// File SingleSource.java

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

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

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

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

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

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

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

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

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

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

ב-Android 7.0 (רמת API 24) ואילך, ‏RenderScript תומך בליבות הפחתה כדי לאפשר שימוש באלגוריתמים יעילים להפחתת נתונים שנכתבו על ידי משתמשים. תוכלו להפעיל ליבה (kernel) של צמצום בקלט עם מאפיין 1, 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;
}

הערה: כאן יש עוד ליבה (kernel) של צמצום לדוגמה.

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

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

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

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

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

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

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

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

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

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

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

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

כתיבת ליבה (kernel) של הפחתה

#pragma rs reduce מגדיר ליבה (kernel) של צמצום על ידי ציון השם שלה, והשמות והתפקידים של הפונקציות שמהן מורכב הליבה. כל הפונקציות האלה חייבות להיות 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)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

מה אי אפשר להניח?

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

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

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

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

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

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

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

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

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

דוגמה: בליבה 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.

דוגמה: בליבה (kernel) של 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);

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

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

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

javaFutureType,‏ סוג ההחזרה של שיטות הפחתה המשתקפות, הוא סוג של שיטת Reflection של קלאס סטטי מוטמע בתוך הכיתה ScriptC_filename. הוא מייצג את התוצאה העתידית של הרצת ליבה (kernel) של הפחתה. כדי לקבל את התוצאה בפועל של ההרצה, צריך להפעיל את השיטה 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() {}
  }
}

הפונקציה JavaScript resultType מחושבת מה-resultType של הפונקציה outconversioner. אלא אם 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], הערך של AnswerjavaType הוא 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 לא יכול לייצג את כל הערכים מסוג 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. המערך הוא ייצוג שטוח: יש בו פי שניים רכיבים סקלריים מאשר שיש להקצאה רכיבי וקטור עם שני רכיבים. זהו אותו האופן שבו פועלות השיטות copyFrom() של Allocation.
  • אם הערך של inXType הוא uint, הערך של deviceSiInXType הוא int. ערך חתום במערך Java מפורש כערך לא חתום של אותו תבנית ביטים בהקצאה. זו אותה הדרך שבה פועלות השיטות copyFrom() של Allocation.
  • אם הערך של inXType הוא uint2, הערך של deviceSiInXType הוא int. זהו שילוב של האופן שבו מטפלים ב-int2 וב-uint: המערך הוא ייצוג שטוח, וערכים חתומים של מערך Java מפורשים כערכים של רכיבים לא חתומים ב-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 שמפורטים בדף הזה.