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

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

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

  • השפה עצמה היא שפה נגזרת מ-C99 לכתיבת מחשוב עם ביצועים גבוהים כתיבת ליבה של RenderScript מתארת איך להשתמש בו כדי לכתוב ליבה (kernel) של מחשוב.
  • Control API משמש לניהול משך החיים של משאבי RenderScript שליטה בביצוע הליבה. היא זמינה בשלוש שפות שונות: Java ו-C++ ב-Android. NDK ושפת הליבה שנגזרת מ-C99. שימוש ב-RenderScript מ-Java Code וגם Single-Source RenderScript מתאר את המשפט הראשון והשלישי בהתאמה,

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

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

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

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

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

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

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

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

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

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

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

      #define RS_KERNEL __attribute__((kernel))
      

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

    • לפניכם דוגמה להפחתה פשוטה ליבה (kernel) שמוסיפה את Elements קלט:

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

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

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

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

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

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

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

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

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

הגדרת רמת הדיוק של הנקודה הצפה

אפשר לקבוע את רמת הדיוק הנדרשת של הנקודה הצפה בסקריפט. האפשרות הזאת שימושית אם אין צורך בתקן IEEE 754-2008 מלא (בשימוש כברירת מחדל). הפרגמות הבאות יכולות להגדיר רמה שונה של דיוק של נקודה צפה (floating-point):

  • #pragma rs_fp_full (ברירת מחדל אם לא צוין): לאפליקציות שדורשות דיוק נקודה צפה (floating-point), כפי שמתואר בתקן 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).

גישה לממשקי 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) גבוהה יותר.

אלו היתרונות:

  • אם אתם משתמשים בממשקי Support Library Library, החלק של RenderScript באפליקציה יהיה תואמים למכשירים שמותקנת בהם גרסת Android 2.3 (API ברמה 9) ואילך, בלי קשר לסוג RenderScript התכונות שבהן אתם משתמשים. זה מאפשר לאפליקציה לפעול ביותר מכשירים מאשר אם משתמשים ממשקי API מותאמים (android.renderscript).
  • תכונות מסוימות של RenderScript לא זמינות דרך ממשקי ה-API של ספריית התמיכה.
  • אם תשתמשו בממשקי Support Library API, תקבלו חבילות APK גדולות יותר (ככל הנראה) יותר מאשר אם אתם משתמשים בממשקי ה-API המקוריים (android.renderscript).

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

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

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

חשוב לשים לב שהחל מ-Android SDK Build-tools 24.0.0 ו-Android 2.2 (API ברמה 8) כבר לא נתמך.

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

כדי להשתמש בממשקי Support Library RenderScript API:

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

      מגניב

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

      Kotlin

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

      ההגדרות שלמעלה קובעות התנהגות ספציפית בתהליך ה-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 יכולים להיות ניגשת אליו בליבה (kernel) באמצעות rsGetElementAt_type() ו- rsSetElementAt_type() כשמוגדר כסקריפט globals. אובייקטים מסוג Allocation מאפשרים להעביר מערכים מקוד Java ל-RenderScript ולהפך. Allocation אובייקטים נוצרים בדרך כלל באמצעות createTyped() או createFromBitmap().
  3. יוצרים את הסקריפטים הנחוצים. יש שני סוגי סקריפטים זמינים כשתשתמשו ב-RenderScript:
    • ScriptC: אלה הסקריפטים בהגדרת המשתמש שמתוארים בקטע כתיבת ליבה של RenderScript למעלה. לכל סקריפט יש מחלקה של Java פעולה שבא לידי ביטוי מהדר (compiler) 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: אלה ליבות (kernel) מובנות של RenderScript לפעולות נפוצות, כמו טשטוש גאוסיאני, קונבולוציה ומיזוג תמונות. למידע נוסף, ראו את מחלקות המשנה של ScriptIntrinsic
  4. אכלוס את ההקצאות בנתונים. למעט הקצאות שנוצרו באמצעות createFromBitmap(), הקצאה מאוכלסת בנתונים ריקים נוצר לראשונה. כדי לאכלס הקצאה, צריך להשתמש באחד מה'עותקים' ב-Allocation. "העותק" הן סינכרוניות.
  5. מגדירים את הסקריפטים מנחים שנדרשים. אפשר להגדיר globals באמצעות שיטות ב- אותה מחלקה ScriptC_filename בשם set_globalname. עבור לדוגמה, כדי להגדיר משתנה int בשם threshold, משתמשים בפונקציה שיטת Java set_threshold(int); וכדי להגדיר משתנה rs_allocation בשם lookup, צריך להשתמש בפונקציה Java set_lookup(Allocation). השיטות של set הם אסינכרוניים.
  6. הפעלת הליבות (kernel) המתאימות והפונקציות הניתנות להפעלה.

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

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

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

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

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

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

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

RenderScript ממקור יחיד

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

ב-RenderScript עם מקור יחיד, כותבים ליבה (kernel) כמו שמתואר ב- כתיבת ליבה של 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) יוצר הקצאה חד-פעמית של 16384 רכיבים, שכל אחד מהם הוא מסוג uchar4.

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

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

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

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

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

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

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

גרעיני הפחתה בעומק

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    static void initializerName(accumType *accum) { … }

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

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

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName) (חובה): מציין את השם של פונקציית הצבירה במקרה הזה ליבה (kernel) של הפחתת מידע. כשמשיקים את הליבה, קריאות RenderScript את הפונקציה הזו פעם אחת לכל קואורדינטה בקלט(או בקלט) כדי לעדכן צוברים בדרך כלשהי נתונים, בהתאם לקלט או לקלט. הפונקציה חייב להיות מוגדר כך:

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

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

    דוגמה לליבה (kernel) עם מספר קלט היא dotProduct.

  • combiner(combinerName)

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

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

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

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

מה נדרש להבטיח?

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

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

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

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

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

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

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

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

דוגמה: בליבה (kernel) של addint, עבור ערך קלט V:

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

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

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

קריאה לליבה (kernel) של הפחתה מקוד Java

לליבה (kernel) של הפחתה בשם 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);

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

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

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

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

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

  • אם resultType הוא int, int2 או int[15], אז resultType הוא int, Int2, או int[]. אפשר לייצג את כל הערכים של resultType מאת JavaScriptType.
  • אם resultType הוא uint, uint2 או uint[15], אז תוצאה של JavaScript היא long, Long2 או long[]. אפשר לייצג את כל הערכים של resultType מאת JavaScriptType.
  • אם resultType הוא ulong, ulong2, או ulong[15], ולאחר מכן JavaתוצאהType הוא long, Long2 או long[]. יש ערכים מסוימים מסוג resultType שאינו מיוצג על ידי JavaresultsType.

JavaFutureType הוא סוג התוצאה העתידית התואמת ל-resultType של ההמרה היוצאת .

  • אם resultType אינו סוג מערך, אז JavaScriptFutureType 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() { … }
  }
}

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

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

שיטה 3 ו-devecSiInXType

devecSiInXType הוא סוג ה-Java שתואם ל- inXType של הארגומנט התואם הפונקציה Acumulator. אלא אם inXType הוא סוג לא חתום או סוג וקטור, devecSiInXType הוא ה-Java התואם ישירות מהסוג הזה. אם inXType הוא סוג סקלר לא חתום, devecSiInXType הוא סוג Java התואם ישירות לסוג הסקלר החתום של אותו גודל. אם inXType הוא סוג וקטור חתום, devecSiInXType הוא ה-Java התואם ישירות לסוג רכיב הווקטור. אם inXType הוא לא חתימה VVector type, אז 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 שמפורטים בדף הזה.