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

  • אפס או יותר ליבות מחשוב. ליבה לעיבוד נתונים היא פונקציה או אוסף של פונקציות שאפשר להורות לסביבת זמן הריצה של RenderScript להריץ במקביל על אוסף של נתונים. יש שני סוגים של ליבות מחשוב: ליבות מיפוי (שנקראים גם ליבות foreach) וליבות הפחתה.

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

    • דוגמה לליבה של מיפוי פשוטה:

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

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

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

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

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

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

      #define RS_KERNEL __attribute__((kernel))
      

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

    • דוגמה לליבה פשוטה של ירידה שמסכמת את הערך של Elements בקלט:

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

      ליבה של הפחתה מורכבת מפונקציה אחת או יותר שנכתבו על ידי משתמשים. #pragma rs reduce משמש להגדרת הליבה על ידי ציון השם שלה (addint בדוגמה הזו) והשמות והתפקידים של הפונקציות שמרכיבות את הליבה (פונקציית accumulatoraddintAccum בדוגמה הזו). כל הפונקציות האלה חייבות להיות 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 כשהסקריפט נוצר בפעם הראשונה. כך אפשר לבצע חישובים מסוימים באופן אוטומטי בזמן יצירת הסקריפט.
  • אפס או יותר פונקציות ו-globals סטטיים של סקריפט. משתנה גלובלי סטטי של סקריפט זהה למשתנה גלובלי של סקריפט, מלבד העובדה שלא ניתן לגשת אליו מקוד 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, ויכולות לסבול פחות דיוק. המצב הזה מאפשר שטיפה לאפס (flush-to-zero) לערכים לא רגילים (denorms) ועיגול לאפס.
  • #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

כדי להשתמש בממשקי ה-API של Support Library RenderScript, צריך להגדיר את סביבת הפיתוח כך שתהיה לה גישה אליהם. כדי להשתמש בממשקי ה-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 – מציין את גרסת הקוד הבינארי שרוצים ליצור. מומלץ להגדיר את הערך הזה לרמת ה-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 בליבות באמצעות 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 מובנות לפעולות נפוצות, כמו טשטוש גאוסיאני, עיבוד נתונים (convolution) ושילוב תמונות. מידע נוסף זמין בקטעים הבאים על תת-הסוגים של ScriptIntrinsic.
  4. איך מאכלסים את הקצאות התקציב בנתונים מלבד הקצאות שנוצרו באמצעות createFromBitmap(), הקצאה מאוכלסת בנתונים ריקים כשהיא נוצרת בפעם הראשונה. כדי לאכלס הקצאה, משתמשים באחת מהשיטות 'copy' ב-Allocation. השיטות 'copy' הן סינכרוניות.
  5. מגדירים את כל המשתנים הגלובליים של הסקריפט הנדרשים. אפשר להגדיר משתנים גלובליים באמצעות שיטות באותה כיתה ScriptC_filename בשם set_globalname. לדוגמה, כדי להגדיר משתנה int בשם threshold, משתמשים ב-method‏ set_threshold(int) של Java. כדי להגדיר משתנה rs_allocation בשם lookup, משתמשים ב-method‏ set_lookup(Allocation) של Java. השיטות של set הן אסינכרוניות.
  6. מפעילים את הליבות והפונקציות הרלוונטיות שניתן להפעיל.

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

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

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

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

משתנה גלובלי של סקריפט הוא משתנה גלובלי רגיל שאינו static בקובץ סקריפט (.rs). לסקריפט ברמת ה-global שנקרא 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 תומך בליבות הפחתה כדי לאפשר שימוש באלגוריתמים יעילים להפחתה שנכתבו על ידי משתמשים. אפשר להפעיל ליבות של ירידה בגודל על קלט עם 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;
}

הערה: דוגמאות נוספות לגרעיני הפחתה מפורטות כאן.

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

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

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

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

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

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

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

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

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

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

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

דוגמה: בליבה findMinAndMax, פונקציית הממיר הפלט מאתחלת את ערך התוצאה 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)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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