RenderScript هو إطار عمل لتنفيذ المهام التي تتطلّب موارد حوسبة مكثّفة بأداء عالٍ على أجهزة Android. يتم توجيه RenderScript بشكل أساسي للاستخدام مع العمليات الحسابية المتوازية للبيانات، على الرغم من أن أعباء العمل التسلسلية يمكن أن تستفيد أيضًا. توزّع بيئة التشغيل RenderScript العمل على المعالجات المتاحة على الجهاز، مثل وحدات المعالجة المركزية (CPU) ووحدات معالجة الرسومات (GPU) المتعددة النواة. يتيح لك ذلك التركيز على التعبير عن الخوارزميات بدلاً من جدولة العمل. يُعدّ RenderScript مفيدًا بشكل خاص للتطبيقات التي تعالج الصور أو التصوير الحاسوبي أو الرؤية الحاسوبية.
للبدء في استخدام RenderScript، هناك مفهومان رئيسيان يجب فهمهما:
- اللغة نفسها هي لغة مشتقة من C99 لكتابة رموز برمجية حوسبية عالية الأداء. توضّح صفحة كتابة نواة RenderScript كيفية استخدامها لكتابة نواة الحوسبة.
- تُستخدم واجهة برمجة التطبيقات للتحكّم في إدارة عمر موارد RenderScript والتحكّم في تنفيذ النواة. وهي متاحة بثلاث لغات مختلفة: Java وC++ في Android NDK ولغة النواة المشتقة من C99 نفسها. استخدام RenderScript من رمز Java وRenderScript أحادي المصدر يصفان الخيارين الأول والثالث على التوالي.
كتابة نواة RenderScript
توجد نواة RenderScript عادةً في ملف .rs في دليل <project_root>/src/rs، ويُطلق على كل ملف .rs اسم نص برمجي. يحتوي كل نص برمجي على مجموعة خاصة به من النواة والدوال والمتغيرات. يمكن أن يحتوي النص البرمجي على:
- تعريف pragma (
#pragma version(1)) يوضّح إصدار لغة نواة RenderScript المستخدَمة في هذا النص البرمجي في الوقت الحالي، القيمة الوحيدة الصالحة هي 1. - تعريف pragma (
#pragma rs java_package_name(com.example.app)) الذي يعلن عن اسم حزمة فئات Java المنعكسة من هذا النص البرمجي. يُرجى العِلم أنّ ملف.rsيجب أن يكون جزءًا من حزمة التطبيق، وليس في مشروع مكتبة. - صفر أو أكثر من الدوال القابلة للاستدعاء. الدالة القابلة للاستدعاء هي دالة RenderScript أحادية السلسلة يمكنك استدعاؤها من رمز Java باستخدام وسيطات عشوائية. غالبًا ما تكون هذه مفيدة للإعداد الأولي أو العمليات الحسابية التسلسلية ضمن مسار معالجة أكبر.
صفر أو أكثر من المتغيّرات العامة للنصوص البرمجية يشبه المتغير العام للنص البرمجي المتغير العام في لغة C. يمكنك الوصول إلى المتغيرات العامة للنص البرمجي من رمز Java، وغالبًا ما يتم استخدامها لتمرير المعلمات إلى نواة RenderScript. يمكنك الاطّلاع على مزيد من التفاصيل حول المتغيّرات العامة للنصوص البرمجية هنا.
صفر أو أكثر من نواة الحوسبة. نواة الحوسبة هي دالة أو مجموعة من الدوال التي يمكنك توجيه وقت تشغيل RenderScript لتنفيذها بالتوازي عبر مجموعة من البيانات. هناك نوعان من نواة الحوسبة: نواة الربط (تُعرف أيضًا باسم نواة foreach) ونواة الاختزال.
نواة الربط هي دالة موازية تعمل على مجموعة من
Allocationsمن الأبعاد نفسها. يتم تنفيذها تلقائيًا مرة واحدة لكل إحداثي في هذه الأبعاد. يُستخدَم عادةً (وليس حصريًا) لتحويل مجموعة منAllocationsالإدخال إلىAllocationالإخراج،Elementواحد في كل مرة.في ما يلي مثال على نواة ربط بسيطة:
uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) { uchar4 out = in; out.r = 255 - in.r; out.g = 255 - in.g; out.b = 255 - in.b; return out; }
In most respects, this is identical to a standard C function. تحدّد السمة
RS_KERNELالمطبَّقة على النموذج الأوّلي للدالة أنّ الدالة هي دالة تحويل انتقائي لتعيين RenderScript بدلاً من دالة قابلة للاستدعاء. يتم ملء الوسيطةinتلقائيًا استنادًا إلى المدخلاتAllocationالتي تم تمريرها إلى تشغيل النواة. يتم تناول الوسيطتَينxوyأدناه. تتم كتابة القيمة التي تم عرضها من النواة تلقائيًا في الموقع المناسب في الناتجAllocation. يتم تشغيل هذه النواة تلقائيًا على مستوى الإدخال بالكاملAllocation، مع تنفيذ واحد لوظيفة النواة لكلElementفيAllocation.قد تحتوي نواة الربط على مَعلمة إدخال واحدة أو أكثر من النوع
Allocationsأو مَعلمة إخراج واحدة من النوعAllocationأو كليهما. يتحقّق وقت تشغيل RenderScript للتأكّد من أنّ جميع عمليات تخصيص الإدخال والإخراج لها الأبعاد نفسها، وأنّ أنواعElementلعمليات تخصيص الإدخال والإخراج تتطابق مع النموذج الأوّلي للنواة. إذا تعذّر إجراء أي من عمليات التحقّق هذه، يعرض RenderScript استثناءً.ملاحظة: قبل الإصدار Android 6.0 (المستوى 23 من واجهة برمجة التطبيقات)، قد لا يحتوي نواة الربط على أكثر من إدخال واحد من النوع
Allocation.إذا كنت بحاجة إلى إدخال أو إخراج
Allocationsأكثر مما تحتويه النواة، يجب ربط هذه العناصر بمتغيراتrs_allocationالنصية العامة والوصول إليها من نواة أو دالة قابلة للاستدعاء عبرrsGetElementAt_type()أوrsSetElementAt_type().ملاحظة:
RS_KERNELهو ماكرو يتم تحديده تلقائيًا بواسطة RenderScript لتسهيل الأمر عليك:#define RS_KERNEL __attribute__((kernel))
نواة الاختزال هي مجموعة من الدوال التي تعمل على مجموعة من المدخلات
Allocationsمن الأبعاد نفسها. بشكل تلقائي، يتم تنفيذ دالة التجميع مرة واحدة لكل إحداثي في هذه الأبعاد. يُستخدَم عادةً (وليس حصريًا) "لتقليل" مجموعة منAllocationsالإدخال إلى قيمة واحدة.في ما يلي مثال على نواة اختزال بسيطة تجمع
Elementsمن مدخلاتها:#pragma rs reduce(addint) accumulator(addintAccum) static void addintAccum(int *accum, int val) { *accum += val; }
تتكوّن نواة الاختزال من دالة واحدة أو أكثر يكتبها المستخدم. يُستخدَم
#pragma rs reduceلتحديد النواة من خلال تحديد اسمها (addint، في هذا المثال) وأسماء وأدوار الدوال التي تشكّل النواة (دالةaccumulatoraddintAccum، في هذا المثال). يجب أن تكون جميع هذه الدوالstatic. يتطلّب نواة الاختزال دائمًا دالةaccumulator، وقد يتضمّن أيضًا دوال أخرى، وذلك حسب ما تريد أن تفعله النواة.يجب أن تعرض دالة تجميع نواة الاختزال
voidوأن تحتوي على وسيطتين على الأقل. الوسيطة الأولى (accumفي هذا المثال) هي مؤشر إلى عنصر بيانات المجمِّع، ويتم ملء الوسيطة الثانية (valفي هذا المثال) تلقائيًا استنادًا إلى الإدخالAllocationالذي تم تمريره إلى تشغيل النواة. يتم إنشاء عنصر بيانات المجمّع من خلال وقت تشغيل RenderScript، ويتم ضبطه تلقائيًا على صفر. يتم تشغيل هذه النواة تلقائيًا على مستوى الإدخال بالكاملAllocation، مع تنفيذ واحد لدالة المُجمِّع لكلElementفيAllocation. بشكل تلقائي، يتم التعامل مع القيمة النهائية لعنصر بيانات المركم على أنها نتيجة الاختزال، ويتم إرجاعها إلى Java. تتحقّق بيئة تشغيل RenderScript للتأكّد من أنّ نوعElementالخاص بـ Allocation للإدخال يتطابق مع النموذج الأوّلي لدالة المُجمِّع، وإذا لم يتطابق، يعرض RenderScript استثناءً.تحتوي نواة الاختزال على مصفوفة
Allocationsواحدة أو أكثر للإدخال، ولكن لا تحتوي على مصفوفةAllocationsللإخراج.يمكنك الاطّلاع على مزيد من التفاصيل حول نواة الاختزال هنا.
تتوفّر نواة الاختزال في الإصدار 7.0 من نظام التشغيل Android (المستوى 24 من واجهة برمجة التطبيقات) والإصدارات الأحدث.
يمكن لدالة نواة الربط أو دالة تجميع نواة الاختزال الوصول إلى إحداثيات التنفيذ الحالي باستخدام الوسيطات الخاصة
xوyوz، والتي يجب أن تكون من النوعintأوuint32_t. هذه الوسيطات اختيارية.قد تستخدم دالة نواة الربط أو دالة تجميع نواة الاختزال أيضًا الوسيطة الخاصة الاختيارية
contextمن النوع rs_kernel_context. وهي مطلوبة من خلال مجموعة من واجهات برمجة التطبيقات لوقت التشغيل التي تُستخدم للاستعلام عن خصائص معينة للتنفيذ الحالي، على سبيل المثال، rsGetDimX. (تتوفّر الوسيطةcontextفي الإصدار 6.0 من نظام التشغيل Android (المستوى 23 من واجهة برمجة التطبيقات) والإصدارات الأحدث).- دالة
init()اختيارية. دالةinit()هي نوع خاص من الدوال القابلة للاستدعاء التي يشغّلها RenderScript عند إنشاء مثيل للنص البرمجي لأول مرة. يسمح هذا بحدوث بعض العمليات الحسابية تلقائيًا عند إنشاء النص البرمجي. - صفر أو أكثر من المتغيّرات والدوال العامة الثابتة للنصوص البرمجية يُعدّ المتغيّر العام الثابت للنص البرمجي مكافئًا للمتغيّر العام للنص البرمجي، ولكن لا يمكن الوصول إليه من خلال رمز Java. الدالة الثابتة هي دالة C عادية يمكن استدعاؤها من أي دالة kernel أو دالة قابلة للاستدعاء في النص البرمجي، ولكن لا يتم عرضها لواجهة برمجة تطبيقات Java. إذا لم تكن هناك حاجة إلى الوصول إلى دالة أو متغيّر عام في النص البرمجي من خلال رمز Java، ننصح بشدة بتعريفهما على أنّهما
static.
ضبط دقة النقطة العائمة
يمكنك التحكّم في المستوى المطلوب من دقة النقطة العائمة في نص برمجي. ويكون ذلك مفيدًا إذا لم يكن معيار 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).
الوصول إلى واجهات برمجة تطبيقات RenderScript من Java
عند تطوير تطبيق Android يستخدم RenderScript، يمكنك الوصول إلى واجهة برمجة التطبيقات الخاصة به من Java بإحدى طريقتين:
android.renderscript: تتوفّر واجهات برمجة التطبيقات في حزمة الفئة هذه على الأجهزة التي تعمل بنظام التشغيل Android 3.0 (المستوى 11 لواجهة برمجة التطبيقات) والإصدارات الأحدث.android.support.v8.renderscript: تتوفّر واجهات برمجة التطبيقات في هذه الحزمة من خلال مكتبة الدعم، ما يتيح لك استخدامها على الأجهزة التي تعمل بالإصدار 2.3 من نظام التشغيل Android (المستوى 9 من واجهة برمجة التطبيقات) والإصدارات الأحدث.
في ما يلي المفاضلات:
- إذا كنت تستخدم واجهات برمجة تطبيقات مكتبة الدعم، سيكون جزء RenderScript من تطبيقك متوافقًا مع الأجهزة التي تعمل بنظام التشغيل Android 2.3 (المستوى 9 لواجهة برمجة التطبيقات) والإصدارات الأحدث، بغض النظر عن ميزات RenderScript التي تستخدمها. يتيح ذلك لتطبيقك العمل على أجهزة أكثر مما لو كنت تستخدم واجهات برمجة التطبيقات الأصلية (
android.renderscript). - لا تتوفّر بعض ميزات RenderScript من خلال واجهات برمجة تطبيقات Support Library.
- إذا كنت تستخدم واجهات برمجة التطبيقات الخاصة بمكتبة الدعم، ستحصل على حِزم APK أكبر (ربما بشكل كبير) من
تلك التي تحصل عليها إذا كنت تستخدم واجهات برمجة التطبيقات الأصلية (
android.renderscript).
استخدام واجهات برمجة التطبيقات لمكتبة دعم RenderScript
لاستخدام واجهات برمجة تطبيقات RenderScript لمكتبة الدعم، يجب ضبط بيئة التطوير لتتمكّن من الوصول إليها. يجب استخدام أدوات حزمة تطوير البرامج (SDK) لنظام التشغيل Android التالية لاستخدام واجهات برمجة التطبيقات هذه:
- الإصدار 22.2 أو إصدار أحدث من أدوات حزمة تطوير البرامج (SDK) لنظام التشغيل Android
- الإصدار 18.1.0 أو إصدار أحدث من أدوات إنشاء حزمة تطوير البرامج (SDK) لنظام التشغيل Android
يُرجى العِلم أنّه اعتبارًا من الإصدار 24.0.0 من أدوات إنشاء حزمة تطوير البرامج (SDK) لنظام التشغيل Android، لم يعُد الإصدار 2.2 من نظام التشغيل Android (المستوى 8 من واجهة برمجة التطبيقات) متاحًا.
يمكنك التحقّق من الإصدار المثبَّت من هذه الأدوات وتحديثه في Android SDK Manager.
لاستخدام واجهات برمجة تطبيقات RenderScript في مكتبة الدعم:
- تأكَّد من تثبيت الإصدار المطلوب من حزمة تطوير البرامج (SDK) لنظام التشغيل Android.
- عدِّل إعدادات عملية إنشاء Android لتشمل إعدادات RenderScript:
- افتح ملف
build.gradleفي مجلد التطبيق الخاص بوحدة التطبيق. - أضِف إعدادات RenderScript التالية إلى الملف:
أنيق
android { compileSdkVersion 36 defaultConfig { minSdkVersion 9 targetSdkVersion 19 renderscriptTargetApi 18 renderscriptSupportModeEnabled true } }
Kotlin
android { compileSdkVersion(36) defaultConfig { minSdkVersion(9) targetSdkVersion(19) renderscriptTargetApi = 18 renderscriptSupportModeEnabled = true } }
تتحكّم الإعدادات المذكورة أعلاه في سلوك معيّن في عملية إنشاء إصدار Android:
renderscriptTargetApi- يحدّد إصدار الرمز الثانوي الذي سيتم إنشاؤه. ننصحك بضبط هذه القيمة على أدنى مستوى لواجهة برمجة التطبيقات يمكنه توفير جميع الوظائف التي تستخدمها وضبطrenderscriptSupportModeEnabledعلىtrue. القيم الصالحة لهذا الإعداد هي أي قيمة عدد صحيح من 11 إلى أحدث مستوى لواجهة برمجة التطبيقات تم إصداره. إذا تم ضبط الحد الأدنى لإصدار حزمة SDK المحدّد في بيان التطبيق على قيمة مختلفة، يتم تجاهل هذه القيمة واستخدام القيمة المستهدَفة في ملف الإصدار لضبط الحد الأدنى لإصدار حزمة SDK.renderscriptSupportModeEnabled: يحدّد هذا الخيار أنّه يجب أن يعود الرمز الثانوي الذي تم إنشاؤه إلى إصدار متوافق إذا كان الجهاز الذي يعمل عليه لا يتوافق مع الإصدار المستهدَف.
- افتح ملف
- في فئات التطبيق التي تستخدم RenderScript، أضِف عملية استيراد لفئات "مكتبة الدعم":
Kotlin
import android.support.v8.renderscript.*
Java
import android.support.v8.renderscript.*;
استخدام RenderScript من رمز Java أو Kotlin
يعتمد استخدام RenderScript من رمز Java أو Kotlin على فئات واجهة برمجة التطبيقات المتوفّرة في حزمة android.renderscript أو android.support.v8.renderscript. تتبع معظم التطبيقات نمط الاستخدام الأساسي نفسه:
- Initialize a RenderScript context. يضمن سياق
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(). تكون عمليات الإطلاق هذه غير متزامنة. استنادًا إلى الوسيطات التي تم تمريرها إلى النواة، تأخذ الطريقة تخصيصًا واحدًا أو أكثر، ويجب أن يكون لجميعها الأبعاد نفسها. بشكل تلقائي، يتم تنفيذ النواة على كل إحداثي في هذه الأبعاد. لتنفيذ نواة على مجموعة فرعية من هذه الإحداثيات، يجب تمريرScript.LaunchOptionsمناسب كوسيطة أخيرة إلى طريقةforEachأوreduce.يمكنك تشغيل الدوال القابلة للاستدعاء باستخدام طرق
invoke_functionNameالموضّحة في فئةScriptC_filenameنفسها. تكون عمليات الإطلاق هذه غير متزامنة. - استرداد البيانات من كائنات
Allocationوكائنات javaFutureType للوصول إلى البيانات منAllocationمن رمز Java، يجب نسخ هذه البيانات مرة أخرى إلى Java باستخدام إحدى طرق "النسخ" فيAllocation. للحصول على نتيجة نواة الاختزال، يجب استخدام طريقةjavaFutureType.get(). تكون الطريقتان "copy" وget()متزامنتين. - إزالة سياق RenderScript يمكنك تدمير سياق RenderScript
باستخدام
destroy()أو من خلال السماح بجمع المهملات من كائن سياق RenderScript. ويؤدي ذلك إلى ظهور استثناء عند استخدام أي عنصر آخر ينتمي إلى هذا السياق.
نموذج التنفيذ غير المتزامن
إنّ الطُرق المنعكسة forEach وinvoke وreduce وset هي طُرق غير متزامنة، وقد يعود كل منها إلى Java قبل إكمال الإجراء المطلوب. ومع ذلك، يتم تسلسل الإجراءات الفردية بالترتيب الذي يتم تشغيلها به.
توفّر الفئة Allocation طرق "نسخ" لنسخ البيانات من وإلى
عمليات التخصيص. طريقة "النسخ" متزامنة، ويتم تسلسلها في ما يتعلق بأي من الإجراءات غير المتزامنة أعلاه التي تؤثر في "التخصيص" نفسه.
توفّر فئات javaFutureType المنعكسة طريقة get() للحصول على نتيجة عملية الاختزال. get() هي دالة متزامنة، ويتم تسلسلها في ما يتعلق بعملية الاختزال (التي تكون غير متزامنة).
RenderScript أحادي المصدر
يقدّم Android 7.0 (مستوى واجهة برمجة التطبيقات 24) ميزة برمجة جديدة تُسمى Single-Source
RenderScript، حيث يتم تشغيل النواة من النص البرمجي الذي تم تحديدها فيه، بدلاً من
Java. يقتصر هذا النهج حاليًا على نواة الربط، والتي يُشار إليها ببساطة باسم "النواة" في هذا القسم للإيجاز. تتيح هذه الميزة الجديدة أيضًا إنشاء عمليات تخصيص من النوع
rs_allocation من داخل النص البرمجي. أصبح من الممكن الآن تنفيذ خوارزمية كاملة داخل نص برمجي واحد، حتى إذا كانت هناك حاجة إلى عمليات تشغيل متعددة للنواة.
تتمثّل الفائدة في جانبَين: رمز برمجي أكثر قابلية للقراءة، لأنّه يحافظ على تنفيذ خوارزمية بلغة واحدة، ورمز برمجي أسرع، بسبب تقليل عمليات الانتقال بين Java وRenderScript عبر عمليات تشغيل متعددة للنواة.
في Single-Source RenderScript، يمكنك كتابة النواة كما هو موضّح في
كتابة نواة RenderScript. بعد ذلك، يمكنك كتابة دالة قابلة للاستدعاء تستدعي
rsForEach() لتشغيلها. تتطلب واجهة برمجة التطبيقات هذه دالة kernel كمعلمة أولى، تليها عمليات تخصيص المدخلات والمخرجات. هناك واجهة برمجة تطبيقات مشابهة، وهي
rsForEachWithOptions()، تأخذ وسيطة إضافية من النوع
rs_script_call_t، والتي تحدّد مجموعة فرعية من العناصر من عمليات تخصيص الإدخال
والإخراج لكي تعالجها دالة النواة.
لبدء عملية احتساب RenderScript، يمكنك استدعاء الدالة القابلة للاستدعاء من Java.
اتّبِع الخطوات الواردة في مقالة استخدام RenderScript من رمز Java.
في خطوة تشغيل النواة المناسبة، استدعِ
الدالة القابلة للاستدعاء باستخدام invoke_function_name()، ما سيؤدي إلى بدء
عملية الحوسبة بأكملها، بما في ذلك تشغيل النواة.
غالبًا ما تكون عمليات التخصيص مطلوبة لحفظ النتائج الوسيطة ونقلها من عملية تشغيل نواة إلى أخرى. ويمكنك إنشاؤها باستخدام
rsCreateAllocation(). ومن بين النماذج السهلة الاستخدام لواجهة برمجة التطبيقات هذه
rsCreateAllocation_<T><W>(…)، حيث يمثّل T نوع البيانات للعنصر، ويمثّل W عرض المتجه للعنصر. تأخذ واجهة برمجة التطبيقات الأحجام في
الأبعاد X وY وZ كوسيطات. بالنسبة إلى عمليات التخصيص أحادية أو ثنائية الأبعاد، يمكن حذف حجم السمة Y أو Z. على سبيل المثال، ينشئ rsCreateAllocation_uchar4(16384) تخصيصًا أحادي الأبعاد لـ 16384 عنصرًا، كل منها من النوع uchar4.
تتم إدارة عمليات التخصيص من خلال النظام تلقائيًا. ليس عليك
تحريرها أو إخلاء سبيلها بشكل صريح. ومع ذلك، يمكنك استدعاء
rsClearObject(rs_allocation* alloc) للإشارة إلى أنّك لم تعُد بحاجة إلى المعرّف
alloc للتخصيص الأساسي،
ليتمكّن النظام من إخلاء الموارد في أقرب وقت ممكن.
يحتوي قسم كتابة نواة RenderScript على مثال لنواة تعكس صورة. يوسّع المثال أدناه ذلك لتطبيق أكثر من تأثير واحد على صورة،
باستخدام 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 والنص البرمجي عبر عمليات تشغيل النواة. قد تطلق بعض الخوارزميات التكرارية النواة مئات المرات، مما يجعل النفقات العامة لمثل هذا الانتقال كبيرة.
Script Globals
متغير النص البرمجي العمومي هو متغير عمومي عادي غير static في ملف نص برمجي (.rs). بالنسبة إلى متغيّر عمومي في النص البرمجي باسم var محدّد في الملف filename.rs، ستكون هناك طريقة get_var تنعكس في الفئة ScriptC_filename. ما لم يكن المتغيّر العام
هو const، ستكون هناك أيضًا
طريقة set_var.
يحتوي النص البرمجي العام على قيمتَين منفصلتَين: قيمة Java وقيمة النص البرمجي. تعمل هذه القيم على النحو التالي:
- إذا كان var يحتوي على مُهيئ ثابت في النص البرمجي، فإنه يحدد القيمة الأولية لـ var في كل من Java والنص البرمجي. وإلا، فإن هذه القيمة الأولية هي صفر.
- تؤدي عمليات الوصول إلى var ضمن النص البرمجي إلى قراءة قيمة النص البرمجي وكتابتها.
- تقرأ الطريقة
get_varقيمة Java. - تكتب طريقة
set_var(إذا كانت متوفّرة) قيمة Java على الفور، وتكتب قيمة النص البرمجي بشكل غير متزامن.
ملاحظة: يعني ذلك أنّه باستثناء أيّ مُهيئ ثابت في النص البرمجي، لا تظهر القيم المكتوبة في متغير عام من داخل النص البرمجي في Java.
نواة الاختزال بالتفصيل
الاختزال هو عملية دمج مجموعة من البيانات في قيمة واحدة. هذا عنصر أساسي مفيد في البرمجة المتوازية، مع تطبيقات مثل ما يلي:
- حساب المجموع أو حاصل الضرب على جميع البيانات
- حوسبة العمليات المنطقية (
andوorوxor) على جميع البيانات - العثور على الحد الأدنى أو الأقصى للقيمة ضمن البيانات
- البحث عن قيمة معيّنة أو عن إحداثيات قيمة معيّنة ضمن البيانات
في الإصدار 7.0 من نظام التشغيل Android (مستوى واجهة برمجة التطبيقات 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) من خلال دالة المجمّع في النواة، حيث تكون الوسيطة الأولى لهذه الدالة هي مؤشر إلى عنصر بيانات المجمّع. By default, every accumulator data item is initialized to zero (as if
by memset); however, you may write an initializer function to do something
different.
مثال: في نواة addint، يتم استخدام عناصر بيانات المجمِّع (من النوع int) لجمع قيم الإدخال. لا توجد دالة تهيئة، لذلك تتم تهيئة كل عنصر بيانات مجمّع على
صفر.
مثال: في نواة findMinAndMax، يتم استخدام عناصر بيانات المجمِّع (من النوع MinAndMax) لتتبُّع الحد الأدنى والحد الأقصى للقيم التي تم العثور عليها حتى الآن. هناك دالة تهيئة لضبط هذه القيم على LONG_MAX وLONG_MIN على التوالي، ولضبط مواقع هذه القيم على -1، ما يشير إلى أنّ القيم غير موجودة فعليًا في الجزء (الفارغ) من الإدخال الذي تمت معالجته.
يستدعي RenderScript دالة المُجمِّع مرة واحدة لكل إحداثي في المدخلات. عادةً، يجب أن تعدّل الدالة عنصر بيانات المجمِّع بطريقة ما وفقًا للإدخال.
مثال: في نواة addint، تضيف دالة المُجمِّع قيمة عنصر الإدخال إلى عنصر بيانات المُجمِّع.
مثال: في نواة findMinAndMax، تتحقّق دالة المُجمِّع من أنّ قيمة عنصر الإدخال أقل من أو تساوي الحد الأدنى للقيمة المسجَّلة في عنصر بيانات المُجمِّع و/أو أكبر من أو تساوي الحد الأقصى للقيمة المسجَّلة في عنصر بيانات المُجمِّع، وتعدِّل عنصر بيانات المُجمِّع وفقًا لذلك.
بعد استدعاء دالة المُجمِّع مرة واحدة لكل إحداثي في المدخلات، يجب على RenderScript دمج عناصر بيانات المُجمِّع معًا في عنصر بيانات مُجمِّع واحد. يمكنك كتابة دالة دمج لإجراء ذلك. إذا كانت دالة المُجمِّع تحتوي على إدخال واحد ولا تحتوي على وسيطات خاصة، لن تحتاج إلى كتابة دالة مُجمِّع، وسيستخدم RenderScript دالة المُجمِّع لدمج عناصر بيانات المُجمِّع. (لا يزال بإمكانك كتابة دالة دمج إذا لم يكن هذا السلوك الافتراضي هو ما تريده.)
مثال: في نواة addint، لا تتوفّر دالة دمج، لذا سيتم استخدام دالة التجميع. هذا هو السلوك الصحيح، لأنه إذا قمنا بتقسيم مجموعة من القيم إلى جزأين، وقمنا بجمع القيم في هذين الجزأين بشكل منفصل، فإن جمع هذين المجموعين هو نفسه جمع المجموعة بأكملها.
مثال: في نواة findMinAndMax، تتحقّق دالة الدمج من أنّ الحدّ الأدنى للقيمة المسجَّلة في عنصر بيانات المجمِّع "المصدر" *val أقل من الحدّ الأدنى للقيمة المسجَّلة في عنصر بيانات المجمِّع "الوجهة" *accum، وتعدّل *accum وفقًا لذلك. ويؤدي عملًا مشابهًا للقيمة القصوى. يؤدي ذلك إلى تعديل *accum
إلى الحالة التي كان من الممكن أن تكون عليها إذا تم تجميع جميع قيم الإدخال في
*accum بدلاً من بعضها في *accum وبعضها في
*val.
بعد دمج جميع عناصر بيانات المُجمِّع، يحدّد RenderScript نتيجة الاختزال التي سيتم عرضها في Java. يمكنك كتابة دالة outconverter لإجراء ذلك. لست بحاجة إلى كتابة دالة 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)(اختياري): يحدّد اسم دالة الدمج لنواة الاختزال هذه. بعد أن يستدعي 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هو مؤشر إلى عنصر بيانات المجمِّع النهائي الذي تم احتسابه من خلال دالة الدمج.إذا لم توفّر دالة outconverter، ينسخ RenderScript عنصر بيانات المجمّع النهائي إلى عنصر بيانات النتيجة، ويتصرف كما لو كانت هناك دالة outconverter تبدو على النحو التالي:
static void outconverterName(accumType *result, const accumType *accum) { *result = *accum; }
إذا أردت نوع نتيجة مختلفًا عن نوع بيانات المجمِّع، تكون دالة outconverter إلزامية.
يُرجى العِلم أنّ النواة تتضمّن أنواع إدخال ونوع عنصر بيانات المجمِّع ونوع النتيجة،
ولا يجب أن تكون أي منها متطابقة. على سبيل المثال، في النواة findMinAndMax، يختلف نوع الإدخال long ونوع عنصر بيانات المُجمِّع MinAndMax ونوع النتيجة int2 عن بعضها البعض.
ما الذي لا يمكنك افتراضه؟
يجب عدم الاعتماد على عدد عناصر بيانات المُجمِّع التي أنشأها RenderScript لتشغيل نواة معيّنة. لا نضمن أنّ عمليتَي تشغيل للنواة نفسها باستخدام الإدخالات نفسها ستؤديان إلى إنشاء العدد نفسه من عناصر بيانات المُجمِّع.
يجب عدم الاعتماد على الترتيب الذي يستدعي به RenderScript دوال المُهيئ والمُجمِّع والمُدمِج، بل قد يستدعي بعضها بالتوازي. ولا نضمن أن يتّبع تشغيلان للنواة نفسها مع الإدخال نفسه الترتيب نفسه. الضمان الوحيد هو أنّ دالة الإعداد فقط هي التي ستتمكّن من رؤية عنصر بيانات المجمّع غير المُعدّ. على سبيل المثال:
- ليس هناك ما يضمن أنّه سيتم إعداد جميع عناصر بيانات المجمِّع قبل استدعاء دالة المجمِّع، على الرغم من أنّه لن يتم استدعاؤها إلا على عنصر بيانات مجمِّع تم إعداده.
- لا نقدّم أي ضمان بشأن الترتيب الذي يتم به تمرير عناصر الإدخال إلى دالة التجميع.
- لا نضمن أنّه تم استدعاء دالة المُجمِّع لجميع عناصر الإدخال قبل استدعاء دالة الدمج.
ومن نتائج ذلك أنّ النواة findMinAndMax ليست حتمية: إذا كان الإدخال يحتوي على أكثر من تكرار واحد للحد الأدنى أو الأقصى نفسه للقيمة، لن تتمكّن من معرفة التكرار الذي ستعثر عليه النواة.
ما الذي يجب أن تضمنه؟
بما أنّ نظام RenderScript يمكنه اختيار تنفيذ النواة بطرق مختلفة، عليك اتّباع قواعد معيّنة لضمان عمل النواة بالطريقة التي تريدها. إذا لم تتبع هذه القواعد، قد تحصل على نتائج غير صحيحة أو سلوك غير حتمي أو أخطاء أثناء التشغيل.
غالبًا ما تنص القواعد أدناه على أنّ عنصرَي بيانات المجمّع يجب أن يكون لهما القيمة نفسها. ماذا يعني ذلك؟ يعتمد ذلك على ما تريد أن تفعله النواة. بالنسبة إلى الاختزال الرياضي مثل addint، من المنطقي عادةً أن تعني عبارة "نفس" المساواة الرياضية. بالنسبة إلى عملية بحث "اختيار أي" مثل findMinAndMax ("العثور على موقع الحد الأدنى والحد الأقصى لقيم الإدخال") حيث قد يكون هناك أكثر من تكرار واحد لقيم الإدخال المتطابقة، يجب اعتبار جميع مواقع قيمة إدخال معينة "متطابقة". يمكنك كتابة نواة مشابهة "للعثور على موضع الحد الأدنى والحد الأقصى لقيم الإدخال في أقصى اليسار" حيث (على سبيل المثال) يُفضَّل الحد الأدنى للقيمة في الموضع 100 على الحد الأدنى للقيمة المطابقة في الموضع 200؛ بالنسبة إلى هذه النواة، تعني عبارة "نفسه" الموضع المطابق، وليس مجرد القيمة المطابقة، ويجب أن تكون دالتا المُجمِّع والمُدمِج مختلفتين عن دالتي 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.
مثال: في نواة addint، تضيف دالة الدمج قيمتَي عنصرَي بيانات المجمِّع، وتكون عملية الجمع تبادلية.
مثال: في النواة findMinAndMax،
fMMCombiner(&A, &B) هي نفسها
A = minmax(A, B)، وminmax تبادلية، لذا
fMMCombiner تبادلية أيضًا.
يجب أن تكون دالة الدمج تجميعية. وهذا يعني أنّه إذا كانت A وB وC عناصر بيانات المركم التي تم إعدادها بواسطة دالة الإعداد، والتي قد تم تمريرها إلى دالة المركم صفر مرة أو أكثر، فيجب أن يضبط تسلسلا الرمز التاليان A على القيمة نفسها:
combinerName(&A, &B); combinerName(&A, &C);
combinerName(&B, &C); combinerName(&A, &B);
مثال: في النواة addint، تضيف دالة الدمج قيمتَي عنصرَي بيانات المجمِّع:
A = A + B A = A + C // Same as // A = (A + B) + C
B = B + C A = A + B // Same as // A = A + (B + C) // B = B + C
الجمع هو عملية تجميعية، وبالتالي فإن دالة الدمج هي أيضًا كذلك.
مثال: في النواة findMinAndMax،
fMMCombiner(&A, &B)
A = minmax(A, B)
A = minmax(A, B) A = minmax(A, C) // Same as // A = minmax(minmax(A, B), C)
B = minmax(B, C) A = minmax(A, B) // Same as // A = minmax(A, minmax(B, C)) // B = minmax(B, C)
minmax تجميعية، وبالتالي fMMCombiner تجميعية أيضًا.
يجب أن تلتزم دالة التجميع ودالة الدمج معًا بقاعدة
الطي الأساسية. أي إذا كان A وB هما عنصرا بيانات المجمِّع، وتمت تهيئة A بواسطة دالة المُهيِّئ وربما تم تمريره إلى دالة المجمِّع صفر مرة أو أكثر، ولم تتم تهيئة B، وargs هي قائمة وسيطات الإدخال والوسيطات الخاصة لاستدعاء معين لدالة المجمِّع، فيجب أن يضبط تسلسلا الرمز التاليان A على القيمة نفسها:
accumulatorName(&A, args); // statement 1
initializerName(&B); // statement 2 accumulatorName(&B, args); // statement 3 combinerName(&A, &B); // statement 4
مثال: في النواة addint، بالنسبة إلى قيمة الإدخال V:
- العبارة 1 هي نفسها
A += V - العبارة 2 هي نفسها
B = 0 - العبارة 3 هي نفسها
B += V، وهي نفسهاB = V - العبارة 4 هي نفسها
A += B، وهي نفسهاA += V
تضبط العبارتان 1 و4 قيمة A على القيمة نفسها، وبالتالي تلتزم هذه النواة بقاعدة الطي الأساسية.
مثال: في نواة findMinAndMax، بالنسبة إلى قيمة الإدخال V عند الإحداثي X:
- العبارة 1 هي نفسها
A = minmax(A, IndexedVal(V, X)) - العبارة 2 هي نفسها
B = INITVAL - العبارة 3 هي نفسها
وهي نفسها، لأنّ B هي القيمة الأولية،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 استثناءً. يتم تنفيذ النواة على كل إحداثي في هذه الأبعاد.
الطريقة الثانية هي نفسها الطريقة الأولى باستثناء أنّ الطريقة الثانية تتطلّب وسيطة إضافية sc يمكن استخدامها لتقييد تنفيذ النواة على مجموعة فرعية من الإحداثيات.
الطريقة الثالثة هي نفسها الطريقة الأولى باستثناء أنّها
تأخذ مدخلات مصفوفة Java بدلاً من مدخلات التخصيص. هذه ميزة مريحة
توفّر عليك كتابة رمز برمجي لإنشاء تخصيص بشكل صريح ونسخ البيانات إليه
من مصفوفة Java. ومع ذلك، فإن استخدام الطريقة 3 بدلاً من الطريقة 1 لا يزيد من أداء الرمز. For each input array, Method 3 creates a temporary
1-dimensional Allocation with the appropriate Element type and
setAutoPadding(boolean) enabled, and copies the array to the
Allocation as if by the appropriate copyFrom() method of Allocation. بعد ذلك، يستدعي الطريقة 1، ويمرّر هذه التخصيصات المؤقتة.
ملاحظة: إذا كان تطبيقك سيُجري عدة طلبات kernel باستخدام المصفوفة نفسها أو باستخدام مصفوفات مختلفة من الأبعاد ونوع العنصر نفسه، يمكنك تحسين الأداء من خلال إنشاء عمليات التخصيص وتعبئتها وإعادة استخدامها بنفسك بشكل صريح، بدلاً من استخدام الطريقة الثالثة.
javaFutureType،
نوع القيمة التي تم إرجاعها لطرق الاختزال المنعكسة، هو فئة متداخلة ثابتة منعكسة
ضمن فئة ScriptC_filename. وتمثّل النتيجة المستقبلية لتشغيل نواة
اختزال. للحصول على النتيجة الفعلية للتشغيل، يمكنك استدعاء طريقة get() لهذه الفئة، والتي تعرض قيمة من النوع javaResultType. get() متزامن.
Kotlin
class ScriptC_filename(rs: RenderScript) : ScriptC(…) { object javaFutureType { fun get(): javaResultType { … } } }
Java
public class ScriptC_filename extends ScriptC { public static class javaFutureType { public javaResultType get() { … } } }
يتم تحديد javaResultType من resultType في دالة outconverter. ما لم يكن resultType نوعًا غير موقَّع (مقياس أو متجه أو صفيفة)، يكون javaResultType هو نوع Java المقابل مباشرةً. إذا كان resultType نوعًا غير موقَّع وكان هناك نوع Java موقَّع أكبر، يكون javaResultType هو نوع Java الموقَّع الأكبر، وإلا سيكون نوع Java المقابل مباشرةً. على سبيل المثال:
- إذا كانت قيمة resultType هي
intأوint2أوint[15]، تكون قيمة javaResultType هيintأوInt2أوint[]. يمكن تمثيل جميع قيم resultType من خلال javaResultType. - إذا كانت قيمة resultType هي
uintأوuint2أوuint[15]، تكون قيمة javaResultType هيlongأوLong2أوlong[]. يمكن تمثيل جميع قيم resultType من خلال javaResultType. - إذا كانت قيمة resultType هي
ulongأوulong2أوulong[15]، تكون قيمة javaResultType هيlongأوLong2أوlong[]. هناك قيم معيّنة لـ resultType لا يمكن تمثيلها من خلال javaResultType.
javaFutureType هو نوع النتيجة المستقبلية المقابل لـ resultType في دالة outconverter.
- إذا لم يكن resultType نوع مصفوفة، يكون javaFutureType
هو
result_resultType. - إذا كانت قيمة resultType مصفوفة بطول Count مع عناصر من النوع memberType،
تكون قيمة javaFutureType هي
resultArrayCount_memberType.
على سبيل المثال:
Kotlin
class ScriptC_filename(rs: RenderScript) : ScriptC(…) { // for kernels with int result object result_int { fun get(): Int = … } // for kernels with int[10] result object resultArray10_int { fun get(): IntArray = … } // for kernels with int2 result // note that the Kotlin type name "Int2" is not the same as the script type name "int2" object result_int2 { fun get(): Int2 = … } // for kernels with int2[10] result // note that the Kotlin type name "Int2" is not the same as the script type name "int2" object resultArray10_int2 { fun get(): Array<Int2> = … } // for kernels with uint result // note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint" object result_uint { fun get(): Long = … } // for kernels with uint[10] result // note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint" object resultArray10_uint { fun get(): LongArray = … } // for kernels with uint2 result // note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2" object result_uint2 { fun get(): Long2 = … } // for kernels with uint2[10] result // note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2" object resultArray10_uint2 { fun get(): Array<Long2> = … } }
Java
public class ScriptC_filename extends ScriptC { // for kernels with int result public static class result_int { public int get() { … } } // for kernels with int[10] result public static class resultArray10_int { public int[] get() { … } } // for kernels with int2 result // note that the Java type name "Int2" is not the same as the script type name "int2" public static class result_int2 { public Int2 get() { … } } // for kernels with int2[10] result // note that the Java type name "Int2" is not the same as the script type name "int2" public static class resultArray10_int2 { public Int2[] get() { … } } // for kernels with uint result // note that the Java type "long" is a wider signed type than the unsigned script type "uint" public static class result_uint { public long get() { … } } // for kernels with uint[10] result // note that the Java type "long" is a wider signed type than the unsigned script type "uint" public static class resultArray10_uint { public long[] get() { … } } // for kernels with uint2 result // note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2" public static class result_uint2 { public Long2 get() { … } } // for kernels with uint2[10] result // note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2" public static class resultArray10_uint2 { public Long2[] get() { … } } }
إذا كان javaResultType نوع كائن (بما في ذلك نوع مصفوفة)، سيؤدي كل استدعاء
لـ javaFutureType.get() على المثيل نفسه إلى عرض الكائن
نفسه.
إذا لم يتمكّن javaResultType من تمثيل جميع قيم النوع resultType، وأنتجت نواة الاختزال قيمة غير قابلة للتمثيل، فسيُظهر javaFutureType.get() استثناءً.
الطريقة 3 وdevecSiInXType
devecSiInXType هو نوع Java المقابل لـ inXType للوسيطة المقابلة لـ دالة المُجمِّع. ما لم يكن inXType نوعًا غير موقَّع أو نوعًا متجهًا، يكون devecSiInXType هو نوع Java المقابل مباشرةً. إذا كان inXType نوعًا عدديًا غير موقَّع، يكون devecSiInXType هو نوع Java الذي يتوافق مباشرةً مع النوع العددي الموقَّع من الحجم نفسه. إذا كان inXType نوع متجه موقَّع، يكون devecSiInXType هو نوع Java الذي يتوافق مباشرةً مع نوع مكوّن المتجه. إذا كان inXType نوع متجه غير موقَّع، يكون devecSiInXType هو نوع Java الذي يتوافق مباشرةً مع نوع العدد القياسي الموقَّع بالحجم نفسه لنوع مكوّن المتجه. على سبيل المثال:
- إذا كانت قيمة inXType هي
int، تكون قيمة devecSiInXType هيint. - إذا كانت قيمة inXType هي
int2، تكون قيمة devecSiInXType هيint. المصفوفة هي تمثيل مبسّط: تحتوي على ضعف عدد عناصر القيم العددية التي تحتوي عليها عناصر المتّجهات المكوّنة من عنصرين في عملية التخصيص. هذه هي الطريقة نفسها التي تعمل بها طرقcopyFrom()فيAllocation. - إذا كانت قيمة inXType هي
uint، تكون قيمة deviceSiInXType هيint. يتم تفسير القيمة الموقَّعة في مصفوفة Java على أنّها قيمة غير موقَّعة لنمط البت نفسه في "التخصيص". وهذه هي الطريقة نفسها التي تعمل بها طرقcopyFrom()فيAllocation. - إذا كانت قيمة inXType هي
uint2، تكون قيمة deviceSiInXType هيint. هذا مزيج من طريقة التعامل معint2وuint: المصفوفة هي تمثيل مسطّح، ويتم تفسير القيم الموقَّعة لمصفوفة Java على أنّها قيم عناصر غير موقَّعة في RenderScript.
يُرجى العِلم أنّه بالنسبة إلى الطريقة 3، يتم التعامل مع أنواع الإدخال بشكل مختلف عن أنواع النتائج:
- يتم تسوية إدخال المتجه الخاص بالنص البرمجي على جانب Java، في حين لا يتم تسوية نتيجة المتجه الخاص بالنص البرمجي.
- يتم تمثيل الإدخال غير الموقَّع للنص البرمجي كإدخال موقَّع بالحجم نفسه في جانب Java، بينما يتم تمثيل النتيجة غير الموقَّعة للنص البرمجي كنوع موقَّع موسَّع في جانب Java (باستثناء حالة
ulong).
أمثلة إضافية على نواة الاختزال
#pragma rs reduce(dotProduct) \ accumulator(dotProductAccum) combiner(dotProductSum) // Note: No initializer function -- therefore, // each accumulator data item is implicitly initialized to 0.0f. static void dotProductAccum(float *accum, float in1, float in2) { *accum += in1*in2; } // combiner function static void dotProductSum(float *accum, const float *val) { *accum += *val; }
// Find a zero Element in a 2D allocation; return (-1, -1) if none #pragma rs reduce(fz2) \ initializer(fz2Init) \ accumulator(fz2Accum) combiner(fz2Combine) static void fz2Init(int2 *accum) { accum->x = accum->y = -1; } static void fz2Accum(int2 *accum, int inVal, int x /* special arg */, int y /* special arg */) { if (inVal==0) { accum->x = x; accum->y = y; } } static void fz2Combine(int2 *accum, const int2 *accum2) { if (accum2->x >= 0) *accum = *accum2; }
// Note that this kernel returns an array to Java #pragma rs reduce(histogram) \ accumulator(hsgAccum) combiner(hsgCombine) #define BUCKETS 256 typedef uint32_t Histogram[BUCKETS]; // Note: No initializer function -- // therefore, each bucket is implicitly initialized to 0. static void hsgAccum(Histogram *h, uchar in) { ++(*h)[in]; } static void hsgCombine(Histogram *accum, const Histogram *addend) { for (int i = 0; i < BUCKETS; ++i) (*accum)[i] += (*addend)[i]; } // Determines the mode (most frequently occurring value), and returns // the value and the frequency. // // If multiple values have the same highest frequency, returns the lowest // of those values. // // Shares functions with the histogram reduction kernel. #pragma rs reduce(mode) \ accumulator(hsgAccum) combiner(hsgCombine) \ outconverter(modeOutConvert) static void modeOutConvert(int2 *result, const Histogram *h) { uint32_t mode = 0; for (int i = 1; i < BUCKETS; ++i) if ((*h)[i] > (*h)[mode]) mode = i; result->x = mode; result->y = (*h)[mode]; }
عيّنات تعليمات برمجية إضافية
توضّح نماذج BasicRenderScript وRenderScriptIntrinsic وHello Compute استخدام واجهات برمجة التطبيقات المشمولة في هذه الصفحة.