RenderScript هو إطار عمل لتنفيذ مهام مكثّفة من الناحية الحسابية بأداء عالٍ على Android. تم تصميم RenderScript بشكل أساسي للاستخدام مع عمليات الحوسبة المتوازية للبيانات، على الرغم من إمكانية الاستفادة من أعباء العمل التسلسلية أيضًا. يوازي وقت تشغيل RenderScript العمل على جميع معالِجات البيانات المتاحة على الأجهزة، مثل وحدات المعالجة المركزية المتعددة النواة ووحدات معالجة الرسومات. يتيح لك هذا التركيز على التعبير عن الخوارزميات بدلاً من جدولة العمل. ويُعدّ RenderScript مفيدًا على وجه الخصوص للتطبيقات التي تُجري معالجة الصور أو التصوير الحاسوبي أو الرؤية الحاسوبية.
لبدء استخدام RenderScript، هناك مفهومان رئيسيان يجب فهمهما:
- اللغة نفسها هي لغة مشتقة من C99 لكتابة رموز حوسبة عالية الأداء. توضّح كتابة Kernel في RenderScript طريقة استخدامها لكتابة نواة الحوسبة.
- يتم استخدام control API لإدارة موارد RenderScript الدائمة والتحكّم في تنفيذ النواة. وهو متوفر بثلاث لغات مختلفة: Java، وC++ في Android NDK، ولغة kernel المستمدة من C99 نفسها. يصف استخدام RenderScript من رمز Java و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 لتنفيذها بالتوازي على مستوى مجموعة من البيانات. هناك نوعان من النواة الحوسبية: نواة التعيين (تُسمّى أيضًا نواة لكلّ) ونواة الخفض.
نواة التعيين هي دالة متوازية تعمل على مجموعة من
Allocations
بالأبعاد نفسها. وبشكلٍ تلقائي، يتم تنفيذها مرة واحدة لكل إحداثي في تلك السمات. ويتم استخدامها عادةً (وليس حصريًا) لتحويل مجموعة من الإدخالاتAllocations
إلى ناتجAllocation
بمقدارElement
في المرة الواحدة.في ما يلي مثال على نواة ربط بسيطة:
uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) { uchar4 out = in; out.r = 255 - in.r; out.g = 255 - in.g; out.b = 255 - in.b; return out; }
وفي معظم الجوانب، تطابق هذه الدالة مع الدالة C العادية. تحدّد السمة
RS_KERNEL
المطبّقة على دالة النموذج الأوّلي أنّ الدالة هي نواة ربط لـ RenderScript بدلاً من دالة قابلة للاستعانة بها. يتم ملء الوسيطةin
تلقائيًا استنادًا إلى الإدخالAllocation
الذي تم تمريره إلى تشغيل النواة. وتتم مناقشة الوسيطاتx
وy
أدناه. تُكتب القيمة المعروضة من النواة تلقائيًا في المكان المناسب في الناتجAllocation
. يتم تشغيل هذه النواة تلقائيًا على مستوى الإدخالAllocation
الكامل، مع تنفيذ دالة kernel مرة واحدة لكلElement
فيAllocation
.قد تحتوي نواة الربط على إدخال
Allocations
واحد أو أكثر، أو إخراج واحدAllocation
، أو كليهما. يتحقّق وقت تشغيل RenderScript من التأكّد من أنّ جميع عمليات تخصيص المدخلات والمخرجات لها الأبعاد نفسها، ومن تطابق أنواعElement
من تخصيصات المدخلات والمخرجات مع النموذج الأوّلي للنواة. إذا لم تنجح أي من عمليتَي الفحص، تُصدر RenderScript استثناءً.ملاحظة: قبل الإصدار Android 6.0 (المستوى 23 من واجهة برمجة التطبيقات)، قد لا يكون لنواة الربط أكثر من مصدر إدخال واحد
Allocation
.إذا كنت بحاجة إلى إدخال أو مخرجات
Allocations
أكثر من kernel، يجب ربط هذه الكائنات بالنصوص البرمجيةrs_allocation
العامة والوصول إليها من kernel أو دالة قابلة للاستعانة بها من خلالrsGetElementAt_type()
أوrsSetElementAt_type()
.ملاحظة:
RS_KERNEL
هي وحدة ماكرو يتم تحديدها تلقائيًا بواسطة RenderScript لتسهيل الأمر عليك:#define RS_KERNEL __attribute__((kernel))
نواة الاختزال هي مجموعة من الدوال التي تعمل على مجموعة من المدخلات
Allocations
لها السمات نفسها. وبشكلٍ تلقائي، يتم تنفيذ دالة المركم الخاصة بها مرة واحدة لكل إحداثي في هذه السمات. ويتم استخدامها عادةً (ولكن ليس حصريًا) "لتقليل" مجموعة إدخالاتAllocations
إلى قيمة واحدة.إليك مثال على نواة تخفيض بسيطة تضيف
Elements
الإدخال الخاص بها:#pragma rs reduce(addint) accumulator(addintAccum) static void addintAccum(int *accum, int val) { *accum += val; }
تتكوّن نواة الاختزال من دالة واحدة أو أكثر من الدوال المكتوبة من قِبل المستخدم. تُستخدم
#pragma rs reduce
لتعريف النواة من خلال تحديد اسمها (في هذا المثالaddint
) وأسماء الدوال التي تتألف منها النواة وأدوارها (وهي الدالةaccumulator
addintAccum
في هذا المثال). يجب أن تكون جميع هذه الدوالstatic
. تتطلب نواة الاختزال دائمًا دالةaccumulator
، وقد يكون لها أيضًا وظائف أخرى، بناءً على ما تريد أن تفعله النواة.يجب أن تعرض دالة مركم نواة التقليل
void
وأن تحتوي على وسيطتين على الأقل. الوسيطة الأولى (accum
، في هذا المثال) هي مؤشر لعنصر بيانات المركم، والوسيطة الثانية (في هذا المثال:val
) يتم ملؤها تلقائيًا استنادًا إلى الإدخالAllocation
الذي تم تمريره إلى تشغيل النواة. ويتم إنشاء عنصر بيانات المركم من خلال وقت تشغيل RenderScript، ويتم إعداده تلقائيًا على صفر. يتم تشغيل هذه النواة تلقائيًا على مستوى الإدخالAllocation
بالكامل، مع تنفيذ دالة المركم واحدة في كلElement
فيAllocation
. وبشكلٍ تلقائي، يتم التعامل مع القيمة النهائية لعنصر بيانات المركم كنتيجة للخفض، ويتم إرجاعها إلى Java. يفحص وقت تشغيل RenderScript للتأكّد من تطابق النوعElement
من تخصيص الإدخال مع النموذج الأوّلي لدالة المركم. وفي حال عدم التطابق، يقدّم RenderScript استثناءً.تحتوي نواة التقليل على إدخال
Allocations
واحد أو أكثر ولكن لا تحتوي على ناتجAllocations
.يمكنك الاطّلاع على مزيد من التفاصيل هنا حول نواة تقليل الانحدار.
تتوفّر نواات الانخفاض في الإصدار 7.0 (المستوى 24 من واجهة برمجة التطبيقات) والإصدارات الأحدث من نظام التشغيل Android.
يمكن لدالة kernel للتعيين أو دالة مركم نواة الاختزال الوصول إلى إحداثيات التنفيذ الحالي باستخدام الوسيطات الخاصة
x
وy
وz
، والتي يجب أن تكون من النوعint
أوuint32_t
. هذه الوسيطات اختيارية.قد تستخدم أيضًا دالة kernel للتعيين أو دالة مركم نواة الاختزال الوسيطة الخاصة الاختيارية
context
من النوع rs_kernel_context. وهو مطلوب من قِبل مجموعة من واجهات برمجة التطبيقات لوقت التشغيل التي يتم استخدامها للاستعلام عن خصائص معيّنة لعملية التنفيذ الحالية، مثل rsGetDimX. (تتوفّر الوسيطةcontext
في الإصدار Android 6.0 (المستوى 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 من واجهة برمجة التطبيقات) والإصدارات الأحدث.
في ما يلي الإيجابيات والسلبيات:
- إذا كنت تستخدم واجهات برمجة تطبيقات Support Library، سيكون جزء RenderScript في تطبيقك متوافقًا مع الأجهزة التي تعمل بالإصدار 2.3 من نظام التشغيل Android (المستوى 9 من واجهة برمجة التطبيقات) والإصدارات الأحدث، بغض النظر عن ميزات RenderScript التي تستخدمها. ويتيح ذلك لتطبيقك العمل على عدد أكبر من الأجهزة مقارنةً باستخدام واجهات برمجة التطبيقات (API) الأصلية (
android.renderscript
). - لا تتوفّر بعض ميزات RenderScript من خلال واجهات برمجة تطبيقات Support Library.
- وإذا كنت تستخدم واجهات برمجة تطبيقات Support Library، ستحصل على حِزم APK أكبر (ربما بشكلٍ كبير) مقارنةً بواجهات برمجة التطبيقات الأصلية (
android.renderscript
).
استخدام واجهات برمجة تطبيقات RenderScript Support Library
لاستخدام واجهات برمجة تطبيقات Support Library RenderScript، يجب إعداد بيئة التطوير فيها حتى تتمكَّن من الوصول إليها. الأدوات التالية لحزمة تطوير البرامج (SDK) لنظام التشغيل Android مطلوبة لاستخدام واجهات برمجة التطبيقات هذه:
- الإصدار 22.2 من أدوات حزمة تطوير البرامج (SDK) لنظام التشغيل Android أو إصدار أحدث
- الإصدار 18.1.0 أو إصدار أحدث من أدوات إنشاء حزمة تطوير البرامج (SDK) لنظام التشغيل Android
يُرجى العلم أنّه بدءًا من الإصدار 24.0.0 من Android SDK Build-tools، لن يعود الإصدار Android 2.2 (المستوى 8 من واجهة برمجة التطبيقات) متاحًا.
يمكنك التحقّق من الإصدار المثبَّت من هذه الأدوات وتحديثه في إدارة حزمة تطوير البرامج (SDK) لنظام التشغيل Android.
لاستخدام واجهات برمجة تطبيقات Support Library RenderScript، اتّبِع الخطوات التالية:
- تأكَّد من تثبيت الإصدار المطلوب من حزمة تطوير البرامج (SDK) لنظام التشغيل Android.
- عدِّل إعدادات عملية إصدار Android لتشمل إعدادات RenderScript:
- افتح ملف
build.gradle
في مجلد التطبيق في وحدة التطبيق. - أضِف إعدادات RenderScript التالية إلى الملف:
رائع
android { compileSdkVersion 33 defaultConfig { minSdkVersion 9 targetSdkVersion 19 renderscriptTargetApi 18 renderscriptSupportModeEnabled true } }
Kotlin
android { compileSdkVersion(33) defaultConfig { minSdkVersion(9) targetSdkVersion(19) renderscriptTargetApi = 18 renderscriptSupportModeEnabled = true } }
تتحكم الإعدادات المذكورة أعلاه في سلوك معين في عملية إصدار 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
. تتبع معظم التطبيقات
نمط الاستخدام الأساسي نفسه:
- إعداد سياق RenderScript: يضمن سياق
RenderScript
، الذي تم إنشاؤه باستخدامcreate(Context)
، إمكانية استخدام RenderScript ويوفّر كائنًا للتحكم في فترة بقاء جميع كائنات RenderScript اللاحقة. يجب أن تعتبر عملية إنشاء السياق عملية طويلة الأمد على الأرجح لأنّها قد تنشئ موارد على أجزاء مختلفة من الأجهزة ويجب ألّا تكون في المسار الحرج للتطبيق إذا كان ذلك ممكنًا. وعادةً ما يحتوي التطبيق على سياق RenderScript واحد فقط في كل مرة. - أنشِئ
Allocation
واحد على الأقل ليتم تمريره إلى نص برمجي.Allocation
هو كائن RenderScript يوفّر مساحة تخزين لكمية ثابتة من البيانات. تستخدم النواة في النصوص البرمجية كائناتAllocation
كمدخلات ومخرجات، ويمكن الوصول إلى كائناتAllocation
في النواة باستخدامrsGetElementAt_type()
وrsSetElementAt_type()
عند ربطها كقيم عامة للنص البرمجي. تسمح كائناتAllocation
بتمرير الصفائف من رمز Java إلى رمز RenderScript، والعكس صحيح. يتم عادةً إنشاء عناصرAllocation
باستخدامcreateTyped()
أوcreateFromBitmap()
. - أنشِئ النصوص البرمجية اللازمة. يتوفّر نوعان من النصوص البرمجية
عند استخدام RenderScript:
- ScriptC: هذه هي النصوص البرمجية التي يحددها المستخدم كما هو موضح في كتابة Kernel 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: هذه هي النصوص البرمجية التي يحددها المستخدم كما هو موضح في كتابة Kernel RenderScript أعلاه. يحتوي كل نص برمجي على فئة Java التي يعكسها المحول البرمجي لـ RenderScript من أجل تسهيل الوصول إلى النص البرمجي من رمز Java، وتحمل هذه الفئة اسم
- ملء عمليات التخصيص بالبيانات: باستثناء التخصيصات التي تم إنشاؤها باستخدام
createFromBitmap()
، تتم تعبئة التخصيص ببيانات فارغة عند إنشائه لأول مرة. لتعبئة عملية تخصيص، استخدِم إحدى طرق "النسخ" فيAllocation
. تكون طرق "النسخ" متزامنة. - اضبط أي عبارات عمومية ضرورية للنصوص. يمكنك ضبط القيم العمومية باستخدام طُرق في فئة
ScriptC_filename
نفسها المسماةset_globalname
. على سبيل المثال، لضبط متغيّرint
باسمthreshold
، استخدِم طريقة Javaset_threshold(int)
، ولضبط متغيّرrs_allocation
باسمlookup
، استخدِم طريقة Javaset_lookup(Allocation)
. طريقةset
غير متزامنة. - ابدأ تشغيل النواة المناسبة والدوال القابلة للاستدعاء.
وتظهر طرق إطلاق نواة معيّنة في الفئة
ScriptC_filename
نفسها باستخدام طُرق تُسمىforEach_mappingKernelName()
أوreduce_reductionKernelName()
. عمليات الإطلاق هذه غير متزامنة. اعتمادًا على وسيطات النواة، تتخذ الطريقة تخصيصًا واحدًا أو أكثر، ويجب أن يكون لكل منها الأبعاد نفسها. يتم بشكل تلقائي تنفيذ kernel على كل إحداثي في هذه الأبعاد. ولتنفيذ kernel على مجموعة فرعية من هذه الإحداثيات، يُرجى تمريرScript.LaunchOptions
مناسبة كوسيطة أخيرة إلى الطريقةforEach
أوreduce
.ابدأ تشغيل الدوال القابلة للاستدعاء باستخدام طرق
invoke_functionName
المنعكسة في فئةScriptC_filename
نفسها. عمليات الإطلاق هذه غير متزامنة. - يمكنك استرداد البيانات من كائنات
Allocation
وكائنات javaFutureType. للوصول إلى البيانات منAllocation
من رمز Java، يجب نسخ هذه البيانات مرة أخرى إلى Java باستخدام إحدى طرق "النسخ" فيAllocation
. للحصول على نتيجة نواة الاختزال، يجب استخدام الطريقةjavaFutureType.get()
. تكون الطريقتان "copy" وget()
متزامنتان. - حدِّد سياق RenderScript. يمكنك محو سياق RenderScript باستخدام
destroy()
أو من خلال السماح بجمع كائن سياق RenderScript. وهذا يتسبب في أي استخدام إضافي لأي شيء ينتمي إلى هذا السياق ينتج عنه استثناء.
نموذج التنفيذ غير المتزامن
الطرق المعروضة forEach
وinvoke
وreduce
وset
غير متزامنة، وقد يعود كل منها إلى Java قبل إكمال
الإجراء المطلوب. ومع ذلك، تُرسَل الإجراءات الفردية بالترتيب الذي تم إطلاقه به.
توفّر الفئة Allocation
طرق "النسخ" لنسخ البيانات من "التخصيصات" وإليها. تكون طريقة "النسخ" متزامنة، ويتم ترتيبها بشكل تسلسلي فيما يتعلق بأي من الإجراءات غير المتزامنة أعلاه التي تلمس التخصيص نفسه.
توفِّر فئات javaFutureType المعروضة طريقة get()
للحصول على نتيجة التخفيض. وتكون السمة get()
متزامنة، ويتم ترتيبها بشكل تسلسلي في ما يتعلق بالتقليل (وهو غير متزامن).
نص RenderScript أحادي المصدر
يقدّم Android 7.0 (المستوى 24 من واجهة برمجة التطبيقات) ميزة برمجة جديدة تُسمى RenderScript
بالمصدر الأحادي، حيث يتم تشغيل النواة من النص البرمجي حيث تم تحديدها، بدلاً من
من Java. يقتصر هذا النهج حاليًا على تخطيط النواة على الخريطة، والتي يُشار إليها ببساطة باسم "النواة"
في هذا القسم لتوفير الإيجاز. تتيح هذه الميزة الجديدة أيضًا إنشاء عمليات تخصيص من النوع
rs_allocation
من داخل النص البرمجي. أصبح من الممكن الآن تنفيذ خوارزمية كاملة داخل نص برمجي فقط، حتى إذا كانت هناك حاجة إلى إطلاق عدة نواة.
وهناك جانبان للفائدة، وهما: رمز أكثر قابلية للقراءة، لأنه يحافظ على تنفيذ خوارزمية بلغة واحدة، وربما يتم ترميزه بشكل أسرع بسبب قلة الانتقالات بين Java
وRenderScript عبر عمليات تشغيل النواة المتعددة.
في RenderScript أحادي المصدر، تكتب نواة كما هو موضّح في
كتابة نواة RenderScript. عليك بعد ذلك كتابة دالة قابلة للاستدعاء تستدعي
rsForEach()
لتشغيلها. تأخذ واجهة برمجة التطبيقات هذه دالة kernel كمعلمة أولى، تليها عمليات تخصيص المدخلات والمخرجات. تستخدم واجهة برمجة تطبيقات مشابهة
rsForEachWithOptions()
وسيطة إضافية من النوع
rs_script_call_t
، والتي تحدد مجموعة فرعية من العناصر من عمليات توزيع الإدخال
والمخرجات لمعالجتها دالة kernel.
لبدء احتساب RenderScript، عليك استدعاء الدالة القابلة للاستدعاء من Java.
اتّبِع الخطوات الواردة في استخدام RenderScript من Java Code.
في خطوة إطلاق النواة المناسبة، يمكنك استدعاء الدالة غير القابلة للاستدعاء باستخدام invoke_function_name()
، والتي ستبدأ العملية الحسابية بالكامل، بما في ذلك إطلاق النواة.
غالبًا ما تكون هناك حاجة إلى التخصيصات لحفظ النتائج المتوسطة
من إطلاق نواة إلى أخرى وتمريرها. يمكنك إنشاؤها باستخدام
rsCreateAllocation(). أحد أشكال واجهة برمجة التطبيقات هذه السهلة الاستخدام هو
rsCreateAllocation_<T><W>(…)
، حيث يكون T هو نوع البيانات لأحد العناصر، وW هو عرض المتجه للعنصر. تستخدم واجهة برمجة التطبيقات الأحجام بالأبعاد X وY وZ كوسيطات. بالنسبة إلى تخصيصات البُعد الواحد أو الثنائي الأبعاد،
يمكن حذف حجم البُعد "ص" أو "ع". على سبيل المثال، تنشئ rsCreateAllocation_uchar4(16384)
عملية تخصيص أحادية الأبعاد لـ 16384 عنصرًا، يكون كل عنصر منها من النوع uchar4
.
يدير النظام عمليات التخصيص تلقائيًا. وليس عليك إصدار هذه الرسائل أو إتاحتها بشكل صريح. مع ذلك، يمكنك طلب الرمز
rsClearObject(rs_allocation* alloc)
للإشارة إلى أنّك لم تعُد بحاجة إلى الاسم المعرِّف
alloc
في التخصيص الأساسي،
كي يتمكّن النظام من إخلاء الموارد في أقرب وقت ممكن.
يحتوي القسم كتابة Kernel في 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 نفسها. بدون استخدام RenderScript أحادي المصدر، سيكون عليك إطلاق نواة الكيرنل (النواة) من رمز Java، وفصل عمليات تشغيل النواة عن تعريفات النواة (kernel) وجعل فهم الخوارزمية بأكملها أكثر صعوبة. لا يقتصر الأمر على تسهيل قراءة رمز RenderScript أحادي المصدر، كما يقلل أيضًا من الانتقال بين Java والنص البرمجي عبر عمليات تشغيل النواة. قد تُطلق بعض الخوارزميات التكرارية وحدات نواة مئات المرات، ما يجعل العمل على هذه العملية الانتقالية كبيرًا.
نص برمجي عالمي
النص البرمجي العمومي هو متغيّر عمومي عادي غير static
في ملف نص برمجي (.rs
). بالنسبة إلى نص برمجي عالمي يحمل الاسم var تم تحديده في الملف filename.rs
، ستظهر الطريقة 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 (المستوى 24 لواجهة برمجة التطبيقات) والإصدارات الأحدث، تتوافق RenderScript مع نواة التقليل للسماح بخوارزميات التقليل الفعّالة التي يكتبها المستخدم. يمكنك إطلاق نواة التقليل في المدخلات ذات الأبعاد 1 أو 2 أو 3.
يوضِّح أحد الأمثلة أعلاه نواة تخفيض بسيطة للإضافة.
في ما يلي نواة اختزال 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
)، ولكن يمكنك كتابة دالة مهيئ لتنفيذ إجراء مختلف.
مثال: في النواة الإضافية، تُستخدم عناصر بيانات المركم (من النوع int
) لإضافة قيم الإدخال. ولا تتوفّر دالة مهيأة، وبالتالي يتم إعداد كل عنصر من عناصر بيانات المركم إلى الصفر.
مثال: في النواة findMinAndMax، تُستخدم عناصر بيانات المركم
(من النوع MinAndMax
) لتتبُّع القيم الدنيا والحد الأقصى
التي تم العثور عليها حتى الآن. تتوفر دالة إعداد لضبط هذه القيم على LONG_MAX
وLONG_MIN
، على التوالي، ولضبط مواقع هذه القيم على -1، ما يشير إلى أنّ القيم ليست موجودة فعليًا في الجزء (الفارغ) من الإدخال الذي تمت
معالجته.
يستدعي RenderScript وظيفة المركم مرة واحدة لكل إحداثي في الإدخالات. في العادة، يجب أن تعدّل الدالة عنصر بيانات المركم بطريقة ما وفقًا للإدخال.
مثال: في النواة addint، تضيف دالة المركم قيمة "عنصر إدخال" إلى عنصر بيانات المركم.
مثال: في النواة findMinAndMax، تتحقّق دالة المركم لمعرفة ما إذا كانت قيمة عنصر الإدخال أقل من أو تساوي الحدّ الأدنى للقيمة المسجَّلة في عنصر بيانات المركم و/أو أكبر من أو تساوي الحدّ الأقصى للقيمة المسجَّلة في عنصر بيانات المركم، ثم تعدّل عنصر بيانات المركم.
بعد استدعاء دالة المركم مرة واحدة لكل إحداثي في الإدخالات، يجب أن يدمج RenderScript عناصر بيانات المركم معًا في عنصر بيانات واحد لتجميع البيانات. يمكنك كتابة دالة مجمّعة للقيام بذلك. إذا كانت دالة المركم تحتوي على إدخال واحد وبدون أي وسيطات خاصة، لن تحتاج إلى كتابة دالة دمج، حيث سيستخدم RenderScript دالة المركم لدمج عناصر بيانات المركم. (لا يزال بإمكانك كتابة دالة الدمج إذا لم يكن هذا السلوك الافتراضي ما تريده).
مثال: في النواة addint، لا تتوفّر دالة الدمج، لذا سيتم استخدام دالة المركم. هذا هو السلوك الصحيح، لأننا إذا قسمنا مجموعة من القيم إلى جزأين، وأضفنا القيم في هذين الجزأين على حدة، فإن جمع هذين المجامين يكون نفس الشيء مثل جمع المجموعة بأكملها.
مثال: في النواة findMinAndMax، تتحقّق دالة الدمج لمعرفة ما إذا كان الحدّ الأدنى للقيمة المسجّلة في عنصر بيانات المركم "المصدر" *val
أقل من الحدّ الأدنى للقيمة المسجّلة في عنصر بيانات المركم "الوجهة"، ويتم تعديلها *accum
وفقًا لذلك.*accum
وهي تقوم بعمل مماثل للقيمة القصوى. يؤدي هذا إلى تعديل *accum
إلى الحالة التي كان من الممكن الحصول عليها إذا تم تجميع جميع قيم الإدخال في
*accum
بدلاً من تجميع بعضها في *accum
والبعض الآخر في
*val
.
بعد دمج جميع عناصر بيانات المركم، يحدِّد RenderScript نتيجة التقليل للرجوع إلى Java. يمكنك كتابة دالة تحويل خارجي للقيام بذلك. ولا تحتاج إلى كتابة دالة outconversion إذا كنت تريد أن تكون القيمة النهائية لعناصر بيانات المركم المُدمجة هي نتيجة التخفيض.
مثال: في النواة addint، لا تتوفّر دالة outconversion. القيمة النهائية لعناصر البيانات المجمّعة هي مجموع كل عناصر الإدخال، وهي القيمة التي نريد عرضها.
مثال: في النواة 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
هي وسيطة واحدة أو أكثر يتم ملؤها تلقائيًا استنادًا إلى المدخلات التي تم تمريرها إلى تشغيل kernel، ووسيطة واحدة لكل إدخال. قد تستخدم دالة المركم أيًّا من الوسيطات الخاصة بشكل اختياري.مثال على النواة التي تتضمّن إدخالات متعددة هي
dotProduct
.combiner(combinerName)
(اختياري): تحدد اسم دالة الدمج الخاصة بنواة التقليل هذه. بعد أن يستدعي RenderScript دالة المركم مرة واحدة لكل إحداثيات في المدخلات، فإنه يطلب هذه الدالة عدة مرات حسب الضرورة لدمج كل عناصر بيانات المركم في عنصر بيانات واحد. يجب تحديد الدالة على النحو التالي:
static void combinerName(accumType *accum, const accumType *other) { … }
ويشير
accum
إلى عنصر بيانات مجمّع "الوجهة" الذي تريد تعديله. يشيرother
إلى عنصر بيانات أحد المُراكم "المصدر" لهذه الدالة بهدف "دمجه" في*accum
.ملاحظة: من المحتمل أن يكون قد تم إعداد
*accum
أو*other
أو كليهما ولكن لم يتم تمريرهما مطلقًا إلى دالة المركم، وهذا يعني أنّه لم يسبق تعديل أحدهما أو كليهما وفقًا لأي بيانات إدخال. على سبيل المثال، في النواة findMinAndMax، تبحث دالة الدمجfMMCombiner
بشكل صريح عنidx < 0
لأنّها تشير إلى عنصر بيانات المركم الذي تكون قيمته INITVAL.إذا لم توفّر دالة الدمج، سيستخدم RenderScript وظيفة المركم بدلاً منها، ويتصرف كما لو كانت هناك دالة دمج تبدو كما يلي:
static void combinerName(accumType *accum, const accumType *other) { accumulatorName(accum, *other); }
تكون دالة الدمج إلزامية إذا كانت النواة تحتوي على أكثر من إدخال واحد أو إذا كان نوع بيانات الإدخال غير مطابق لنوع بيانات المركم أو إذا كانت دالة المركم تستقبل وسيطة خاصة واحدة أو أكثر.
outconverter(outconverterName)
(اختياري): تحدّد اسم دالة التحويل الخارجي لنواة الخفض هذه. وبعد أن يجمع RenderScript جميع عناصر بيانات المركم، فإنه يستدعي هذه الدالة لتحديد نتيجة الانخفاض والعودة إلى Java. يجب تحديد الدالة على النحو التالي:static void outconverterName(resultType *result, const accumType *accum) { … }
ويُشير
result
إلى عنصر بيانات نتيجة (تم تخصيصه ولكن لم يتم إعداده من خلال وقت تشغيل RenderScript) لهذه الدالة من أجل إعدادها مع نتيجة الخفض. وresultType هو نوع عنصر البيانات هذا، ويجب ألا يكون مطابقًا لنوع عنصر البيانات accumType.accum
هو مؤشر إلى عنصر بيانات المركم النهائي الذي حسبه دالة التجميع.إذا لم توفّر دالة خارجية محوّلة، ينسخ RenderScript عنصر بيانات المركم النهائي إلى عنصر بيانات النتيجة، ويتبع هذا الإجراء كما لو كانت هناك دالة موزّعة خارجية تبدو على النحو التالي:
static void outconverterName(accumType *result, const accumType *accum) { *result = *accum; }
إذا كنت تريد نوع نتائج مختلف عن نوع بيانات المركم، تكون دالة outconverter إلزامية.
يُرجى العلم أنّ النواة (kernel) تتضمّن أنواع إدخالات ونوع عنصر بيانات مراكم ونوع نتائج، ولا يلزم أن يكون أيًا منهما متطابقًا. على سبيل المثال، في النواة findMinAndMax، يختلف نوع الإدخال long
ونوع عنصر بيانات المركم MinAndMax
ونوع النتيجة int2
.
ما الذي لا يمكنك افتراضه؟
يجب عدم الاعتماد على عدد عناصر بيانات المركم التي أنشأتها RenderScript في عملية تشغيل kernel معيّنة. وما من ضمانة بأن يؤدي تشغيلان للنواة نفسها بالإدخالات نفسها إلى إنشاء العدد نفسه من عناصر بيانات المركم.
يجب عدم الاعتماد على الترتيب الذي يستدعي RenderScript دوال الإعداد والمراكم ودوال الدمج، بل قد يطلب بعضها بعضًا بشكل متوازٍ. وليس هناك ما يضمن تنفيذ عمليتَي إطلاق للنواة نفسها بالإدخال نفسه وفق الترتيب نفسه. الضمان الوحيد هو أنّ وظيفة أداة الإعداد هي الوحيدة التي يمكنها رؤية عنصر بيانات المركم غير المهيأ. على سبيل المثال:
- وما مِن ضمانة على إعداد جميع عناصر بيانات المركم قبل استدعاء دالة المركم، علمًا بأنّه سيتم طلبها فقط على عنصر بيانات تم إعداده في هذا المركم.
- وليس هناك ما يضمن ترتيب نقل عناصر الإدخال إلى دالة المركم.
- وليس هناك ما يضمن استدعاء دالة المركم لجميع عناصر الإدخال قبل استدعاء دالة الدمج.
نتيجةً لذلك، لن تكون النواة findMinAndMax حاسمة، إذ إذا كان الإدخال يحتوي على أكثر من موضع ورود واحد للقيمة الدنيا أو القصوى نفسها، لن يكون بإمكانك معرفة موضع الورود الذي ستعثر عليه النواة.
ما الذي يجب أن تضمنه؟
بما أنّ نظام RenderScript قد يختار تنفيذ نواة بعدة طرق مختلفة، عليك اتّباع قواعد معيّنة للتأكّد من أنّ النواة تعمل على النحو المطلوب. في حال عدم اتّباع هذه القواعد، قد تظهر لك نتائج غير صحيحة أو سلوك غير محدَّد أو أخطاء في وقت التشغيل.
تنص القواعد الواردة أدناه في أغلب الأحيان على أنّ عنصرَي بيانات المركم يجب أن يتضمّنا "القيمة نفسها". ما معنى ذلك؟ يعتمد ذلك على ما تريد أن تنفّذه النواة. بالنسبة إلى الاختزال الحسابي، مثل الدالة الإضافية، من المنطقي عادةً أن تشير كلمة "نفس" إلى المساواة الرياضية. عند إجراء عملية بحث عن "اختيار أي" مثل findMinAndMax ("العثور على موقع القيم الدنيا والقصوى للإدخال") حيث قد يكون هناك أكثر من موضع واحد لقيم إدخال متطابقة، يجب اعتبار جميع المواقع الجغرافية لأي قيمة إدخال على "الحالة نفسها". يمكنك كتابة نواة مشابهة لـ "العثور على موقع القيم الدنيا والقصوى للإدخال للأقصى" حيث يُفضّل (على سبيل المثال) قيمة أدنى قيمة في الموقع 100 على قيمة أدنى متطابقة في الموقع 200. بالنسبة إلى هذه النواة، تعني كلمة "نفس" الموقع الجغرافي المتطابق، وليس القيمة المتطابقة فقط، ويجب أن تكون دالة المركم والدمج مختلفة عن دالتَي Max ودمجهما.
يجب أن تنشئ دالة الإعداد قيمة هوية. وهذا يعني أنّه إذا كانI
وA
هما عنصرَي بيانات مراكم تم إعدادهما من خلال دالة الإعداد، ولم يتم أبدًا تمرير السمة I
إلى دالة المركم (ولكن ربما تم نقل السمة A
)، يتم عندها:
مثال: في النواة الإضافة، يتم إعداد عنصر بيانات المُجمّع إلى صفر. تؤدي دالة الدمج الخاصة بالنواة هذه عملية الإضافة، والصفر هو قيمة الهوية للجمع.
مثال: في النواة 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
لم يتم إعداده، والوسيطات هي
قائمة وسيطات الإدخال والوسيطات الخاصة لاستدعاء معيّن لدالة المركم، ثم يجب ضبط تسلسلَي الرموز التاليين على A
accumulatorName(&A, args); // statement 1
initializerName(&B); // statement 2 accumulatorName(&B, args); // statement 3 combinerName(&A, &B); // statement 4
مثال: في النواة addint، لقيمة الإدخال V:
- العبارة 1 هي نفسها
A += V
- العبارة 2 هي نفسها
B = 0
- العبارة 3 هي نفسها
B += V
، والتي تماثلB = V
- العبارة 4 هي نفسها
A += B
، والتي تماثلA += V
تعين العبارتين 1 و4 A
على القيمة ذاتها، وبالتالي تمتثل هذه النواة لقاعدة الطي الأساسية.
مثال: في النواة findMinAndMax، لقيمة الإدخال V في الإحداثي X:
- العبارة 1 هي نفسها
A = minmax(A, IndexedVal(V, X))
- العبارة 2 هي نفسها
B = INITVAL
- العبارة 3 هي نفسها
B = minmax(B, IndexedVal(V, X))
التي، لأن B هي القيمة الأولية، وهي نفسهاB = IndexedVal(V, X)
- العبارة 4 هي نفسها
A = minmax(A, B)
والتي تتشابه فيA = minmax(A, IndexedVal(V, X))
تعين العبارتين 1 و4 A
على القيمة ذاتها، وبالتالي تمتثل هذه النواة لقاعدة الطي الأساسية.
استدعاء نواة اختزال من رمز Java
بالنسبة إلى نواة الاختزال المسماة kernelName والمحددة في الملف filename.rs
، تظهر ثلاث طرق تظهر في الفئة ScriptC_filename
:
Kotlin
// Function 1 fun reduce_kernelName(ain1: Allocation, …, ainN: Allocation): javaFutureType // Function 2 fun reduce_kernelName(ain1: Allocation, …, ainN: Allocation, sc: Script.LaunchOptions): javaFutureType // Function 3 fun reduce_kernelName(in1: Array<devecSiIn1Type>, …, inN: Array<devecSiInNType>): javaFutureType
Java
// Method 1 public javaFutureType reduce_kernelName(Allocation ain1, …, Allocation ainN); // Method 2 public javaFutureType reduce_kernelName(Allocation ain1, …, Allocation ainN, Script.LaunchOptions sc); // Method 3 public javaFutureType reduce_kernelName(devecSiIn1Type[] in1, …, devecSiInNType[] inN);
في ما يلي بعض الأمثلة على طلب النواة المكوّن الإضافي:
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
إدخال واحدة لكل وسيطة إدخال في دالة المركم في kernel. يتحقّق وقت تشغيل RenderScript من التأكّد من أنّ جميع عمليات تخصيص الإدخال
لها الأبعاد نفسها وأنّ النوع Element
لكل من عمليات تخصيص الإدخال يتطابق مع نوع وسيطة الإدخال
المقابلة للنموذج الأوّلي لدالة المركم. في حال تعذّر اجتياز أي من عمليات الفحص هذه، يعرض RenderScript استثناءً. ويتم تنفيذ النواة على كل إحداثي في تلك الأبعاد.
الطريقة 2 هي نفسها الطريقة 1، إلا أنّ الطريقة الثانية تستخدم وسيطة إضافية sc
يمكن استخدامها لتقييد تنفيذ النواة على مجموعة فرعية من الإحداثيات.
الطريقة 3 هي نفسها الطريقة 1، إلا أنّه بدلاً من أخذ مدخلات التخصيص، يتم استخدام إدخالات صفيف من Java. وهو ما يسهّل عليك كتابة رمز برمجي لإنشاء تخصيص بشكل صريح ونسخ البيانات إليه من مصفوفة Java. مع ذلك، لن يؤدي استخدام الطريقة الثالثة بدلاً من الطريقة 1 إلى زيادة
أداء الرمز. بالنسبة إلى كل صفيف إدخال، تنشئ الطريقة الثالثة عملية تخصيص مؤقتة أحادية البُعد مع تفعيل النوع Element
وsetAutoPadding(boolean)
المناسبين، وتنسخ المصفوفة إلى التخصيص كما لو تم ذلك باستخدام طريقة copyFrom()
المناسبة في 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]
، تكون javaResultTypeint
أوInt2
أوint[]
. يمكن تمثيل جميع قيم resultType من خلال javaResultType. - إذا كانت قيمة resultType هي
uint
أوuint2
أوuint[15]
، تكون javaResultType هيlong
أوLong2
أوlong[]
. يمكن تمثيل جميع قيم resultType من خلال javaResultType. - إذا كانت قيمة resultType هي
ulong
أوulong2
أوulong[15]
، تكون javaResultTypelong
أو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، وأدت kernel التقليل إلى قيمة غير قابلة للتمثيل، تُطرح javaFutureType.get()
استثناءً.
الطريقة 3 وdevecSiInXType
devecSiInXType هو نوع Java المقابل للنوع inXType للوسيطة المقابلة في دالة المركم. ما لم يكن inXType نوعًا غير موقَّع أو نوع متّجه، يكون devecSiInXType هو نوع Java المقابل مباشرةً. إذا كان inXType عبارة عن نوع عددي غير موقَّع، يكون devecSiInXType هو نوع Java الذي يتوافق مباشرةً مع النوع القياسي الموقَّع من النوع نفسه. إذا كان inXType هو نوع متّجه مُوقَّع، تكون قيمة devecSiInXType هي نوع Java الذي يتوافق مباشرةً مع نوع مكوّن المتّجه. إذا كان inXType نوع متّجه غير موقَّع، يكون devecSiInXType هو نوع Java الذي يتوافق مباشرةً مع النوع العددي الموقَّع والذي له حجم نوع مكوّن المتّجه نفسه. على سبيل المثال:
- إذا كانت قيمة inXType هي
int
، تكون قيمة devecSiInXTypeint
. - إذا كانت قيمة inXType هي
int2
، تكون قيمة devecSiInXTypeint
. المصفوفة هي تمثيل مسطّح: يتضمّن ضعف عدد العناصر العددية التي تتضمّن عناصر متّجه مكوّنة من مكوّنَين. وهذه هي الطريقة نفسها التي تعمل بها طُرق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 كيفية استخدام واجهات برمجة التطبيقات الواردة في هذه الصفحة.