RenderScript הוא פלטפורמה להרצת משימות שדורשות הרבה חישובים בביצועים גבוהים ב-Android. RenderScript מיועד בעיקר לשימוש בחישובים מקבילים, אבל הוא יכול להועיל גם לעומסי עבודה סדרתיים. סביבת זמן הריצה של RenderScript מבצעת הקבלה של העבודה בין המעבדים שזמינים במכשיר, כמו מעבדי CPU מרובי ליבות ומעבדי GPU. כך תוכלו להתמקד בהגדרת אלגוריתמים במקום בתזמון עבודה. RenderScript שימושי במיוחד לאפליקציות שמבצעות עיבוד תמונות, צילום חישובי או ראייה ממוחשבת.
כדי להתחיל להשתמש ב-RenderScript, צריך להבין שני מושגים עיקריים:
- השפה עצמה היא שפה שנגזרת מ-C99, ומשמשת לכתיבת קוד של מחשוב עתיר ביצועים (HPC). במאמר Writing a RenderScript Kernel מוסבר איך להשתמש בו כדי לכתוב ליבות חישוב.
- Control API משמש לניהול משך החיים של משאבי RenderScript ולשליטה בהרצת ליבת המערכת. היא זמינה בשלוש שפות שונות: Java, C++ ב-Android NDK ושפת הליבה עצמה שנגזרת מ-C99. במאמרים Using RenderScript from Java Code ו-Single-Source RenderScript מוסבר על האפשרות הראשונה והשלישית, בהתאמה.
כתיבת ליבת RenderScript
ליבת RenderScript נמצאת בדרך כלל בקובץ .rs בספרייה <project_root>/src/rs. כל קובץ .rs נקרא סקריפט. כל סקריפט מכיל קבוצה משלו של ליבות, פונקציות ומשתנים. סקריפט יכול להכיל:
- הצהרת pragma (
#pragma version(1)) שמצהירה על הגרסה של שפת ליבת RenderScript שנעשה בה שימוש בסקריפט הזה. בשלב הזה, הערך התקין היחיד הוא 1. - הצהרת pragma (
#pragma rs java_package_name(com.example.app)) שמצהירה על שם החבילה של מחלקות Java שמשתקפות מהסקריפט הזה. שימו לב שקובץ.rsצריך להיות חלק מחבילת האפליקציה, ולא בפרויקט של ספרייה. - אפס או יותר פונקציות שאפשר להפעיל. פונקציה שאפשר להפעיל היא פונקציית RenderScript עם שרשור יחיד שאפשר לקרוא לה מקוד Java עם ארגומנטים שרירותיים. הן שימושיות בדרך כלל להגדרה ראשונית או לחישובים סדרתיים בתוך צינור עיבוד גדול יותר.
אפס או יותר משתנים גלובליים של סקריפט. משתנה גלובלי בסקריפט דומה למשתנה גלובלי ב-C. אפשר לגשת למשתנים גלובליים של סקריפט מקוד Java, ולעתים קרובות משתמשים בהם להעברת פרמטרים לליבות של RenderScript. הסבר מפורט יותר על משתנים גלובליים בסקריפט זמין כאן.
אפס ליבות מחשוב או יותר. ליבת מחשוב היא פונקציה או אוסף של פונקציות שאפשר להנחות את זמן הריצה של RenderScript לבצע במקביל על אוסף של נתונים. יש שני סוגים של ליבות חישוב: ליבות מיפוי (נקראות גם ליבות foreach) וליבות צמצום.
ליבת מיפוי היא פונקציה מקבילית שפועלת על אוסף של
Allocationsבאותם ממדים. כברירת מחדל, הפונקציה מופעלת פעם אחת לכל קואורדינטה בממדים האלה. בדרך כלל (אבל לא רק) משתמשים בו כדי להמיר אוסף של נתוני קלטAllocationsלפלטAllocationאחדElementבכל פעם.דוגמה לליבת מיפוי פשוטה:
uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) { uchar4 out = in; out.r = 255 - in.r; out.g = 255 - in.g; out.b = 255 - in.b; return out; }
ברוב המקרים, הפונקציה הזו זהה לפונקציה רגילה של C. המאפיין
RS_KERNELשמוחל על אב הטיפוס של הפונקציה מציין שהפונקציה היא ליבת מיפוי של RenderScript ולא פונקציה שאפשר להפעיל. הארגומנטinמתמלא אוטומטית על סמך הקלטAllocationשמועבר להפעלת הליבה. הארגומנטיםxו-yמוסברים בהמשך. הערך שמוחזר מהליבה נכתב אוטומטית במיקום המתאים בפלטAllocation. כברירת מחדל, הליבה הזו מופעלת על כל הקלט שלהAllocation, עם הפעלה אחת של פונקציית הליבה לכלElementב-Allocation.לליבת מיפוי יכול להיות קלט אחד או יותר
Allocations, פלט יחידAllocationאו שניהם. סביבת זמן הריצה של RenderScript מבצעת בדיקות כדי לוודא שלכל הקצאות הקלט והפלט יש את אותם ממדים, ושהסוגים של הקצאות הקלט והפלט תואמים לאב-טיפוס של הליבה. אם אחת מהבדיקות האלה נכשלת, RenderScript זורק חריגה.Elementהערה: לפני Android 6.0 (רמת API 23), יכול להיות שלליבת מיפוי לא יהיה יותר מקלט אחד
Allocation.אם אתם צריכים יותר קלט או פלט
Allocationsממה שיש לליבת ה-kernel, צריך לקשור את האובייקטים האלה לrs_allocationמשתנים גלובליים של הסקריפט ולגשת אליהם מליבת ה-kernel או מפונקציה שאפשר להפעיל באמצעותrsGetElementAt_type()אוrsSetElementAt_type().הערה:
RS_KERNELהוא מאקרו שמוגדר אוטומטית על ידי RenderScript לנוחיותכם:#define RS_KERNEL __attribute__((kernel))
ליבת צמצום היא משפחה של פונקציות שפועלות על אוסף של נתוני קלט
Allocationsמאותם ממדים. כברירת מחדל, פונקציית המצבר שלו מופעלת פעם אחת לכל קואורדינטה בממדים האלה. הפונקציה הזו משמשת בדרך כלל (אבל לא רק) כדי לצמצם אוסף של קלטAllocationsלערך בודד.בדוגמה הבאה מוצג קרנל פשוט של צמצום שמסכם את
Elementsשל הקלט שלו:#pragma rs reduce(addint) accumulator(addintAccum) static void addintAccum(int *accum, int val) { *accum += val; }
גרעין של צמצום מורכב מפונקציה אחת או יותר שנכתבו על ידי המשתמש. הפונקציה
#pragma rs reduceמשמשת להגדרת הליבה על ידי ציון השם שלה (addintבדוגמה הזו) והשמות והתפקידים של הפונקציות שמרכיבות את הליבה (פונקציהaccumulator,addintAccumבדוגמה הזו). כל הפונקציות האלה חייבות להיותstatic. ליבת צמצום תמיד דורשת פונקצייתaccumulator, ויכולות להיות בה גם פונקציות אחרות, בהתאם לפעולות שרוצים שהליבה תבצע.פונקציית צובר של ליבת הפחתה חייבת להחזיר
voidולכלול לפחות שני ארגומנטים. הארגומנט הראשון (accumבדוגמה הזו) הוא מצביע לפריט נתונים של צובר, והארגומנט השני (valבדוגמה הזו) מתמלא אוטומטית על סמך קלטAllocationשמועבר להפעלת הליבה. פריט הנתונים של המצבר נוצר על ידי זמן הריצה של RenderScript. כברירת מחדל, הוא מאותחל לאפס. כברירת מחדל, הליבה הזו מופעלת על כל הקלט שלהAllocation, עם הפעלה אחת של פונקציית המצבר לכלElementב-Allocation. כברירת מחדל, הערך הסופי של פריט הנתונים של המצבר נחשב לתוצאה של הצמצום ומוחזר ל-Java. סביבת זמן הריצה של RenderScript בודקת כדי לוודא שהסוגElementשל הקצאת הקלט תואם לאב-טיפוס של פונקציית האקומולטור. אם אין התאמה, RenderScript זורקת חריגה.לליבת צמצום יש קלט אחד או יותר
Allocationsאבל אין פלטAllocations.כאן מוסבר על ליבות של הפחתה בפירוט רב יותר.
ליבות של הפחתה נתמכות ב-Android 7.0 (רמת API 24) ואילך.
פונקציית ליבה של מיפוי או פונקציית ליבה של צבירה לצמצום יכולות לגשת לקואורדינטות של ההפעלה הנוכחית באמצעות הארגומנטים המיוחדים
x,yו-z, שחייבים להיות מסוגintאוuint32_t. הארגומנטים האלה הם אופציונליים.פונקציית מיפוי של ליבת המערכת או פונקציית צובר של ליבת המערכת יכולות לקבל גם את הארגומנט המיוחד האופציונלי
contextמסוג rs_kernel_context. היא נדרשת למשפחה של ממשקי API של זמן ריצה שמשמשים להרצת שאילתות על מאפיינים מסוימים של הביצוע הנוכחי – לדוגמה, rsGetDimX. (הארגומנטcontextזמין ב-Android 6.0 (רמת API 23) ואילך).- פונקציה אופציונלית
init(). הפונקציהinit()היא סוג מיוחד של פונקציה שאפשר להפעיל, ו-RenderScript מריץ אותה כשהסקריפט מופעל לראשונה. כך אפשר לבצע חישובים מסוימים באופן אוטומטי בזמן יצירת הסקריפט. - אפס או יותר פונקציות ומשתנים גלובליים סטטיים של סקריפט. משתנה גלובלי סטטי בסקריפט שווה למשתנה גלובלי בסקריפט, אלא שאי אפשר לגשת אליו מקוד Java. פונקציה סטטית היא פונקציית C רגילה שאפשר להפעיל מכל פונקציית ליבה או פונקציה שאפשר להפעיל בסקריפט, אבל היא לא חשופה ל-Java API. אם אין צורך לגשת לסקריפט גלובלי או לפונקציה מקוד Java, מומלץ מאוד להצהיר עליהם כ-
static.
הגדרת רמת הדיוק של נקודה צפה (floating-point)
אתם יכולים לשלוט ברמת הדיוק הנדרשת של נקודה צפה בסקריפט. האפשרות הזו שימושית אם לא נדרש תקן IEEE 754-2008 מלא (שמשמש כברירת מחדל). הפרגמות הבאות יכולות להגדיר רמת דיוק שונה של נקודה צפה:
-
#pragma rs_fp_full(ברירת מחדל אם לא מצוין כלום): לאפליקציות שנדרשת בהן דיוק של נקודה צפה, כפי שמתואר בתקן IEEE 754-2008. -
#pragma rs_fp_relaxed: לאפליקציות שלא נדרשת בהן תאימות מלאה לתקן IEEE 754-2008, ויכולות לפעול עם רמת דיוק נמוכה יותר. במצב הזה מופעלת האפשרות 'איפוס לאפס' עבור מספרים לא נורמליים ו'עיגול לאפס'. #pragma rs_fp_imprecise: לאפליקציות שאין להן דרישות דיוק מחמירות. במצב הזה אפשר לעשות הכול ב-rs_fp_relaxed, וגם את הפעולות הבאות:- פעולות שמניבות את הערך -0.0 יכולות להחזיר במקומו את הערך +0.0.
- הפעולות על INF ו-NAN לא מוגדרות.
ברוב האפליקציות אפשר להשתמש ב-rs_fp_relaxed בלי תופעות לוואי. האפשרות הזו יכולה להיות מאוד שימושית בארכיטקטורות מסוימות, כי היא מאפשרת לבצע אופטימיזציות נוספות שזמינות רק עם דיוק מופחת (כמו הוראות SIMD CPU).
גישה ל-RenderScript APIs מ-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 הזה מאפשר לאפליקציה לפעול ביותר מכשירים מאשר אם משתמשים בממשקי ה-API המקוריים (
android.renderscript). - חלק מהתכונות של RenderScript לא זמינות דרך ממשקי ה-API של ספריית התמיכה.
- אם משתמשים בממשקי Support Library APIs, חבילות ה-APK שמתקבלות גדולות יותר (יכול להיות שהרבה יותר) מאלה שמתקבלות אם משתמשים בממשקי API מקוריים (
android.renderscript).
שימוש בממשקי API של ספריית התמיכה של RenderScript
כדי להשתמש בממשקי API של RenderScript בספריית התמיכה, צריך להגדיר את סביבת הפיתוח כך שתהיה לה גישה אליהם. כדי להשתמש בממשקי ה-API האלה, צריך את כלי Android SDK הבאים:
- Android SDK Tools מגרסה 22.2 ואילך
- Android SDK Build-tools מגרסה 18.1.0 ואילך
שימו לב: החל מ-Android SDK Build-tools 24.0.0, אין יותר תמיכה ב-Android 2.2 (רמת API 8).
אפשר לבדוק ולעדכן את הגרסה המותקנת של הכלים האלה בAndroid SDK Manager.
כדי להשתמש ב-Support Library RenderScript APIs:
- מוודאים שמותקנת הגרסה הנדרשת של Android SDK.
- מעדכנים את ההגדרות של תהליך הבנייה של 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 } }
ההגדרות שמופיעות למעלה שולטות בהתנהגות ספציפית בתהליך הבנייה של Android:
-
renderscriptTargetApi– מציין את גרסת ה-bytecode שתיצור. מומלץ להגדיר את הערך הזה לרמת ה-API הנמוכה ביותר שיכולה לספק את כל הפונקציונליות שבה אתם משתמשים, ולהגדיר אתrenderscriptSupportModeEnabledל-true. הערכים התקינים להגדרה הזו הם כל ערך של מספר שלם מ-11 ועד לרמת ה-API העדכנית ביותר. אם גרסת ה-SDK המינימלית שצוינה במניפסט האפליקציה מוגדרת לערך אחר, המערכת מתעלמת מהערך הזה ומשתמשת בערך היעד בקובץ ה-build כדי להגדיר את גרסת ה-SDK המינימלית. -
renderscriptSupportModeEnabled– מציין שהבייטקוד שנוצר צריך לחזור לגרסה תואמת אם המכשיר שהוא פועל בו לא תומך בגרסת היעד.
-
- פותחים את הקובץ
- במחלקות האפליקציה שמשתמשות ב-RenderScript, מוסיפים ייבוא למחלקות Support Library:
Kotlin
import android.support.v8.renderscript.*
Java
import android.support.v8.renderscript.*;
שימוש ב-RenderScript מקוד Java או Kotlin
השימוש ב-RenderScript מקוד Java או Kotlin מתבסס על מחלקות ה-API שנמצאות בחבילה android.renderscript או android.support.v8.renderscript. רוב האפליקציות פועלות לפי אותו דפוס שימוש בסיסי:
- מאתחלים הקשר של RenderScript. ההקשר
RenderScript, שנוצר באמצעותcreate(Context), מבטיח שאפשר להשתמש ב-RenderScript ומספק אובייקט לשליטה במשך החיים של כל אובייקטי RenderScript הבאים. יצירת הקשר היא פעולה שעלולה להימשך זמן רב, כי יכול להיות שהיא יוצרת משאבים בחלקים שונים של החומרה. לכן, אם אפשר, לא כדאי להוסיף אותה לנתיב הקריטי של האפליקציה. בדרך כלל, לאפליקציה יש רק הקשר RenderScript אחד בכל פעם. - יוצרים לפחות
Allocationאחד להעברה לסקריפט. Allocationהוא אובייקט RenderScript שמספק אחסון לכמות קבועה של נתונים. הקלט והפלט של ליבות בסקריפטים הם אובייקטים מסוגAllocation, ואפשר לגשת לאובייקטים מסוגAllocationבליבות באמצעותrsGetElementAt_type()ו-rsSetElementAt_type()כשהם מאוגדים כמשתנים גלובליים של הסקריפט. אובייקטים מסוגAllocationמאפשרים להעביר מערכים מקוד Java לקוד RenderScript ולהיפך. בדרך כלל יוצרים אובייקטים מסוגAllocationבאמצעותcreateTyped()אוcreateFromBitmap(). - יוצרים את כל הסקריפטים הנדרשים. יש שני סוגים של סקריפטים שזמינים לכם כשאתם משתמשים ב-RenderScript:
- ScriptC: אלה סקריפטים שהוגדרו על ידי המשתמש, כפי שמתואר בקטע כתיבת ליבת RenderScript למעלה. לכל סקריפט יש מחלקת Java שמשתקפת על ידי מהדר RenderScript כדי לאפשר גישה קלה לסקריפט מקוד Java. שם המחלקה הוא
ScriptC_filename. לדוגמה, אם ליבת המיפוי שלמעלה נמצאת ב-invert.rsוהקשר של RenderScript כבר נמצא ב-mRenderScript, קוד ה-Java או ה-Kotlin ליצירת מופע של הסקריפט יהיה:Kotlin
val invert = ScriptC_invert(renderScript)
Java
ScriptC_invert invert = new ScriptC_invert(renderScript);
- ScriptIntrinsic: אלה ליבות RenderScript מובנות לפעולות נפוצות, כמו טשטוש גאוסיאני, קונבולוציה ומיזוג תמונות. מידע נוסף זמין במאמר בנושא מחלקות משנה של
ScriptIntrinsic.
- ScriptC: אלה סקריפטים שהוגדרו על ידי המשתמש, כפי שמתואר בקטע כתיבת ליבת RenderScript למעלה. לכל סקריפט יש מחלקת Java שמשתקפת על ידי מהדר RenderScript כדי לאפשר גישה קלה לסקריפט מקוד Java. שם המחלקה הוא
- מאכלסים את הכרטיסייה 'הקצאות' בנתונים. מלבד הקצאות שנוצרו באמצעות
createFromBitmap(), הקצאה מאוכלסת בנתונים ריקים כשהיא נוצרת לראשונה. כדי לאכלס הקצאה, משתמשים באחת משיטות ההעתקה ב-Allocation. השיטות של 'העתקה' הן סינכרוניות. - מגדירים את המשתנים הגלובליים של הסקריפט שנדרשים. אפשר להגדיר משתנים גלובליים באמצעות שיטות באותה מחלקה
ScriptC_filenameשנקראתset_globalname. לדוגמה, כדי להגדיר משתנהintבשםthreshold, משתמשים בשיטת Javaset_threshold(int). כדי להגדיר משתנהrs_allocationבשםlookup, משתמשים בשיטת Javaset_lookup(Allocation). השיטות שלsetהן אסינכרוניות. - מפעילים את ליבות הקרנל המתאימות ואת הפונקציות שאפשר להפעיל.
השיטות להפעלת ליבה נתונה משתקפות באותה מחלקה
ScriptC_filenameעם שיטות בשםforEach_mappingKernelName()אוreduce_reductionKernelName(). ההשקות האלה הן אסינכרוניות. בהתאם לארגומנטים של הליבה, השיטה מקבלת הקצאה אחת או יותר, שלכולן צריכים להיות אותם ממדים. כברירת מחדל, ליבת ה-kernel מופעלת על כל קואורדינטה בממדים האלה. כדי להפעיל ליבת kernel על קבוצת משנה של הקואורדינטות האלה, צריך להעבירScript.LaunchOptionsמתאים כארגומנט האחרון לשיטהforEachאוreduce.מפעילים פונקציות שאפשר להפעיל באמצעות השיטות
invoke_functionNameשמשתקפות באותה מחלקהScriptC_filename. ההשקות האלה הן אסינכרוניות. - אחזור נתונים מאובייקטים מסוג
Allocationומאובייקטים מסוג javaFutureType. כדי לגשת לנתונים מ-Allocationמקוד Java, צריך להעתיק את הנתונים האלה בחזרה ל-Java באמצעות אחת משיטות ההעתקה ב-Allocation. כדי לקבל את התוצאה של ליבת צמצום, צריך להשתמש בשיטהjavaFutureType.get(). השיטות 'העתקה' ו-get()הן סינכרוניות. - מפרקים את ההקשר של RenderScript. אפשר להרוס את ההקשר של RenderScript באמצעות
destroy()או על ידי מתן אפשרות לאובייקט ההקשר של RenderScript לעבור איסוף אשפה. כתוצאה מכך, כל שימוש נוסף באובייקט ששייך להקשר הזה יגרום להחזרת חריגה.
מודל ביצוע אסינכרוני
השיטות forEach, invoke, reduce ו-set הן אסינכרוניות – כל אחת מהן עשויה לחזור ל-Java לפני השלמת הפעולה המבוקשת. עם זאת, הפעולות הבודדות מבוצעות בסדר שבו הן מופעלות.
המחלקות Allocation מספקות שיטות 'העתקה' להעתקת נתונים אל Allocations וממנו. השיטה 'copy' היא סינכרונית, והיא עוברת סריאליזציה ביחס לכל אחת מהפעולות האסינכרוניות שלמעלה שמשפיעות על אותה הקצאה.
המחלקות javaFutureType שמשקפות את התוצאה מספקות שיטה get() לקבלת התוצאה של צמצום. get() הוא סינכרוני, והוא עובר סריאליזציה ביחס לצמצום (שהוא אסינכרוני).
Single-Source RenderScript
ב-Android 7.0 (רמת API 24) הושקה תכונת תכנות חדשה בשם Single-Source RenderScript (מקור יחיד של RenderScript), שבה ליבות מופעלות מהסקריפט שבו הן מוגדרות, ולא מ-Java. הגישה הזו מוגבלת כרגע למיפוי ליבות (kernels), שנקראות בפשטות 'ליבות' בקטע הזה כדי לשמור על תמציתיות. התכונה החדשה הזו תומכת גם ביצירת הקצאות מהסוג
rs_allocation מתוך הסקריפט. עכשיו אפשר להטמיע אלגוריתם שלם רק בסקריפט, גם אם נדרשות כמה הפעלות של ליבת המערכת.
היתרון הוא כפול: קוד קריא יותר, כי הוא שומר על ההטמעה של אלגוריתם בשפה אחת, וקוד מהיר יותר, כי יש פחות מעברים בין Java ל-RenderScript בכמה הפעלות של ליבת המערכת.
ב-RenderScript עם מקור יחיד, כותבים ליבות כמו שמתואר במאמר
כתיבת ליבת RenderScript. אחר כך כותבים פונקציה שאפשר להפעיל ולקרוא לה כדי להפעיל את
rsForEach(). ה-API הזה מקבל פונקציית ליבה כפרמטר הראשון, ואחריה הקצאות של קלט ופלט. API דומה
rsForEachWithOptions() מקבל ארגומנט נוסף מסוג
rs_script_call_t, שמציין קבוצת משנה של הרכיבים מהקצאות הקלט והפלט לעיבוד של פונקציית הליבה.
כדי להתחיל חישוב RenderScript, מפעילים את הפונקציה הניתנת להפעלה מ-Java.
פועלים לפי השלבים במאמר שימוש ב-RenderScript מקוד Java.
בשלב הפעלת ליבות מתאימות, קוראים לפונקציה שאפשר להפעיל באמצעות invoke_function_name(), מה שיפעיל את כל החישוב, כולל הפעלת ליבות.
לרוב, צריך הקצאות כדי לשמור ולהעביר תוצאות ביניים מהפעלת ליבה אחת להפעלת ליבה אחרת. אפשר ליצור אותם באמצעות
rsCreateAllocation(). אחת מהצורות של ה-API הזה שקל להשתמש בהן היא
rsCreateAllocation_<T><W>(…), כאשר T הוא סוג הנתונים של רכיב, ו-W הוא רוחב הווקטור של הרכיב. ה-API מקבל את הגדלים בממדים X, Y ו-Z כארגומנטים. במקרים של הקצאות חד-ממדיות או דו-ממדיות, אפשר להשמיט את הגודל של המאפיין Y או Z. לדוגמה, rsCreateAllocation_uchar4(16384) יוצר הקצאה חד-ממדית של 16,384 רכיבים, שכל אחד מהם הוא מסוג uchar4.
המערכת מנהלת את ההקצאות באופן אוטומטי. אין צורך לשחרר אותם באופן מפורש. עם זאת, אפשר להתקשר אל
rsClearObject(rs_allocation* alloc) כדי לציין שאתם כבר לא צריכים את ה-handle alloc להקצאה הבסיסית, כדי שהמערכת תוכל לפנות משאבים מוקדם ככל האפשר.
בקטע Writing a RenderScript Kernel יש דוגמה לליבת מערכת שמבצעת היפוך של תמונה. בדוגמה הבאה מוסבר איך להחיל יותר מאפקט אחד על תמונה באמצעות Single-Source RenderScript. הוא כולל ליבה נוספת, greyscale, שהופכת תמונה צבעונית לשחור-לבן. פונקציה שאפשר להפעיל process() מפעילה את שני הגרעינים האלה ברצף על תמונת קלט, ומפיקה תמונת פלט. ההקצאות של הקלט ושל הפלט מועברות כארגומנטים מהסוג
rs_allocation.
// File: singlesource.rs #pragma version(1) #pragma rs java_package_name(com.android.rssample) static const float4 weight = {0.299f, 0.587f, 0.114f, 0.0f}; uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) { uchar4 out = in; out.r = 255 - in.r; out.g = 255 - in.g; out.b = 255 - in.b; return out; } uchar4 RS_KERNEL greyscale(uchar4 in) { const float4 inF = rsUnpackColor8888(in); const float4 outF = (float4){ dot(inF, weight) }; return rsPackColorTo8888(outF); } void process(rs_allocation inputImage, rs_allocation outputImage) { const uint32_t imageWidth = rsAllocationGetDimX(inputImage); const uint32_t imageHeight = rsAllocationGetDimY(inputImage); rs_allocation tmp = rsCreateAllocation_uchar4(imageWidth, imageHeight); rsForEach(invert, inputImage, tmp); rsForEach(greyscale, tmp, outputImage); }
אפשר להפעיל את הפונקציה process() מ-Java או מ-Kotlin באופן הבא:
Kotlin
val RS: RenderScript = RenderScript.create(context) val script = ScriptC_singlesource(RS) val inputAllocation: Allocation = Allocation.createFromBitmapResource( RS, resources, R.drawable.image ) val outputAllocation: Allocation = Allocation.createTyped( RS, inputAllocation.type, Allocation.USAGE_SCRIPT or Allocation.USAGE_IO_OUTPUT ) script.invoke_process(inputAllocation, outputAllocation)
Java
// File SingleSource.java RenderScript RS = RenderScript.create(context); ScriptC_singlesource script = new ScriptC_singlesource(RS); Allocation inputAllocation = Allocation.createFromBitmapResource( RS, getResources(), R.drawable.image); Allocation outputAllocation = Allocation.createTyped( RS, inputAllocation.getType(), Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT); script.invoke_process(inputAllocation, outputAllocation);
בדוגמה הזו אפשר לראות איך אפשר להטמיע אלגוריתם שכולל שני הפעלות של ליבת המערכת, באופן מלא בשפת RenderScript עצמה. בלי Single-Source RenderScript, תצטרכו להפעיל את שני ליבות המערכת מקוד Java, להפריד בין הפעלות הליבה לבין הגדרות הליבה, ויהיה קשה יותר להבין את האלגוריתם כולו. קוד ה-RenderScript ממקור יחיד לא רק קל יותר לקריאה, אלא גם מבטל את המעבר בין Java לבין הסקריפט בהפעלות של ליבת המערכת. אלגוריתמים איטרטיביים מסוימים עשויים להפעיל ליבות מאות פעמים, ולכן התקורה של מעבר כזה היא משמעותית.
משתנים גלובליים בסקריפט
משתנה גלובלי בסקריפט הוא משתנה גלובלי רגיל שאינו static
בסקריפט (קובץ .rs). למשתנה גלובלי של סקריפט בשם var שמוגדר בקובץ filename.rs, תהיה שיטה get_var שמשתקפת במחלקה ScriptC_filename. אלא אם הערך של global
הוא const, תהיה גם שיטה
set_var.
לכל משתנה גלובלי בסקריפט יש שני ערכים נפרדים – ערך Java וערך סקריפט. הערכים האלה מתנהגים באופן הבא:
- אם ל-var יש מאתחל סטטי בסקריפט, הוא מציין את הערך הראשוני של var גם ב-Java וגם בסקריפט. אחרת, הערך ההתחלתי הוא אפס.
- גישות אל var בתוך התסריט קוראות וכותבות את ערך התסריט שלו.
- השיטה
get_varקוראת את הערך של Java. - השיטה
set_var(אם היא קיימת) כותבת את הערך של Java באופן מיידי, ואת הערך של הסקריפט באופן אסינכרוני.
הערה: המשמעות היא שערכים שנכתבים לגלובל מתוך סקריפט לא גלויים ל-Java, למעט מאתחלים סטטיים בסקריפט.
הסבר מפורט על ליבות של הפחתה
צמצום הוא תהליך של שילוב אוסף נתונים לערך יחיד. זוהי פרימיטיב שימושי בתכנות מקבילי, עם יישומים כמו:
- חישוב הסכום או המכפלה של כל הנתונים
- חישוב פעולות לוגיות (
and,or,xor) על כל הנתונים - חיפוש הערך המינימלי או המקסימלי בנתונים
- חיפוש ערך ספציפי או הקואורדינטה של ערך ספציפי בנתונים
ב-Android 7.0 (רמת API 24) ואילך, RenderScript תומך בליבות של הפחתה כדי לאפשר אלגוריתמים יעילים של הפחתה שנכתבו על ידי המשתמש. אפשר להפעיל ליבות של הפחתה על קלט עם מימד אחד, שניים או שלושה.
בדוגמה שלמעלה מוצג גרעין פשוט של addint.
הנה ליבת צמצום findMinAndMax מורכבת יותר
שמוצאת את המיקומים של ערכי המינימום והמקסימום long ב-Allocation חד-ממדי:
#define LONG_MAX (long)((1UL << 63) - 1) #define LONG_MIN (long)(1UL << 63) #pragma rs reduce(findMinAndMax) \ initializer(fMMInit) accumulator(fMMAccumulator) \ combiner(fMMCombiner) outconverter(fMMOutConverter) // Either a value and the location where it was found, or INITVAL. typedef struct { long val; int idx; // -1 indicates INITVAL } IndexedVal; typedef struct { IndexedVal min, max; } MinAndMax; // In discussion below, this initial value { { LONG_MAX, -1 }, { LONG_MIN, -1 } } // is called INITVAL. static void fMMInit(MinAndMax *accum) { accum->min.val = LONG_MAX; accum->min.idx = -1; accum->max.val = LONG_MIN; accum->max.idx = -1; } //---------------------------------------------------------------------- // In describing the behavior of the accumulator and combiner functions, // it is helpful to describe hypothetical functions // IndexedVal min(IndexedVal a, IndexedVal b) // IndexedVal max(IndexedVal a, IndexedVal b) // MinAndMax minmax(MinAndMax a, MinAndMax b) // MinAndMax minmax(MinAndMax accum, IndexedVal val) // // The effect of // IndexedVal min(IndexedVal a, IndexedVal b) // is to return the IndexedVal from among the two arguments // whose val is lesser, except that when an IndexedVal // has a negative index, that IndexedVal is never less than // any other IndexedVal; therefore, if exactly one of the // two arguments has a negative index, the min is the other // argument. Like ordinary arithmetic min and max, this function // is commutative and associative; that is, // // min(A, B) == min(B, A) // commutative // min(A, min(B, C)) == min((A, B), C) // associative // // The effect of // IndexedVal max(IndexedVal a, IndexedVal b) // is analogous (greater . . . never greater than). // // Then there is // // MinAndMax minmax(MinAndMax a, MinAndMax b) { // return MinAndMax(min(a.min, b.min), max(a.max, b.max)); // } // // Like ordinary arithmetic min and max, the above function // is commutative and associative; that is: // // minmax(A, B) == minmax(B, A) // commutative // minmax(A, minmax(B, C)) == minmax((A, B), C) // associative // // Finally define // // MinAndMax minmax(MinAndMax accum, IndexedVal val) { // return minmax(accum, MinAndMax(val, val)); // } //---------------------------------------------------------------------- // This function can be explained as doing: // *accum = minmax(*accum, IndexedVal(in, x)) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // *accum is INITVAL, then this function sets // *accum = IndexedVal(in, x) // // After this function is called, both accum->min.idx and accum->max.idx // will have nonnegative values: // - x is always nonnegative, so if this function ever sets one of the // idx fields, it will set it to a nonnegative value // - if one of the idx fields is negative, then the corresponding // val field must be LONG_MAX or LONG_MIN, so the function will always // set both the val and idx fields static void fMMAccumulator(MinAndMax *accum, long in, int x) { IndexedVal me; me.val = in; me.idx = x; if (me.val <= accum->min.val) accum->min = me; if (me.val >= accum->max.val) accum->max = me; } // This function can be explained as doing: // *accum = minmax(*accum, *val) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // one of the two accumulator data items is INITVAL, then this // function sets *accum to the other one. static void fMMCombiner(MinAndMax *accum, const MinAndMax *val) { if ((accum->min.idx < 0) || (val->min.val < accum->min.val)) accum->min = val->min; if ((accum->max.idx < 0) || (val->max.val > accum->max.val)) accum->max = val->max; } static void fMMOutConverter(int2 *result, const MinAndMax *val) { result->x = val->min.idx; result->y = val->max.idx; }
הערה: כאן אפשר למצוא עוד דוגמאות לליבות של הפחתת מימדים.
כדי להפעיל ליבת צמצום, סביבת זמן הריצה של RenderScript יוצרת משתנה אחד או יותר
בשם פריטי נתונים של צובר כדי לשמור את המצב של תהליך הצמצום. סביבת זמן הריצה של RenderScript בוחרת את מספר פריטי הנתונים של המצבר באופן שממקסם את הביצועים. הסוג של פריטי הנתונים של המצבר (accumType) נקבע על ידי פונקציית המצבר של ליבת המערכת – הארגומנט הראשון של הפונקציה הזו הוא מצביע לפריט נתונים של המצבר. כברירת מחדל, כל פריט נתונים של צובר מאותחל לאפס (כמו ב-memset), אבל אפשר לכתוב פונקציית אתחול כדי לבצע פעולה אחרת.
דוגמה: בגרעין addint, פריטי הנתונים של האוגר (מסוג int) משמשים לסיכום ערכי הקלט. אין פונקציית אתחול, ולכן כל פריט נתונים של צובר מאותחל לאפס.
דוגמה: בגרעין findMinAndMax, פריטי הנתונים של המצבר (מהסוג MinAndMax) משמשים למעקב אחרי הערכים המינימליים והמקסימליים שנמצאו עד כה. יש פונקציית אתחול להגדרת הערכים האלה ל-LONG_MAX ול-LONG_MIN, בהתאמה, ולהגדרת המיקומים של הערכים האלה ל-1-, מה שמציין שהערכים לא נמצאים בפועל בחלק (הריק) של הקלט שעבר עיבוד.
RenderScript קורא לפונקציית האקומולטור פעם אחת לכל קואורדינטה בקלט. בדרך כלל, הפונקציה צריכה לעדכן את פריט הנתונים של המצבר בצורה כלשהי בהתאם לקלט.
דוגמה: בגרעין addint, פונקציית האקומולטור מוסיפה את הערך של רכיב קלט לפריט נתוני האקומולטור.
דוגמה: בגרעין findMinAndMax, פונקציית המצבר בודקת אם הערך של רכיב קלט קטן מהערך המינימלי שמתועד בפריט הנתונים של המצבר או שווה לו, ו/או גדול מהערך המקסימלי שמתועד בפריט הנתונים של המצבר או שווה לו, ומעדכנת את פריט הנתונים של המצבר בהתאם.
אחרי שהפונקציה המצטברת נקראת פעם אחת לכל קואורדינטה בקלט, RenderScript צריך לשלב את פריטי הנתונים המצטברים לפריט נתונים מצטבר יחיד. אפשר לכתוב פונקציית combiner כדי לעשות את זה. אם לפונקציית המצבר יש קלט יחיד ואין לה ארגומנטים מיוחדים, לא צריך לכתוב פונקציית שילוב. RenderScript ישתמש בפונקציית המצבר כדי לשלב את פריטי הנתונים של המצבר. (אפשר עדיין לכתוב פונקציית שילוב אם התנהגות ברירת המחדל הזו לא מתאימה לכם).
דוגמה: בגרעין addint אין פונקציית צירוף, ולכן נעשה שימוש בפונקציית האקומולטור. זהו ההתנהגות הנכונה, כי אם מפצלים אוסף של ערכים לשני חלקים, וסוכמים את הערכים בשני החלקים בנפרד, סכום שני הסכומים שווה לסכום של כל הערכים באוסף.
דוגמה: בגרעין findMinAndMax, פונקציית ה-combiner בודקת אם הערך המינימלי שמתועד בפריט הנתונים של המצבר source *val קטן מהערך המינימלי שמתועד בפריט הנתונים של המצבר destination *accum, ומעדכנת את *accum בהתאם. הוא מבצע פעולה דומה לגבי הערך המקסימלי. הפעולה הזו מעדכנת את *accum
למצב שהיה מתקבל אם כל ערכי הקלט היו מצטברים ב-*accum ולא אם חלק מהם היו מצטברים ב-*accum וחלק ב-*val.
אחרי שכל פריטי הנתונים של האקומולטור משולבים, RenderScript קובע את תוצאת הרדוקציה כדי להחזיר אותה ל-Java. אפשר לכתוב פונקציית outconverter כדי לעשות את זה. אין צורך לכתוב פונקציית outconverter אם רוצים שהערך הסופי של פריטי הנתונים המצטברים המשולבים יהיה התוצאה של הפעולה.
דוגמה: בגרעין addint, אין פונקציית outconverter. הערך הסופי של פריטי הנתונים המשולבים הוא סכום כל הרכיבים של הקלט, שהוא הערך שאנחנו רוצים להחזיר.
דוגמה: בגרעין findMinAndMax, הפונקציה outconverter מאתחלת ערך תוצאה int2 כדי להחזיק את המיקומים של הערכים המינימליים והמקסימליים שמתקבלים משילוב של כל פריטי הנתונים של המצבר.
כתיבת ליבת צמצום
הפונקציה #pragma rs reduce מגדירה ליבת צמצום על ידי ציון השם שלה והשמות והתפקידים של הפונקציות שמרכיבות את הליבה. כל הפונקציות האלה צריכות להיות static. ליבת צמצום תמיד דורשת פונקציה accumulator. אפשר להשמיט חלק מהפונקציות האחרות או את כולן, בהתאם לפעולה שרוצים שהליבה תבצע.
#pragma rs reduce(kernelName) \ initializer(initializerName) \ accumulator(accumulatorName) \ combiner(combinerName) \ outconverter(outconverterName)
המשמעות של הפריטים ב-#pragma היא כדלקמן:
-
reduce(kernelName)(חובה): מציין שמוגדרת ליבת צמצום. שיטת Java שמשקפת אתreduce_kernelNameתפעיל את ליבת המערכת.
initializer(initializerName)(אופציונלי): מציין את השם של פונקציית האתחול של ליבת הצמצום הזו. כשמפעילים את הליבה, RenderScript קוראת לפונקציה הזו פעם אחת לכל פריט נתונים של צובר. הפונקציה צריכה להיות מוגדרת כך:static void initializerName(accumType *accum) { … }
accumהוא מצביע לפריט נתונים של צובר שצריך לאתחל עבור הפונקציה הזו.אם לא מספקים פונקציית אתחול, RenderScript מאתחל כל פריט נתונים של צובר לאפס (כמו באמצעות
memset), כאילו הייתה פונקציית אתחול שנראית כך:static void initializerName(accumType *accum) { memset(accum, 0, sizeof(*accum)); }
accumulator(accumulatorName) (חובה): מציין את השם של פונקציית האקומולטור עבור ליבת הרדוקציה הזו. כשמפעילים את הליבה, RenderScript קורא לפונקציה הזו פעם אחת לכל קואורדינטה בקלט, כדי לעדכן פריט נתונים מצטבר בדרך כלשהי בהתאם לקלט. הפונקציה צריכה להיות מוגדרת כך:static void accumulatorName(accumType *accum, in1Type in1, …, inNType inN [, specialArguments]) { … }
accumהוא מצביע לפריט נתונים של צובר שהפונקציה הזו יכולה לשנות. in1עדinNהם ארגומנט אחד או יותר שמולאו באופן אוטומטי על סמך הקלט שהועבר להפעלת הליבה, ארגומנט אחד לכל קלט. פונקציית האקומולטור יכולה לקבל גם ארגומנטים מיוחדים.דוגמה לליבה עם כמה קלטים היא
dotProduct.combiner(combinerName)(optional): מציין את השם של פונקציית השילוב עבור הליבה של הפחתת הערכים הזו. אחרי ש-RenderScript קורא לפונקציית האקומולטור פעם אחת לכל קואורדינטה בקלט, הוא קורא לפונקציה הזו כמה פעמים שצריך כדי לשלב את כל פריטי הנתונים של האקומולטור לפריט נתונים אחד של האקומולטור. הפונקציה צריכה להיות מוגדרת כך:
static void combinerName(accumType *accum, const accumType *other) { … }
accumהוא מצביע לפריט נתונים של צובר 'יעד' שהפונקציה הזו תשנה. otherהוא מצביע לפריט נתונים של צובר 'מקור' שהפונקציה הזו 'משלבת' לתוך*accum.הערה: יכול להיות שהמשתנים
*accum,*otherאו שניהם אותחלו אבל מעולם לא הועברו לפונקציית הצבירה, כלומר, אחד מהם או שניהם מעולם לא עודכנו בהתאם לנתוני קלט כלשהם. לדוגמה, בגרעין findMinAndMax, פונקציית ה-combinerfMMCombinerבודקת במפורש אם הערך הואidx < 0, כי זה מציין פריט נתונים של מצבר כזה, שהערך שלו הוא INITVAL.אם לא מספקים פונקציית שילוב, RenderScript משתמש בפונקציית האקומולטור במקומה, כאילו הייתה פונקציית שילוב שנראית כך:
static void combinerName(accumType *accum, const accumType *other) { accumulatorName(accum, *other); }
פונקציית שילוב היא חובה אם לליבה יש יותר מקלט אחד, אם סוג נתוני הקלט לא זהה לסוג נתוני המצבר, או אם פונקציית המצבר מקבלת ארגומנטים מיוחדים אחד או יותר.
outconverter(outconverterName)(אופציונלי): מציין את השם של פונקציית ה-outconverter עבור ליבת הצמצום הזו. אחרי ש-RenderScript משלב את כל פריטי הנתונים של המצבר, הוא קורא לפונקציה הזו כדי לקבוע את תוצאת הצמצום שצריך להחזיר ל-Java. הפונקציה צריכה להיות מוגדרת כך:static void outconverterName(resultType *result, const accumType *accum) { … }
resultהוא מצביע לפריט נתונים של תוצאה (שהוקצה אבל לא אותחל על ידי זמן הריצה של RenderScript) שהפונקציה הזו מאתחלת עם התוצאה של הצמצום. resultType הוא הסוג של פריט הנתונים הזה, שלא חייב להיות זהה ל-accumType. accumהוא מצביע לפריט הנתונים הסופי של צובר שהמערכת חישבה באמצעות פונקציית ה-combiner.אם לא מספקים פונקציית outconverter, RenderScript מעתיקה את פריט הנתונים הסופי של המצבר לפריט הנתונים של התוצאה, כאילו הייתה פונקציית outconverter שנראית כך:
static void outconverterName(accumType *result, const accumType *accum) { *result = *accum; }
אם רוצים סוג תוצאה שונה מסוג הנתונים של המצבר, חובה להשתמש בפונקציה outconverter.
שימו לב: לליבה יש סוגי קלט, סוג פריט נתונים של צובר וסוג תוצאה, ואף אחד מהם לא צריך להיות זהה. לדוגמה, בגרעין findMinAndMax, סוג הקלט long, סוג פריט הנתונים של צובר MinAndMax וסוג התוצאה int2 שונים זה מזה.
מה אסור להניח?
אסור להסתמך על מספר פריטי הנתונים של המצבר שנוצרו על ידי RenderScript להפעלת ליבה נתונה. אין ערובה לכך ששתי הפעלות של אותו ליבה עם אותם נתוני קלט ייצרו את אותו מספר של פריטי נתונים של צובר.
אסור להסתמך על הסדר שבו RenderScript קורא לפונקציות initializer, accumulator ו-combiner. יכול להיות שהוא אפילו יקרא לחלק מהן במקביל. אין ערובה לכך ששתי הפעלות של אותו ליבה עם אותו קלט יפעלו באותו סדר. הדבר היחיד שמובטח הוא שרק פונקציית האתחול תראה אי פעם פריט נתונים של צובר לא מאותחל. לדוגמה:
- אין ערובה לכך שכל פריטי הנתונים של המצבר יאותחלו לפני הקריאה לפונקציית המצבר, למרות שהקריאה תתבצע רק לפריט נתונים מאותחל של המצבר.
- אין ערובה לסדר שבו רכיבי הקלט מועברים לפונקציית המצבר.
- אין ערובה לכך שהפונקציה המצטברת נקראה עבור כל רכיבי הקלט לפני שהפונקציה המשולבת נקראת.
אחת מההשלכות של זה היא שהליבה findMinAndMax לא דטרמיניסטית: אם הקלט מכיל יותר ממופע אחד של אותו ערך מינימלי או מקסימלי, אין לכם דרך לדעת איזה מופע הליבה תמצא.
מה צריך להבטיח?
מערכת RenderScript יכולה לבחור לבצע ליבת מערכת בדרכים רבות ושונות, ולכן צריך לפעול לפי כללים מסוימים כדי לוודא שהליבה מתנהגת כמו שרוצים. אם לא תפעלו לפי הכללים האלה, יכול להיות שתקבלו תוצאות שגויות, התנהגות לא דטרמיניסטית או שגיאות בזמן ריצה.
בכללים שלמטה מצוין לעיתים קרובות ששני פריטי נתונים של צובר חייבים להיות בעלי אותו ערך. מה זה אומר? זה תלוי במה שרוצים שהליבה תעשה. לצמצום מתמטי כמו addint, בדרך כלל הגיוני ש'זהה' יתייחס לשוויון מתמטי. בחיפוש מסוג 'בחירת כל האפשרויות', כמו findMinAndMax (חיפוש המיקום של ערכי הקלט המינימליים והמקסימליים), שבו יכול להיות יותר ממופע אחד של ערכי קלט זהים, כל המיקומים של ערך קלט נתון נחשבים 'זהים'. אפשר לכתוב ליבת הפעלה דומה לזו של 'מציאת המיקום של ערכי הקלט המינימליים והמקסימליים הכי שמאליים', שבה (לדוגמה) ערך מינימלי במיקום 100 עדיף על ערך מינימלי זהה במיקום 200. בליבה הזו, 'זהה' יתייחס למיקום זהה, ולא רק לערך זהה, ופונקציות האקומולטור והקומביינר יהיו שונות מאלה של findMinAndMax.
פונקציית האתחול חייבת ליצור ערך זהות. כלומר, אםI ו-A הם פריטי נתונים של צובר שהופעלו על ידי פונקציית ההפעלה, ואם I מעולם לא הועבר לפונקציית הצובר (אבל יכול להיות ש-A הועבר), אז
דוגמה: בגרעין addint, פריט נתונים של צובר מאותחל לאפס. הפונקציה המשולבת של ליבת ה-kernel הזו מבצעת חיבור; אפס הוא ערך הזהות של החיבור.
דוגמה: בגרעין findMinAndMax, פריט נתונים של צובר מאותחל ל-INITVAL.
- הפונקציה
fMMCombiner(&A, &I)משאירה אתAללא שינוי, כיIהואINITVAL. -
fMMCombiner(&I, &A)מגדיר אתIלערךA, כיIהואINITVAL.
לכן, INITVAL הוא אכן ערך של זהות.
פונקציית השילוב חייבת להיות קומוטטיבית. כלומר, אם A ו-B הם פריטי נתונים של צובר שאותחלו על ידי פונקציית האתחול, ואולי הועברו לפונקציית הצובר אפס פעמים או יותר, אז combinerName(&A, &B) חייבת להגדיר את A לאותו ערך שמוגדר על ידי combinerName(&B, &A) עבור B.
דוגמה: בגרעין addint, פונקציית הצירוף מוסיפה את שני הערכים של פריטי הנתונים של המצבר. פעולת החיבור היא קומוטטיבית.
דוגמה: בגרעין findMinAndMax, fMMCombiner(&A, &B) זהה ל-A = minmax(A, B), ו-minmax הוא קומוטטיבי, ולכן גם fMMCombiner הוא קומוטטיבי.
פונקציית השילוב חייבת להיות אסוציאטיבית. כלומר, אם A, B ו-C הם פריטי נתונים של מצבר שאותחלו על ידי פונקציית האתחול, ואולי הועברו לפונקציית המצבר אפס פעמים או יותר, אז שתי רצפי הקוד הבאים חייבים להגדיר את A לאותו ערך:
combinerName(&A, &B); combinerName(&A, &C);
combinerName(&B, &C); combinerName(&A, &B);
דוגמה: בגרעין addint, פונקציית הצירוף מוסיפה את שני הערכים של פריטי הנתונים של צובר:
A = A + B A = A + C // Same as // A = (A + B) + C
B = B + C A = A + B // Same as // A = A + (B + C) // B = B + C
פעולת החיבור היא אסוציאטיבית, ולכן גם פונקציית הצירוף היא אסוציאטיבית.
דוגמה: בגרעין findMinAndMax,
fMMCombiner(&A, &B)
A = minmax(A, B)
A = minmax(A, B) A = minmax(A, C) // Same as // A = minmax(minmax(A, B), C)
B = minmax(B, C) A = minmax(A, B) // Same as // A = minmax(A, minmax(B, C)) // B = minmax(B, C)
הפעולה minmax היא אסוציאטיבית, ולכן גם הפעולה fMMCombiner היא אסוציאטיבית.
הפונקציה המצטברת והפונקציה המשולבת חייבות יחד לפעול לפי כלל הקיפול הבסיסי. כלומר, אם A
ו-B הם פריטי נתונים של צובר, A אותחל על ידי פונקציית האתחול ויכול להיות שהוא הועבר לפונקציית הצובר אפס פעמים או יותר, B לא אותחל, ו-args הוא רשימת ארגומנטים של קלט וארגומנטים מיוחדים לקריאה מסוימת לפונקציית הצובר, אז שתי רצפי הקוד הבאים חייבים להגדיר את A לאותו ערך:
accumulatorName(&A, args); // statement 1
initializerName(&B); // statement 2 accumulatorName(&B, args); // statement 3 combinerName(&A, &B); // statement 4
דוגמה: בגרעין addint, עבור ערך קלט V:
- ההצהרה 1 זהה ל
A += V - ההצהרה השנייה זהה ל
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)) - ההצהרה השנייה זהה ל
B = INITVAL - ההצהרה 3 זהה ל
שהוא, בגלל ש-B הוא הערך ההתחלתי, זהה ל-B = minmax(B, IndexedVal(V, X))
B = IndexedVal(V, X)
- הטענה 4 זהה לטענה
שזה כמוA = minmax(A, B)
A = minmax(A, IndexedVal(V, X))
ההצהרות 1 ו-4 מגדירות את A לאותו ערך, ולכן ליבת המערכת פועלת לפי כלל הקיפול הבסיסי.
הפעלת ליבת צמצום מקוד Java
עבור ליבת צמצום בשם kernelName שמוגדרת בקובץ filename.rs, יש שלוש שיטות שמשתקפות במחלקה ScriptC_filename:
Kotlin
// Function 1 fun reduce_kernelName(ain1: Allocation, …, ainN: Allocation): javaFutureType // Function 2 fun reduce_kernelName(ain1: Allocation, …, ainN: Allocation, sc: Script.LaunchOptions): javaFutureType // Function 3 fun reduce_kernelName(in1: Array<devecSiIn1Type>, …, inN: Array<devecSiInNType>): javaFutureType
Java
// Method 1 public javaFutureType reduce_kernelName(Allocation ain1, …, Allocation ainN); // Method 2 public javaFutureType reduce_kernelName(Allocation ain1, …, Allocation ainN, Script.LaunchOptions sc); // Method 3 public javaFutureType reduce_kernelName(devecSiIn1Type[] in1, …, devecSiInNType[] inN);
הנה כמה דוגמאות לקריאה לליבת addint:
Kotlin
val script = ScriptC_example(renderScript) // 1D array // and obtain answer immediately val input1 = intArrayOf(…) val sum1: Int = script.reduce_addint(input1).get() // Method 3 // 2D allocation // and do some additional work before obtaining answer val typeBuilder = Type.Builder(RS, Element.I32(RS)).apply { setX(…) setY(…) } val input2: Allocation = Allocation.createTyped(RS, typeBuilder.create()).also { populateSomehow(it) // fill in input Allocation with data } val result2: ScriptC_example.result_int = script.reduce_addint(input2) // Method 1 doSomeAdditionalWork() // might run at same time as reduction val sum2: Int = result2.get()
Java
ScriptC_example script = new ScriptC_example(renderScript); // 1D array // and obtain answer immediately int input1[] = …; int sum1 = script.reduce_addint(input1).get(); // Method 3 // 2D allocation // and do some additional work before obtaining answer Type.Builder typeBuilder = new Type.Builder(RS, Element.I32(RS)); typeBuilder.setX(…); typeBuilder.setY(…); Allocation input2 = createTyped(RS, typeBuilder.create()); populateSomehow(input2); // fill in input Allocation with data ScriptC_example.result_int result2 = script.reduce_addint(input2); // Method 1 doSomeAdditionalWork(); // might run at same time as reduction int sum2 = result2.get();
לשיטה 1 יש ארגומנט קלט Allocation אחד לכל ארגומנט קלט בפונקציית הצבירה של ליבת המערכת. סביבת זמן הריצה של RenderScript בודקת כדי לוודא שלכל ההקצאות של הקלט יש את אותם ממדים, ושהסוג Element של כל אחת מההקצאות של הקלט תואם לסוג של ארגומנט הקלט התואם של אב הטיפוס של פונקציית המצבר. אם אחת מהבדיקות האלה נכשלת, RenderScript זורק חריג. הליבה פועלת על כל קואורדינטה בממדים האלה.
שיטה 2 זהה לשיטה 1, אבל בשיטה 2 יש ארגומנט נוסף sc שאפשר להשתמש בו כדי להגביל את הביצוע של הליבה לקבוצת משנה של הקואורדינטות.
שיטה 3 זהה לשיטה 1, אלא שבמקום לקבל קלט של הקצאה היא מקבלת קלט של מערך Java. השימוש בשיטה הזו חוסך לכם את הצורך לכתוב קוד כדי ליצור הקצאה ולהעתיק אליה נתונים ממערך Java. עם זאת, שימוש בשיטה 3 במקום בשיטה 1 לא משפר את הביצועים של הקוד. לכל מערך קלט, שיטה 3 יוצרת הקצאה זמנית חד-ממדית עם סוג Element מתאים ועם setAutoPadding(boolean) מופעל, ומעתיקה את המערך להקצאה כאילו באמצעות שיטת copyFrom() המתאימה של Allocation. לאחר מכן, הוא קורא ל-Method 1 ומעביר את ההקצאות הזמניות האלה.
הערה: אם האפליקציה שלכם תבצע כמה קריאות לליבת המערכת עם אותה מערך, או עם מערכים שונים באותם ממדים ובאותו סוג רכיב, יכול להיות שתצליחו לשפר את הביצועים אם תיצרו, תאכלסו ותשתמשו מחדש בהקצאות בעצמכם, במקום להשתמש בשיטה 3.
javaFutureType, סוג ההחזרה של שיטות ההפחתה המשתקפות, הוא מחלקה סטטית מקוננת משתקפת בתוך המחלקה ScriptC_filename. הוא מייצג את התוצאה העתידית של הפעלה של ליבת הפחתה. כדי לקבל את התוצאה בפועל של ההרצה, קוראים ל-method get() של המחלקה הזו, שמחזירה ערך מסוג javaResultType. get() היא סינכרונית.
Kotlin
class ScriptC_filename(rs: RenderScript) : ScriptC(…) { object javaFutureType { fun get(): javaResultType { … } } }
Java
public class ScriptC_filename extends ScriptC { public static class javaFutureType { public javaResultType get() { … } } }
הערך של javaResultType נקבע לפי הערך של resultType של הפונקציה outconverter. אלא אם resultType הוא סוג לא חתום (סקלר, וקטור או מערך), javaResultType הוא סוג Java שתואם ישירות. אם resultType הוא סוג לא חתום ויש סוג חתום גדול יותר של Java, אז javaResultType הוא סוג חתום גדול יותר של Java; אחרת, הוא סוג Java התואם ישירות. לדוגמה:
- אם הערך של resultType הוא
int,int2אוint[15], אז הערך של javaResultType הואint,Int2אוint[]. כל הערכים של resultType יכולים להיות מיוצגים על ידי javaResultType. - אם הערך של resultType הוא
uint,uint2אוuint[15], אז הערך של javaResultType הואlong,Long2אוlong[]. כל הערכים של resultType יכולים להיות מיוצגים על ידי javaResultType. - אם הערך של resultType הוא
ulong,ulong2אוulong[15], אז הערך של javaResultType הואlong,Long2אוlong[]. יש ערכים מסוימים של resultType שלא ניתן לייצג באמצעות javaResultType.
javaFutureType הוא סוג התוצאה העתידי שמתאים ל-resultType של הפונקציה outconverter.
- אם resultType הוא לא סוג של מערך, אז javaFutureType
הוא
result_resultType. - אם resultType הוא מערך באורך Count עם רכיבים מהסוג memberType,
אז javaFutureType הוא
resultArrayCount_memberType.
לדוגמה:
Kotlin
class ScriptC_filename(rs: RenderScript) : ScriptC(…) { // for kernels with int result object result_int { fun get(): Int = … } // for kernels with int[10] result object resultArray10_int { fun get(): IntArray = … } // for kernels with int2 result // note that the Kotlin type name "Int2" is not the same as the script type name "int2" object result_int2 { fun get(): Int2 = … } // for kernels with int2[10] result // note that the Kotlin type name "Int2" is not the same as the script type name "int2" object resultArray10_int2 { fun get(): Array<Int2> = … } // for kernels with uint result // note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint" object result_uint { fun get(): Long = … } // for kernels with uint[10] result // note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint" object resultArray10_uint { fun get(): LongArray = … } // for kernels with uint2 result // note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2" object result_uint2 { fun get(): Long2 = … } // for kernels with uint2[10] result // note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2" object resultArray10_uint2 { fun get(): Array<Long2> = … } }
Java
public class ScriptC_filename extends ScriptC { // for kernels with int result public static class result_int { public int get() { … } } // for kernels with int[10] result public static class resultArray10_int { public int[] get() { … } } // for kernels with int2 result // note that the Java type name "Int2" is not the same as the script type name "int2" public static class result_int2 { public Int2 get() { … } } // for kernels with int2[10] result // note that the Java type name "Int2" is not the same as the script type name "int2" public static class resultArray10_int2 { public Int2[] get() { … } } // for kernels with uint result // note that the Java type "long" is a wider signed type than the unsigned script type "uint" public static class result_uint { public long get() { … } } // for kernels with uint[10] result // note that the Java type "long" is a wider signed type than the unsigned script type "uint" public static class resultArray10_uint { public long[] get() { … } } // for kernels with uint2 result // note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2" public static class result_uint2 { public Long2 get() { … } } // for kernels with uint2[10] result // note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2" public static class resultArray10_uint2 { public Long2[] get() { … } } }
אם javaResultType הוא סוג אובייקט (כולל סוג מערך), כל קריאה ל-javaFutureType.get() באותו מופע תחזיר את אותו אובייקט.
אם javaResultType לא יכול לייצג את כל הערכים של הסוג resultType, וליבת צמצום מייצרת ערך שלא ניתן לייצוג, אז הפונקציה javaFutureType.get() זורקת חריגה.
שיטה 3 ו-devecSiInXType
devecSiInXType הוא סוג Java שמתאים ל-inXType של הארגומנט המתאים של פונקציית המצבר. אלא אם inXType הוא סוג לא חתום או סוג וקטורי, devecSiInXType הוא סוג Java התואם ישירות. אם inXType הוא סוג סקלרי לא חתום, אז devecSiInXType הוא סוג Java שמתאים ישירות לסוג הסקלרי החתום באותו גודל. אם inXType הוא סוג וקטור חתום, אז devecSiInXType הוא סוג Java שמתאים ישירות לסוג רכיב הווקטור. אם inXType הוא סוג וקטור לא חתום, אז devecSiInXType הוא סוג Java שתואם ישירות לסוג הסקלר החתום באותו גודל כמו סוג רכיב הווקטור. לדוגמה:
- אם הערך של inXType הוא
int, אז הערך של devecSiInXType הואint. - אם הערך של inXType הוא
int2, אז הערך של devecSiInXType הואint. המערך הוא ייצוג שטוח: יש בו פי שניים רכיבי סקלר מאשר ברכיבי הווקטור של ההקצאה, שכוללים 2 רכיבים. השיטה הזו זהה לשיטותcopyFrom()שלAllocation. - אם הערך של inXType הוא
uint, אז הערך של deviceSiInXType הואint. ערך חתום במערך Java מתפרש כערך לא חתום של אותו דפוס ביטים בהקצאה. השיטה הזו זהה לשיטותcopyFrom()שלAllocation. - אם הערך של inXType הוא
uint2, אז הערך של deviceSiInXType הואint. השילוב הזה מייצג את האופן שבו מטפלים ב-int2וב-uint: המערך הוא ייצוג שטוח, והערכים החתומים של מערך Java מפורשים כערכים לא חתומים של Element ב-RenderScript.
שימו לב שבשיטה 3, סוגי הקלט מטופלים באופן שונה מסוגי התוצאות:
- קלט וקטורי של סקריפט משוטח בצד Java, אבל תוצאה וקטורית של סקריפט לא משוטחת.
- קלט לא חתום של סקריפט מיוצג כקלט חתום באותו גודל בצד של Java, ואילו תוצאה לא חתומה של סקריפט מיוצגת כסוג חתום מורחב בצד של Java (למעט במקרה של
ulong).
דוגמאות נוספות לליבות של צמצום
#pragma rs reduce(dotProduct) \ accumulator(dotProductAccum) combiner(dotProductSum) // Note: No initializer function -- therefore, // each accumulator data item is implicitly initialized to 0.0f. static void dotProductAccum(float *accum, float in1, float in2) { *accum += in1*in2; } // combiner function static void dotProductSum(float *accum, const float *val) { *accum += *val; }
// Find a zero Element in a 2D allocation; return (-1, -1) if none #pragma rs reduce(fz2) \ initializer(fz2Init) \ accumulator(fz2Accum) combiner(fz2Combine) static void fz2Init(int2 *accum) { accum->x = accum->y = -1; } static void fz2Accum(int2 *accum, int inVal, int x /* special arg */, int y /* special arg */) { if (inVal==0) { accum->x = x; accum->y = y; } } static void fz2Combine(int2 *accum, const int2 *accum2) { if (accum2->x >= 0) *accum = *accum2; }
// Note that this kernel returns an array to Java #pragma rs reduce(histogram) \ accumulator(hsgAccum) combiner(hsgCombine) #define BUCKETS 256 typedef uint32_t Histogram[BUCKETS]; // Note: No initializer function -- // therefore, each bucket is implicitly initialized to 0. static void hsgAccum(Histogram *h, uchar in) { ++(*h)[in]; } static void hsgCombine(Histogram *accum, const Histogram *addend) { for (int i = 0; i < BUCKETS; ++i) (*accum)[i] += (*addend)[i]; } // Determines the mode (most frequently occurring value), and returns // the value and the frequency. // // If multiple values have the same highest frequency, returns the lowest // of those values. // // Shares functions with the histogram reduction kernel. #pragma rs reduce(mode) \ accumulator(hsgAccum) combiner(hsgCombine) \ outconverter(modeOutConvert) static void modeOutConvert(int2 *result, const Histogram *h) { uint32_t mode = 0; for (int i = 1; i < BUCKETS; ++i) if ((*h)[i] > (*h)[mode]) mode = i; result->x = mode; result->y = (*h)[mode]; }
דוגמאות קוד נוספות
בדוגמאות BasicRenderScript, RenderScriptIntrinsic ו-Hello Compute מוצגות דוגמאות נוספות לשימוש בממשקי ה-API שמוסברים בדף הזה.