RenderScript היא מסגרת להרצת משימות שמתבצעות בעיקר באמצעות חישובים, עם ביצועים גבוהים ב-Android. RenderScript מיועד בעיקר לשימוש עם חישוב מקבילי של נתונים, אבל עומסי עבודה טוריים יכולים להפיק ממנו תועלת גם כן. סביבת זמן הריצה של RenderScript מבצעת עבודה במקביל במעבדים שזמינים במכשיר, כמו מעבדי CPU ו-GPU עם כמה ליבות. כך תוכלו להתמקד בביטוי האלגוריתמים במקום בתזמון העבודה. RenderScript הוא הם שימושיים במיוחד לאפליקציות שמבצעים עיבוד תמונות, צילום חישובי ראייה ממוחשבת.
כדי להתחיל ב-RenderScript, יש שני מושגים עיקריים שצריך להבין:
- השפה עצמה היא שפה שמבוססת על C99, המשמשת לכתיבת קוד מחשוב עתיר ביצועים. במאמר כתיבה של ליבה של RenderScript מוסבר איך משתמשים בה כדי לכתוב ליבות מחשוב.
- Control API משמש לניהול משך החיים של משאבי RenderScript שליטה בביצוע הליבה. הוא זמין בשלושה שפות שונות: Java, C++ ב-Android NDK ושפת הליבה עצמה שמבוססת על C99. שימוש ב-RenderScript מ-Java Code וגם Single-Source RenderScript מתאר את המשפט הראשון והשלישי בהתאמה,
כתיבת ליבה של RenderScript
ליבה של RenderScript נמצאת בדרך כלל בקובץ .rs
בספרייה <project_root>/src/rs
. כל קובץ .rs
נקרא סקריפט. כל סקריפט מכיל קבוצה משלו של ליבות, פונקציות ומשתנים. סקריפט יכול
מכילים:
- הצהרת פרגמה (
#pragma version(1)
) שמצהירה על הגרסה של שפת הליבה של RenderScript שנעשה בה שימוש בסקריפט הזה. בשלב הזה, 1 הוא הערך החוקי היחיד. - הצהרת פרגמה (
#pragma rs java_package_name(com.example.app)
) מצהירה על שם החבילה של מחלקות Java שמשתקפות מהסקריפט הזה. חשוב לזכור שקובץ.rs
חייב להיות חלק מחבילת האפליקציה, ולא בפרויקט ספרייה. - אפס פונקציות שניתנות להפעלה או יותר. פונקציה שניתן להפעיל היא פונקציית RenderScript עם ליבה יחידה, שאפשר לקרוא לה מקוד Java עם ארגומנטים שרירותיים. לרוב, הן שימושיות להגדרה ראשונית או לחישוב טורי בצינור עיבוד נתונים גדול יותר.
אפס או יותר שאלות ותשובות לסקריפטים. סקריפט גלובלי דומה למשתנה גלובלי ב-C. אפשר לגשת למשתנים גלובליים של סקריפטים מקוד Java, והם משמשים לרוב להעברת פרמטרים לליבת RenderScript. כאן יש הסבר מפורט על שאלות נפוצות בנושא סקריפט.
אפס או יותר ליבות מחשוב. ליבת מחשוב היא פונקציה או אוסף של פונקציות שאפשר להורות להן להפעיל במקביל את זמן הריצה של RenderScript באוסף של נתונים. יש שני סוגי מחשוב ליבות: מיפוי ליבה (kernel) (נקראת גם ליבה foreach) והפחתה של הליבה.
ליבה (kernel) של מיפוי היא פונקציה מקבילה שפועלת על אוסף של
Allocations
בעלי מאפיינים זהים. כברירת מחדל, הפונקציה מתבצעת פעם אחת לכל קואורדינטה במאפיינים האלה. בדרך כלל (אבל לא רק) משמש הופכים אוסף של קלטAllocations
פלטAllocation
אחדElement
בכל פעם בזמן האימון.דוגמה לליבה של מיפוי פשוטה:
uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) { uchar4 out = in; out.r = 255 - in.r; out.g = 255 - in.g; out.b = 255 - in.b; return out; }
מבחינות רבות, היא זהה לפונקציה רגילה ב-C. המאפיין
RS_KERNEL
שהוחל על אב הטיפוס של הפונקציה מציין שהפונקציה היא ליבה (kernel) של מיפוי RenderScript במקום בפונקציה שניתן להפעיל. הארגומנטin
מתמלא באופן אוטומטי על סמך הקלטAllocation
שמוענק להפעלת הליבה. הארגומנטיםx
ו-y
הם שעליהם אנחנו מדברים בהמשך. הערך המוחזר מהליבה נכתב באופן אוטומטי במיקום המתאים בפלטAllocation
. כברירת מחדל, הליבה (kernel) הזו עוברת בכל הקלטAllocation
, באמצעות הפעלה אחת של פונקציית הליבה לכלElement
ב-Allocation
.ליבת מיפוי יכולה לכלול קלט אחד או יותר
Allocations
, פלט יחידAllocation
או את שניהם. בדיקות זמן ריצה של RenderScript כדי לוודא שכל הקצאות הקלט והפלט זהות ושהמאפייניםElement
של הקלט והפלט ההקצאות תואמות לאב-טיפוס של הליבה; אם אחת מהבדיקות האלה נכשלה, RenderScript ויוצרת חריגה.הערה: לפני Android 6.0 (רמת API 23), ליבה (kernel) של מיפוי עשויה לא יכול להכיל יותר מערך אחד של קלט
Allocation
.אם אתם צריכים יותר
Allocations
של קלט או פלט ממה שיש בליבה, צריך לקשר את העצמים האלה למשתנים הגלובליים של הסקריפטrs_allocation
ולגשת אליהם מהליבה או מפונקציה שניתן להפעיל דרך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
. ליבה של הפחתה תמיד מחייבת פונקצייתaccumulator
. יכולות להיות לה גם פונקציות אחרות, בהתאם למה שאתם רוצים שהליבה תעשה.פונקציית המצטבר של ליבה של ירידה חייבת להחזיר את הערך
void
, וצריכים להיות לה לפחות שני ארגומנטים. הארגומנט הראשון (accum
בדוגמה הזו) הוא הפניה לפריט נתונים של המצטבר, והשני (val
בדוגמה הזו) מתמלא באופן אוטומטי על סמך הקלטAllocation
שמוענק להפעלת הליבה. פריט הנתונים של המצטבר נוצר על ידי סביבת זמן הריצה של RenderScript, והוא מאופשר לאפס כברירת מחדל. כברירת מחדל, הליבה הזו פועלת על כל הקלט שלהAllocation
, עם הפעלה אחת של פונקציית המצטבר לכלElement
ב-Allocation
. כברירת מחדל, הערך הסופי של פריט הנתונים של המצטבר מטופל כתוצאה מהצמצום, והוא מוחזר ל-Java. זמן הריצה של RenderScript בודק אם הסוגElement
של הקצאת הקלט תואם לפונקציית הצבירה אב טיפוס; אם הוא לא תואם, RenderScript יקפיץ ערך חריג.לליבת הפחתה יש קלט
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 רץ כשהסקריפט נוצר לראשונה. כך ניתן להגדיר החישוב יתבצע באופן אוטומטי במהלך יצירת הסקריפט. - אפס או יותר פונקציות ו-globals סטטיים של סקריפט. סקריפט סטטי גלובלי מקביל
סקריפט גלובלי, מלבד זאת שלא ניתן לגשת אליו מקוד Java. פונקציה סטטית היא פונקציית C רגילה שאפשר להפעיל מכל ליבה או מכל פונקציה שניתן להפעיל בסקריפט, אבל היא לא חשופה ל-Java API. אם אין צורך לגשת לפונקציה או למשתנה גלובלי של סקריפט מקוד Java, מומלץ מאוד להצהיר עליהם כ-
static
.
הגדרת רמת הדיוק של נקודה צפה (floating-point)
אפשר לקבוע את רמת הדיוק הנדרשת של הנקודה הצפה (floating-point) בסקריפט. האפשרות הזו מועילה אם לא נדרש תקן IEEE 754-2008 המלא (שמשמש כברירת מחדל). הפרגמות הבאות יכולות להגדיר רמה שונה של דיוק של נקודה צפה (floating-point):
#pragma rs_fp_full
(ברירת המחדל אם לא צוין דבר): לאפליקציות שדורשות דיוק של נקודה צפה כפי שמתואר בתקן IEEE 754-2008.#pragma rs_fp_relaxed
: לאפליקציות שלא מחייבות תאימות קפדנית ל-IEEE 754-2008, ויכולות לסבול פחות דיוק. המצב הזה מאפשר שטיפה לאפס (flush-to-zero) לערכים לא רגילים (denorms) ועיגול לאפס.#pragma rs_fp_imprecise
: לאפליקציות ברמת דיוק מחמירה בדרישות שלנו. המצב הזה מפעיל את כל מה שנמצא ב-rs_fp_relaxed
יחד עם הבאים:- פעולות שמניבות את הערך -0.0 יכולות להחזיר במקום זאת את הערך +0.0.
- הפעולות על INF ו-NAN לא מוגדרות.
רוב האפליקציות יכולות להשתמש ב-rs_fp_relaxed
ללא תופעות לוואי. זה יכול להיות
מתאימות לארכיטקטורות מסוימות עקב אופטימיזציות נוספות שזמינות רק כשמשתמשים
דיוק (למשל הוראות לגבי מעבדי SIMD).
גישה לממשקי 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, עליך להגדיר את גרסת הפיתוח כדי לגשת אליהם. כדי להשתמש בממשקי ה-API האלה, נדרשים הכלים הבאים של Android SDK:
- Android SDK Tools גרסה 22.2 ואילך
- Android SDK Build-tools גרסה 18.1.0 ואילך
חשוב לדעת: החל מגרסה 24.0.0 של Android SDK Build-tools, אין יותר תמיכה ב-Android 2.2 (רמת API 8).
ניתן לבדוק ולעדכן את הגרסה המותקנת של הכלים האלה מנהל ה-SDK של Android.
כדי להשתמש בממשקי Support Library RenderScript API:
- חשוב לוודא שמותקנת גרסת ה-SDK הנדרשת של Android.
- מעדכנים את ההגדרות של תהליך ה-build ב-Android כך שיכללו את ההגדרות של RenderScript:
- פותחים את הקובץ
build.gradle
בתיקיית האפליקציה של מודול האפליקציה. - מוסיפים את ההגדרות הבאות של RenderScript לקובץ:
android { compileSdkVersion 33 defaultConfig { minSdkVersion 9 targetSdkVersion 19 renderscriptTargetApi 18 renderscriptSupportModeEnabled true } }
android { compileSdkVersion(33) defaultConfig { minSdkVersion(9) targetSdkVersion(19) renderscriptTargetApi = 18 renderscriptSupportModeEnabled = true } }
ההגדרות שלמעלה משפיעות על התנהגות ספציפית בתהליך ה-build של Android:
renderscriptTargetApi
– מציין את גרסת הקוד הבינארי שרוצים ליצור. מומלץ להגדיר את הערך הזה לרמת ה-API הנמוכה ביותר שאפשר לספק. כל הפונקציונליות שמשמשת אותך ומגדיריםrenderscriptSupportModeEnabled
אלtrue
. הערכים החוקיים להגדרה הזו הם כל ערך שלם מ-11 ועד לרמת ה-API שפורסמה לאחרונה. אם גרסת ה-SDK המינימלית שצוינה במניפסט של האפליקציה מוגדרת לערך אחר, הערך הזה מבוטל והערך היעד בקובץ ה-build משמש להגדרת גרסת ה-SDK המינימלית.renderscriptSupportModeEnabled
– קובע שהקוד הבינארי שנוצר יעבור לגרסה תואמת אם המכשיר שבו הוא פועל לא תומך בגרסת היעד.
- פותחים את הקובץ
- בכיתות האפליקציה שמשתמשות ב-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
פעולה שבא לידי ביטוי מהדר (compiler) של RenderScript כדי להקל על הגישה לסקריפט מקוד Java.
למחלקה הזו יש את השם
ScriptC_filename
. לדוגמה, אם הליבה של המיפוי שמופיעה למעלה הייתה ממוקמת ב-invert.rs
וההקשר של RenderScript כבר היה ממוקם ב-mRenderScript
, הקוד ב-Java או ב-Kotlin ליצירת מופע של הסקריפט יהיה: - ScriptIntrinsic: אלה ליבות (kernel) מובנות של RenderScript לפעולות נפוצות,
כמו טשטוש גאוסיאני, קונבולציה ומיזוג תמונות. מידע נוסף זמין בקטעים הבאים על תת-הסוגים של
ScriptIntrinsic
.
- ScriptC: אלה הסקריפטים בהגדרת המשתמש שמתוארים בקטע כתיבת ליבה של RenderScript למעלה. לכל סקריפט יש מחלקה של Java
פעולה שבא לידי ביטוי מהדר (compiler) של RenderScript כדי להקל על הגישה לסקריפט מקוד Java.
למחלקה הזו יש את השם
- אכלוס את ההקצאות בנתונים. מלבד הקצאות שנוצרו באמצעות
createFromBitmap()
, הקצאה מאוכלסת בנתונים ריקים כשהיא נוצרת בפעם הראשונה. כדי לאכלס הקצאה, משתמשים באחת מהשיטות 'copy' ב-Allocation
. שיטות ה'העתקה' הן סינכרוניות. - מגדירים את הסקריפטים מנחים שנדרשים. אפשר להגדיר משתנים גלובליים באמצעות שיטות באותה כיתה
ScriptC_filename
בשםset_globalname
. עבור לדוגמה, כדי להגדיר משתנהint
בשםthreshold
, משתמשים בפונקציה שיטת Javaset_threshold(int)
; וכדי להגדיר משתנהrs_allocation
בשםlookup
, צריך להשתמש בפונקציה Javaset_lookup(Allocation)
. השיטות שלset
הם אסינכרוניים. - מפעילים את הליבות והפונקציות הרלוונטיות שניתן להפעיל.
שיטות להפעלת ליבה נתונה משתקפות באותה כיתה
ScriptC_filename
עם שיטות בשםforEach_mappingKernelName()
אוreduce_reductionKernelName()
. ההשקות האלה הן אסינכרוניות. בהתאם לארגומנטים של הליבה, השיטה מקבלת Allocations אחד או יותר, וכל אחד מהם חייב להיות באותו המאפיינים. כברירת מחדל, הליבה פועלת בכל הקואורדינטות במאפיינים האלה. כדי להריץ ליבה על קבוצת משנה של הקואורדינטות האלה, מעבירים אתScript.LaunchOptions
המתאים כארגומנטים האחרונים לשיטהforEach
אוreduce
.מפעילים פונקציות שניתן להפעיל באמצעות השיטות של
invoke_functionName
שמופיעות באותה מחלקהScriptC_filename
. ההשקות האלה הן לא סינכרוניות. - אחזור נתונים מ-
Allocation
אובייקטים ו-JavaScriptFutureType אובייקטים. כדי לגשת לנתונים מ-Allocation
מקוד Java, עליך להעתיק את הנתונים האלה חזרה ל-Java באמצעות אחד ההעתקים ב-Allocation
. כדי לקבל את התוצאה של ליבה של הפחתה, צריך להשתמש בשיטהjavaFutureType.get()
. השיטות copy ו-get()
הן סינכרוניות. - בודקים את ההקשר של RenderScript. אפשר להשמיד את ההקשר של RenderScript באמצעות
destroy()
או לאפשר לאובייקט ההקשר של RenderScript לעבור איסוף גרוטאות. הדבר יגרום לכל שימוש נוסף באובייקט ששייך לזה הקשר מסוים כדי חריג חריג.
מודל ביצוע אסינכרוני
הערכים המשתקפים של forEach
, invoke
, reduce
,
ו-set
הן אסינכרוניות – כל אחת מהן עשויה לחזור ל-Java לפני השלמת
הפעולה המבוקשת. עם זאת, הפעולות הנפרדות מסודרות ברצף לפי הסדר שבו הן הופעלו.
הכיתה Allocation
מספקת "עותק" שיטות להעתקת נתונים אליהם
ומתוך 'הקצאות'. שיטת 'copy' היא סינכרונית, והיא עוברת סריאליזציה ביחס לכל אחת מהפעולות האסינכרוניות שלמעלה שמשפיעות על אותה הקצאה.
מחלקות JavaFutureType שמשתקפות מספקות
שיטת get()
לקבלת התוצאה של הפחתה. הפונקציה get()
היא סינכרונית, והיא עוברת סריאליזציה ביחס לירידה (שהיא אסינכרונית).
RenderScript ממקור יחיד
ב-Android 7.0 (רמת API 24) נכללת תכונת תכנות חדשה שנקראת מקור יחיד
RenderScript, שבו ליבה (kernel) מושקות מהסקריפט שבו הן מוגדרות, ולא
מתוך Java. הגישה הזו מוגבלת כרגע למיפוי של ליבות (kernels), שנקראות פשוט ליבה (kernels)
כדי לשמור על תמציתיות. תכונה חדשה זו תומכת גם ביצירת הקצאות מסוג
rs_allocation
מתוך הסקריפט. עכשיו אפשר להטמיע אלגוריתם שלם רק בתוך סקריפט, גם אם נדרשים מספר הפעלות של הליבה.
היתרונות הם כפולים: קוד קל יותר לקריאה, כי ההטמעה של האלגוריתם נשארת בשפה אחת, וקוד מהיר יותר, כי יש פחות מעברים בין 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)
יוצרת הקצאה של 1,6384 רכיבים ב-1D, כל אחד מהם מסוג uchar4
.
המערכת מנהלת את ההקצאות באופן אוטומטי. אין צורך לשחרר אותם או לפנות אותם באופן מפורש. אבל אפשר להתקשר
rsClearObject(rs_allocation* alloc)
כדי לציין שכבר אין לך צורך בכינוי
alloc
להקצאה הבסיסית,
כדי שהמערכת תוכל לפנות משאבים בהקדם האפשרי.
בקטע כתיבה של ליבה של RenderScript מופיעה ליבה לדוגמה שמהפכת תמונה. הדוגמה הבאה מרחיבה את העובדה שכדי להחיל יותר מאפקט אחד על תמונה,
באמצעות 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 באופן הבא:
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)
// File SingleSource.java RenderScript RS = RenderScript.create(context); ScriptC_singlesource script = new ScriptC_singlesource(RS); Allocation inputAllocation = Allocation.createFromBitmapResource( RS, getResources(), R.drawable.image); Allocation outputAllocation = Allocation.createTyped( RS, inputAllocation.getType(), Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT); script.invoke_process(inputAllocation, outputAllocation);
הדוגמה הזו ממחישה איך ניתן ליישם באופן מלא אלגוריתם שכולל שתי השקות ליבה (kernel) בשפת RenderScript עצמה. בלי Single-Source RenderScript, תצטרכו להפעיל את שני הליבות מקוד Java, ולהפריד בין הפעלות הליבה לבין הגדרות הליבה, מה שיקשה יותר להבין את האלגוריתם כולו. לא רק קל יותר לקרוא קוד RenderScript עם מקור יחיד, וגם מבטל את המעבר בין Java לסקריפט בהשקות ליבה (kernel). חלק מהאלגוריתמים האיטרטיביים עשויים להפעיל ליבה (kernel) מאות פעמים, וכך התקורה של מעבר כזה משמעותית מאוד.
משתנים גלובליים של סקריפט
משתנה גלובלי של סקריפט הוא משתנה גלובלי רגיל שאינו static
בקובץ סקריפט (.rs
). לסקריפט ברמת ה-global שנקרא var ומוגדר בקובץ filename.rs
, יהיה method get_var
שמוצג בכיתה ScriptC_filename
. אלא אם גלובלי
const
, יהיה גם
אמצעי התשלום set_var
.
לסקריפט גלובלי נתון יש שני ערכים נפרדים – Java וערך script. הערכים האלה פועלים באופן הבא:
- אם ל-var יש מאתחל סטטי בסקריפט, הוא מציין את הערך הראשוני של var ב-Java וגם ב- סקריפט. אחרת, הערך הראשוני הוא אפס.
- ניגש אל var בתוך הסקריפט, קריאה וכתיבה ערך הסקריפט.
- השיטה
get_var
קוראת את הערך ב-Java. - השיטה
set_var
(אם היא קיימת) כותבת את הערך של Java באופן מיידי, ואת הערך של הסקריפט באופן אסינכרוני.
הערה: המשמעות היא שלמעט מאתחל סטטי בסקריפט, ערכים שנכתבו לגלובלי בתוך סקריפט, לא גלויים ל-Java.
גרעיני הפחתה בעומק
הפחתה היא תהליך שילוב של אוסף נתונים לערך יחיד. זהו פרימיטיבי שימושי בתכנות מקביל, שיש בו יישומים כמו הבאים:
- חישוב הסכום או המכפלה של כל הנתונים
- פעולות לוגיות של מחשוב (
and
,or
,xor
) בכל הנתונים - מציאת הערך המינימלי או המקסימלי מתוך הנתונים
- חיפוש ערך ספציפי או קואורדינטה של ערך ספציפי בתוך הנתונים
ב-Android 7.0 (רמת API 24) ואילך, RenderScript תומך בליבות הפחתה כדי לאפשר שימוש באלגוריתמים יעילים להפחתת נתונים שנכתבו על ידי משתמשים. תוכלו להפעיל ליבות של הפחתה על קלט באמצעות 1, 2 או 3 מאפיינים.
הדוגמה שלמעלה מציגה ליבה פשוטה של הפחתת addint.
לפניכם ליבה מורכבת יותר של הפחתה מסוג findMinAndMax שמוצאת את המיקומים של הערכים המינימלי והמקסימלי של long
במערך Allocation
בעל מימד אחד:
#define LONG_MAX (long)((1UL << 63) - 1) #define LONG_MIN (long)(1UL << 63) #pragma rs reduce(findMinAndMax) \ initializer(fMMInit) accumulator(fMMAccumulator) \ combiner(fMMCombiner) outconverter(fMMOutConverter) // Either a value and the location where it was found, or INITVAL. typedef struct { long val; int idx; // -1 indicates INITVAL } IndexedVal; typedef struct { IndexedVal min, max; } MinAndMax; // In discussion below, this initial value { { LONG_MAX, -1 }, { LONG_MIN, -1 } } // is called INITVAL. static void fMMInit(MinAndMax *accum) { accum->min.val = LONG_MAX; accum->min.idx = -1; accum->max.val = LONG_MIN; accum->max.idx = -1; } //---------------------------------------------------------------------- // In describing the behavior of the accumulator and combiner functions, // it is helpful to describe hypothetical functions // IndexedVal min(IndexedVal a, IndexedVal b) // IndexedVal max(IndexedVal a, IndexedVal b) // MinAndMax minmax(MinAndMax a, MinAndMax b) // MinAndMax minmax(MinAndMax accum, IndexedVal val) // // The effect of // IndexedVal min(IndexedVal a, IndexedVal b) // is to return the IndexedVal from among the two arguments // whose val is lesser, except that when an IndexedVal // has a negative index, that IndexedVal is never less than // any other IndexedVal; therefore, if exactly one of the // two arguments has a negative index, the min is the other // argument. Like ordinary arithmetic min and max, this function // is commutative and associative; that is, // // min(A, B) == min(B, A) // commutative // min(A, min(B, C)) == min((A, B), C) // associative // // The effect of // IndexedVal max(IndexedVal a, IndexedVal b) // is analogous (greater . . . never greater than). // // Then there is // // MinAndMax minmax(MinAndMax a, MinAndMax b) { // return MinAndMax(min(a.min, b.min), max(a.max, b.max)); // } // // Like ordinary arithmetic min and max, the above function // is commutative and associative; that is: // // minmax(A, B) == minmax(B, A) // commutative // minmax(A, minmax(B, C)) == minmax((A, B), C) // associative // // Finally define // // MinAndMax minmax(MinAndMax accum, IndexedVal val) { // return minmax(accum, MinAndMax(val, val)); // } //---------------------------------------------------------------------- // This function can be explained as doing: // *accum = minmax(*accum, IndexedVal(in, x)) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // *accum is INITVAL, then this function sets // *accum = IndexedVal(in, x) // // After this function is called, both accum->min.idx and accum->max.idx // will have nonnegative values: // - x is always nonnegative, so if this function ever sets one of the // idx fields, it will set it to a nonnegative value // - if one of the idx fields is negative, then the corresponding // val field must be LONG_MAX or LONG_MIN, so the function will always // set both the val and idx fields static void fMMAccumulator(MinAndMax *accum, long in, int x) { IndexedVal me; me.val = in; me.idx = x; if (me.val <= accum->min.val) accum->min = me; if (me.val >= accum->max.val) accum->max = me; } // This function can be explained as doing: // *accum = minmax(*accum, *val) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // one of the two accumulator data items is INITVAL, then this // function sets *accum to the other one. static void fMMCombiner(MinAndMax *accum, const MinAndMax *val) { if ((accum->min.idx < 0) || (val->min.val < accum->min.val)) accum->min = val->min; if ((accum->max.idx < 0) || (val->max.val > accum->max.val)) accum->max = val->max; } static void fMMOutConverter(int2 *result, const MinAndMax *val) { result->x = val->min.idx; result->y = val->max.idx; }
הערה: דוגמאות נוספות לגרעיני הפחתה מפורטות כאן.
כדי להריץ ליבה של הפחתה, סביבת זמן הריצה של RenderScript יוצרת משתנה אחד או יותר שנקרא פריטי נתונים של המצטבר כדי לאחסן את המצב של תהליך ההפחתה. זמן הריצה של RenderScript
בוחר את מספר פריטי הנתונים המצטברים באופן שיאפשר מיקסום הביצועים. הסוג של פריטי הנתונים במצטבר (accumType) נקבע על ידי פונקציית המצטבר של הליבה – הארגומנט הראשון בפונקציה הזו הוא הפניה לפריט נתונים במצטבר. כברירת מחדל, כל פריט נתונים של צבירה מאותחל לאפס (כאילו
עד memset
); עם זאת, אפשר לכתוב פונקציית התחלה כדי לבצע פעולה כלשהי
אחרת.
דוגמה: בליבה addint, פריטי הנתונים של המצטבר (מסוג int
) משמשים להוספת ערכי הקלט. אין פונקציית הפעלה, לכן כל פריט נתונים של צבירה מאותחל
אפס.
דוגמה: בליבה findMinAndMax, פריטי הנתונים של המצטבר (מסוג MinAndMax
) משמשים למעקב אחרי הערכים המינימלי והמקסימלי שנמצאו עד כה. יש פונקציית איפוס שמגדירה את הערכים האלה ל-LONG_MAX
ול-LONG_MIN
, בהתאמה, ומגדירה את המיקומים של הערכים האלה ל-1-, כדי לציין שהערכים לא נמצאים בפועל בחלק (הריק) של הקלט שעבר עיבוד.
RenderScript קורא לפונקציית המצטבר שלכם פעם לכל קואורדינטה בנתוני הקלט. בדרך כלל, הפונקציה צריכה לעדכן את פריט הנתונים של המצטבר באופן כלשהו בהתאם לקלט.
דוגמה: בליבה addint, פונקציית המצטבר מוסיפה את הערך של רכיב קלט לפריט הנתונים של המצטבר.
לדוגמה: בשדה הליבה (kernel) של findMinAndMax, פונקציית הצבירה בודק אם הערך של רכיב קלט קטן מהמינימום או שווה לו שתועד בפריט נתוני הצבירה ו/או גדול מהערך המקסימלי או שווה לו שתועד בפריט נתוני הצבירה, ומעדכנת את פריט נתוני הצבירה בהתאם.
אחרי הקריאה לפונקציית הצבירה פעם אחת לכל קואורדינטה בקלט או בקלט, RenderScript חייב לשלב את המצבר פריטי נתונים יחד לפריט נתונים של צבירה אחת. כדי לעשות זאת, אפשר לכתוב פונקציית שרשור. אם לפונקציית המצטבר יש קלט יחיד ואין לה ארגומנטים מיוחדים, אין צורך לכתוב פונקציית מיזוג. מערכת RenderScript תשתמש בפונקציית המצטבר כדי לשלב את פריטי הנתונים של המצטבר. (עדיין אפשר לכתוב פונקציית שילוב אם התנהגות ברירת המחדל הזו לא מתאימה לכם).
לדוגמה: בתוסף. אין פונקציית שילוב, לכן ייעשה שימוש בפונקציית הצבירה. זהו ההתנהגות הנכונה, כי אם מחלקים אוסף ערכים לשני חלקים ומחשבים את הסכומים של שני החלקים בנפרד, הסכום של שני הסכומים האלה זהה להסכום של כל האוסף.
לדוגמה: בשדה
הליבה של findMinAndMax, פונקציית האפשרויות המשולבת
בודק אם הערך המינימלי שתועד בשדה 'מקור' נתוני צבירה
הפריט *val
קטן מהערך המינימלי שתועד ב'יעד'
פריט נתוני צבירה *accum
, ועדכון *accum
בהתאם. היא פועלת באופן דומה לגבי הערך המקסימלי. הפעולה הזו מעדכנת את *accum
למצב שהיה לו אם כל ערכי הקלט היו מצטברים ב-*accum
, במקום חלק ב-*accum
וחלק ב-*val
.
לאחר שכל פריטי הנתונים של הצבירה שולבו, RenderScript קובע התוצאה של ההפחתה בחזרה ל-Java. אפשר לכתוב ערך משלים function כדי לעשות זאת. אם רוצים שהערך הסופי של פריטי הנתונים המשולבים של המצטבר יהיה תוצאת הפחתה, אין צורך לכתוב פונקציית המרה של פלט.
לדוגמה: בליבה (kernel) addint, אין פונקציית ממיר. הערך הסופי של פריטי הנתונים המשולבים הוא הסכום של כל הרכיבים של הקלט, והוא הערך שאנחנו רוצים להחזיר.
לדוגמה: בשדה
הליבה של findMinAndMax, הפונקציה להמרה
מאתחל ערך תוצאה int2
כדי לשמור את המיקומים של המינימום
והערכים המקסימליים שנובעים מהשילוב של כל פריטי הנתונים של הצבירה.
כתיבת ליבה (kernel) של הפחתה
#pragma rs reduce
מגדיר ליבה של הפחתה על ידי ציון השם שלה והשמות והתפקידים של הפונקציות שמרכיבות את הליבה. כל הפונקציות האלה חייבות להיות מסוג static
. לליבת הפחתה תמיד נדרשת פונקציית accumulator
. אפשר להשמיט חלק מהפונקציות האחרות או את כולן, בהתאם למה שאתם רוצים שהליבה תעשה.
#pragma rs reduce(kernelName) \ initializer(initializerName) \ accumulator(accumulatorName) \ combiner(combinerName) \ outconverter(outconverterName)
המשמעות של הפריטים ב-#pragma
היא:
reduce(kernelName)
(חובה): מציין שהליבה של ההפחתה בתהליך ההגדרה. שיטה משתקפת של Javareduce_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)
(אופציונלי): שם הפונקציה להמרת הפלט של ליבה זו של הפחתה. אחרי שהשילוב של RenderScript משלב את כל הצבירה הוא קורא לפונקציה הזו כדי לקבוע את התוצאה לחזרה ל-Java. הפונקציה צריכה להיות מוגדרת כך הזה:static void outconverterName(resultType *result, const accumType *accum) { … }
result
הוא הפניה לפריט נתונים של תוצאה (שסופקו אבל לא הופעלו על ידי סביבת זמן הריצה של RenderScript) כדי שהפונקציה הזו תוכל להפעיל אותו עם תוצאת הפחתה. resultType הוא הסוג של פריט הנתונים הזה, והוא לא חייב להיות זהה ל-accumType.accum
הוא מצביע לפריט הנתונים הנצבר הסופי שמחושב על ידי הפונקציה המשולבת.אם לא תספקו פונקציית המרה של פלט, RenderScript יקפיא את פריט הנתונים הסופי של המצטבר אל פריט הנתונים של התוצאה, ויפעל כאילו הייתה פונקציית המרה של פלט שנראית כך:
static void outconverterName(accumType *result, const accumType *accum) { *result = *accum; }
אם רוצים סוג תוצאה שונה מסוג הנתונים של המצטבר, צריך להשתמש בפונקציית הממיר הפלט.
שימו לב שלליבה יש סוגי קלט, סוג פריט של נתוני צבירה וסוג תוצאה,
כולן לא צריכות להיות זהות. לדוגמה, ב-kernel findMinAndMax, סוג הקלט long
, סוג פריט הנתונים של המצטבר MinAndMax
וסוג התוצאה int2
הם כולם שונים.
מה אי אפשר להניח?
אין להסתמך על מספר פריטי הנתונים הנצברים שנוצרו על ידי RenderScript עבור בהשקת ליבה (kernel). אין ערובה לכך ששתי השקות של אותה ליבה (kernel) באמצעות אותם ערכי קלט ייצרו את אותו מספר של פריטי נתונים של צבירה.
אסור להסתמך על הסדר שבו RenderScript קורא לפונקציות המאפסת, המצטברת והממזגת. יכול להיות שהוא יקרא לחלק מהן במקביל. לא ניתן להבטיח שתי השקות של אותו ליבה עם אותו קלט יופיעו באותו סדר. היחיד היא להבטיח שרק פונקציית המאתחל תראה אי פעם צבירת נתונים לא מאותחלת של הנתונים. לדוגמה:
- אנחנו לא מבטיחים שכל פריטי נתוני הצבירה יאתחלו לפני נקראת פונקציית הצבירה, למרות שהיא תתבצע רק במצבור מאתחל של הנתונים.
- אין ערובה לסדר שבו רכיבי הקלט מועברים לפונקציית המצטבר.
- אין ערובה לכך שפונקציית הצבירה הופעלה בכל רכיבי הקלט לפני קריאה לפונקציית ה-Combiner.
אחת מהתוצאות האלה היא findMinAndMax הליבה (kernel) לא דטרמיניסטית: אם הקלט מכיל יותר ממופע אחד של ערך מינימלי או מקסימלי, אין דרך לדעת איזה אירוע הליבה (kernel) למצוא.
מה נדרש להבטיח?
מכיוון שמערכת RenderScript יכולה להריץ ליבה בדרכים רבות, עליכם לפעול לפי כללים מסוימים כדי לוודא שהליבה תתנהג כפי שאתם רוצים. אם לא תפעלו לפי הכללים האלה, אתם עלולים לקבל תוצאות שגויות, או שגיאות זמן ריצה.
לרוב, הכללים הבאים קובעים ששני פריטים של נתוני המצטבר חייבים להיות בעלי אותו ערך. מה זה אומר? זה תלוי במה שאתם רוצים שהליבה תעשה. בדרך כלל, כשמדובר בהפחתה מתמטית כמו addint, הגיוני שהמשמעות של 'זהה' תהיה שוויון מתמטי. אפשר לבחור כל סוג של לחפש כך כמו findMinAndMax ("חיפוש המיקום של ערכי המינימום ערכי קלט מקסימליים") כאשר יכול להיות יותר ממופע אחד של קלט זהה כל המיקומים של ערך קלט נתון צריכים להיחשב כ'אותו'. אפשר לכתוב ליבה דומה ל"מציאת המיקום של ערכי הקלט המינימליים והמקסימליים השמאליים ביותר" כאשר (למשל) ערך מינימלי במיקום 100 מועדף על ערך מינימלי זהה במיקום 200; לליבה (kernel) הזו, 'אותו' פירושו מיקום זהה, לא רק value זהה, והפונקציות של הצבירה והשילוב חייבות להיות שונים מאלה של findMinAndMax.
פונקציית המאתחל חייבת ליצור ערך זהות. כלומר, אםI
ו-A
הם פריטי נתוני צבירה אותחלו
באמצעות פונקציית האתחול, ו-I
אף פעם לא הועבר אל
פונקציית צבירה (אבל ייתכן ש-A
הייתה), אז
דוגמה: בליבה addint, פריט נתונים של המצטבר מאופשר לאפס. פונקציית המיזוג של הליבה הזו מבצעת חיבור. אפס הוא ערך הזהות לחיבור.
דוגמה: בליבה findMinAndMax, פריט נתונים של המצטבר מאופשר ל-INITVAL
.
fMMCombiner(&A, &I)
משאיר אתA
ללא שינוי, כיI
הואINITVAL
.fMMCombiner(&I, &A)
מגדיר אתI
לערךA
, כיI
הואINITVAL
.
לכן, INITVAL
הוא אכן ערך זהות.
פונקציית השילוב חייבת להיות קומוטטיבית. כלומר, אם A
ו-B
הם פריטים של נתוני המצטבר שהוגדרו על ידי פונקציית המאפיין, ויכול להיות שהועברו לפונקציית המצטבר אפס פעמים או יותר, אז combinerName(&A, &B)
חייב להגדיר את A
לאותו ערך שפונקציית combinerName(&B, &A)
מגדירה את B
.
לדוגמה: בתוסף. הליבה, פונקציית השילוב מוסיפה את שני הערכים של פריטי נתוני הצבירה; ההוספה היא קומוטטיבית.
לדוגמה: בליבה (kernel) של 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
לא אותחלה ו-args היא רשימת ארגומנטים של קלט וארגומנטים מיוחדים לקריאה מסוימת לפונקציית המצטבר, אז שתי רצפי הקוד הבאים חייבים להגדיר את 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 הוא הערך הראשוני, זההB = minmax(B, IndexedVal(V, X))
B = IndexedVal(V, X)
- טענת 4 זהה לטענה
שזהה ל-A = minmax(A, B)
A = minmax(A, IndexedVal(V, X))
הצהרות 1 ו-4 מגדירות את A
לאותו ערך, ולכן הליבה (kernel) הזו תואמת
כלל קיפול בסיסי.
קריאה לליבת הפחתה מקוד Java
לליבה (kernel) של הפחתה בשם kernelName שמוגדרת ב
filename.rs
, יש שלוש שיטות שמשתקפות
מחלקה ScriptC_filename
:
// 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
// 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) של התוסף:
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()
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
. לאחר מכן הוא קורא ל-Method 1 ומעביר את ההקצאות הזמניות.
הערה: אם האפליקציה תבצע מספר קריאות ליבה (kernel) באמצעות את אותו מערך, או עם מערכים שונים בעלי אותם מאפיינים וסוג רכיב, אפשר לשפר על ידי יצירה, אכלוס ושימוש חוזר של הקצאות משלכם, במקום באמצעות שיטה 3.
javaFutureType,
סוג ההחזרה של שיטות הפחתת ההשתקפות, הוא סוג של שיטת Reflection של קלאס סטטי מוטמע בתוך הכיתה ScriptC_filename
. הוא מייצג את התוצאה העתידית של הפעלת הליבה עם הפחתה. כדי לקבל את התוצאה בפועל של ההרצה, צריך להפעיל את השיטה get()
של הכיתה הזו, שמחזירה ערך מסוג javaResultType. get()
הוא סינכרוני.
class ScriptC_filename(rs: RenderScript) : ScriptC(…) { object javaFutureType { fun get(): javaResultType { … } } }
public class ScriptC_filename extends ScriptC { public static class javaFutureType { public javaResultType get() { … } } }
resultType נקבע מה-resultType של פונקציית פלט ממיר. אלא אם resultType הוא טיפוס ללא סימן (סקלר, וקטור או מערך), הערך של javaResultType הוא הטיפוס התואם ישירות ב-Java. אם resultType הוא סוג לא חתום וקיים סוג חתום גדול יותר, ב-Java, אז resultType הוא הסוג החתום הגדול יותר של Java; אחרת, הוא ישירות סוג ה-Java המתאים. לדוגמה:
- אם resultType הוא
int
,int2
אוint[15]
, אז resultType הואint
,Int2
, אוint[]
. כל הערכים של resultType יכולים להיות מיוצגים על ידי javaResultType. - אם resultType הוא
uint
,uint2
אוuint[15]
, אז תוצאה של JavaScript היאlong
,Long2
אוlong[]
. כל הערכים של resultType יכולים להיות מיוצגים על ידי javaResultType. - אם הערך של resultType הוא
ulong
,ulong2
אוulong[15]
, הערך של javaResultType הואlong
,Long2
אוlong[]
. יש ערכים מסוימים של resultType שלא ניתן לייצג באמצעות javaResultType.
JavaFutureType הוא סוג התוצאה העתידית התואמת ל-resultType של ההמרה היוצאת .
- אם resultType אינו סוג של מערך, אז JavaScriptFutureType
result_resultType
. - אם resultType הוא מערך באורך Count עם איברים מסוג memberType,
אז JavaFutureType הוא
resultArrayCount_memberType
.
לדוגמה:
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> = … } }
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 של הארגומנט התואם הפונקציה Acumulator. אלא אם inXType הוא סוג ללא סימן או סוג וקטור, devecSiInXType הוא הסוג התואם ישירות ב-Java. אם inXType הוא סוג סקלר לא חתום, devecSiInXType הוא סוג Java התואם ישירות לסוג הסקלר החתום של אותו גודל. אם inXType הוא סוג וקטור חתום, devecSiInXType הוא Java התואם ישירות לסוג רכיב הווקטור. אם inXType הוא סוג וקטור ללא סימן, אז devecSiInXType הוא סוג Java שתואם ישירות לסוג הסקלר החתום באותו גודל כמו סוג הרכיב של הווקטור. לדוגמה:
- אם הערך של inXType הוא
int
, הערך של devecSiInXType הואint
. - אם הערך של inXType הוא
int2
, הערך של devecSiInXType הואint
. המערך הוא ייצוג שטוח: יש בו פי שניים רכיבים סקלריים מאשר יש להקצאה רכיבי וקטור עם שני רכיבים. זהו אותו האופן שבו פועלות השיטותcopyFrom()
שלAllocation
. - אם inXType הוא
uint
, אז deviceSiInXTypeint
. ערך חתום במערך 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 שמפורטים בדף הזה.