RenderScript هو إطار عمل لتشغيل المهام التي تتطلّب الكثير من العمليات الحسابية بأداء عالٍ على Android. تم تصميم RenderScript بشكل أساسي للاستخدام مع العمليات الحسابية الموازية للبيانات، على الرغم من أنّه يمكن أن تستفيد أيضًا المهام المتعلّقة بالمعالجة التسلسلية. تعمل بيئة تشغيل RenderScript على تنفيذ المهام بشكل موازٍ على جميع المعالجات المتاحة على الجهاز، مثل وحدات المعالجة المركزية ووحدات معالجة الرسومات متعددة النوى. يتيح لك ذلك التركيز على التعبير عن الخوارزميات بدلاً من جدولة العمل. يكون RenderScript مفيداً بشكل خاص للتطبيقات التي تُجري معالجة الصور أو التصوير الحاسوبي أو الرؤية الحاسوبية.
للبدء باستخدام RenderScript، هناك مفهومان رئيسيان يجب فهمهما:
- اللغة نفسها هي لغة مشتقة من C99 لكتابة رمز حسابي عالي الأداء. يصف مقالة كتابة نواة RenderScript كيفية استخدامها لكتابة نوى العمليات الحسابية.
- تُستخدَم واجهة برمجة التطبيقات للتحكّم لإدارة مدة استخدام موارد RenderScript و التحكّم في تنفيذ النواة. وتتوفّر بثلاث لغات مختلفة: Java وC++ في حزمة تطوير البرامج (NDK) لنظام التشغيل Android ولغة النواة المستندة إلى C99 نفسها. يوضّح المقالان استخدام RenderScript من رمز Java وRenderScript من مصدر واحد الخيارَين الأول والثالث، على التوالي.
كتابة نواة RenderScript
عادةً ما يكون نواة RenderScript في ملف .rs
في الدليل
<project_root>/src/rs
، ويُطلق على كل ملف .rs
اسم
نص. يحتوي كل نص برمجي على مجموعة من النوى والدوال والمتغيّرات الخاصة به. يمكن أن يحتوي النص البرمجي على:
- بيان pragma (
#pragma version(1)
) يعرِض إصدار لغة ملف kernel لتطبيق RenderScript المستخدَم في هذا النص البرمجي القيمة 1 هي القيمة الصالحة الوحيدة حاليًا. - بيان pragma (
#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
، مع تنفيذ واحد لدالة النواة لكلElement
فيAllocation
.قد يحتوي نواة الربط على إدخال واحد أو أكثر
Allocations
أو ناتج واحدAllocation
أو كليهما. تُجري معالجة RenderScript أثناء التشغيل عمليات تحقّق للتأكّد من أنّ جميع عمليات تخصيص الإدخال والإخراج لها سمات متطابقة، وأنّ أنواعElement
عمليات تخصيص الإدخال والإخراج تتطابق مع النموذج الأولي للنواة. وفي حال تعذّر إجراء أيّ من عمليات التحقّق هذه، تُرسِل معالجة RenderScript استثناءً.ملاحظة: قبل الإصدار 6.0 من نظام التشغيل Android (المستوى 23 من واجهة برمجة التطبيقات)، قد لا يحتوي نواة الربط على أكثر من إدخال واحد
Allocation
.إذا كنت بحاجة إلى المزيد من
Allocations
الإدخال أو الإخراج مقارنةً بما يتيحهrs_allocation
، يجب ربط هذه العناصر بعناصر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
في هذا المثال) وأسماء الدوال وأدوارها التي تشكّل النواة (دالةaccumulator
addintAccum
في هذا المثال ). يجب أن تكون جميع هذه الدوالّstatic
. تتطلّب نواة الاختزال دائمًا دالةaccumulator
، وقد تحتوي أيضًا على دوال أخرى، استنادًا إلى ما تريد أن تُجريه النواة.يجب أن تعرِض دالة تراكم نواة الاختزال القيمة
void
وأن تحتوي على مَعلمتَين على الأقل. الوسيطة الأولى (accum
في هذا المثال) هي مؤشر إلى عنصر بيانات المُجمِّع، ويتم تلقائيًا ملء الوسيطة الثانية (val
في هذا المثال) استنادًا إلى الإدخالAllocation
الذي تم تمريره إلى بدء تشغيل النواة. يتم إنشاء عنصر بيانات المُجمِّع من خلال وقت تشغيل RenderScript، ويتم بدءه تلقائيًا على القيمة صفر. يتمّ تشغيل هذا البرنامج الأساسي تلقائيًا على جميع مدخلاتهAllocation
، مع تنفيذ الدالة المُجمّعة مرّة واحدة لكلElement
فيAllocation
. يتم تلقائيًا التعامل مع القيمة النهائية لعنصر بيانات المُجمِّع على أنّها نتيجة الطرح، ويتم عرضها في Java. يتحقّق وقت تشغيل RenderScript من أنّ نوعElement
لـ Allocation الخاص بالمدخلات يتطابق مع ملف prototype لدالة المُجمِّع. وفي حال عدم التطابق، يُرسِل RenderScript استثناءً.يحتوي نواة التخفيض على إدخال واحد أو أكثر
Allocations
ولكن ليس لديه ناتجAllocations
.يمكنك الاطّلاع على مزيد من التفاصيل حول نوى الاختزال هنا.
تتوفّر نوى المعالجة المنخفضة في Android 7.0 (المستوى 24 من واجهة برمجة التطبيقات) والإصدارات الأحدث.
يمكن أن تصل وظيفة نواة الربط أو وظيفة تراكم نواة الاختزال إلى إحداثيات التنفيذ الحالي باستخدام الوسيطات الخاصة
x
وy
وz
، والتي يجب أن تكون من النوعint
أوuint32_t
. هذه الوسيطات اختيارية.يمكن أن تأخذ دالة نواة الربط أو دالة تراكم نواة الاختزال دالة الوسيطة الخاصة الاختيارية
context
من النوع rs_kernel_context. ويحتاج إليه عدد من واجهات برمجة التطبيقات لوقت التشغيل التي تُستخدَم لطلب خصائص معيّنة للتنفيذ الحالي، مثل rsGetDimX. (تتوفّر الوسيطةcontext
في Android 6.0 (المستوى 23 من واجهة برمجة التطبيقات) والإصدارات الأحدث.)- دالة
init()
اختيارية دالةinit()
هي نوع خاص من الدوال التي يمكن استدعاؤها والتي يشغّلها RenderScript عند إنشاء مثيل للنص البرمجي لأول مرة. ويسمح ذلك بإجراء بعض العمليات الحسابية تلقائيًا عند إنشاء النص البرمجي. - صفر أو أكثر من الدوال والنصوص البرمجية الثابتة إنّ المتغيّر الثابت العام للنص البرمجي يعادل
المتغيّر العام للنص البرمجي باستثناء أنّه لا يمكن الوصول إليه من رمز Java. الدالة الثابتة هي دالة C
عادية يمكن استدعاؤها من أي نواة أو دالة قابلة للاستدعاء في النص البرمجي، ولكن لا يتم عرضها
على Java API. إذا لم تكن هناك حاجة للوصول إلى دالة أو عنصر برمجي عام من نص برمجي من رمز 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 من خلال واجهات برمجة تطبيقات مكتبة الدعم.
- في حال استخدام واجهات برمجة تطبيقات مكتبة الدعم، ستحصل على حِزم APK أكبر (ربما بشكل كبير) مقارنةً بحال استخدام واجهات برمجة التطبيقات الأصلية (
android.renderscript
).
استخدام واجهات برمجة تطبيقات مكتبة دعم RenderScript
لاستخدام واجهات برمجة التطبيقات RenderScript في مكتبة الدعم، عليك ضبط بيئة التطوير لتتمكّن من الوصول إليها. يجب توفُّر أدوات حزمة تطوير البرامج (SDK) لنظام التشغيل Android التالية لاستخدام واجهات برمجة التطبيقات هذه:
- الإصدار 22.2 أو إصدار أحدث من أدوات حزمة تطوير البرامج (SDK) لنظام التشغيل Android
- الإصدار 18.1.0 أو إصدار أحدث من أدوات إنشاء حزمة تطوير البرامج (SDK) لنظام التشغيل Android
يُرجى العِلم أنّه اعتبارًا من الإصدار 24.0.0 من "أدوات إنشاء حِزم تطوير البرامج (SDK) لنظام التشغيل Android"، لم يعُد نظام التشغيل Android 2.2 (المستوى 8 من واجهة برمجة التطبيقات) متوافقًا.
يمكنك التحقّق من الإصدار المثبّت من هذه الأدوات وتحديثه في Android SDK Manager.
لاستخدام واجهات برمجة التطبيقات 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
: لتحديد أنّه يجب الرجوع إلى إصدار متوافق من رمز bytecode الذي تم إنشاؤه إذا كان الجهاز الذي يتم تشغيله عليه لا يتوافق مع الإصدار المستهدَف.
-
- افتح ملف
- في فئات تطبيقك التي تستخدم 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: هذه هي النصوص البرمجية التي يحدّدها المستخدم كما هو موضّح في مقالة كتابة نواة 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
، استخدِمset_threshold(int)
في Java. ولضبط متغيّرrs_allocation
باسمlookup
، استخدِمset_lookup(Allocation)
set_threshold(int)
في Java. طُرقset
غير متزامنة. - اطلق النوى والدوالّ القابلة للدعوة المناسبة.
تظهر طرق تشغيل نواة معيّنة في فئة
ScriptC_filename
نفسها باستخدام طرق باسمforEach_mappingKernelName()
أوreduce_reductionKernelName()
. تكون عمليات الإطلاق هذه غير متزامنة. استنادًا إلى الوسيطات التي يتم تمريرها إلى النواة، تأخذ المحاولة Allocation واحدة أو أكثر، ويجب أن تتضمّن جميعها السمات نفسها. يتم تنفيذ ملف التمويه تلقائيًا على كل إحداثي في هذه السمات. لتنفيذ ملف تمويه على مجموعة فرعية من هذه الإحداثيات، يجب تمرير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
طرق "النسخ" لنسخ البيانات من
وإلى "عمليات التوزيع". تكون طريقة "copy" متزامنة، ويتم تسلسلها بالنسبة إلى أي
من الإجراءات غير المتزامنة أعلاه التي تؤثر في عملية التوزيع نفسها.
توفّر فئات javaFutureType المُعاد تمثيلها
طريقة get()
للحصول على نتيجة عملية تقليل. get()
هو
متزامن، ويتم تسلسله بالنسبة إلى عملية التخفيض (التي تكون غير متزامنة).
RenderScript من مصدر واحد
يقدّم نظام Android 7.0 (المستوى 24 من واجهة برمجة التطبيقات) ميزة برمجة جديدة تُعرف باسم Single-Source
RenderScript، حيث يتم تشغيل النوى من النص البرمجي الذي تم تحديدها فيه، بدلاً من
Java. يقتصر هذا النهج حاليًا على نوى الربط، والتي يُشار إليها ببساطة باسم "النوى"
في هذا القسم لمزيد من الإيجاز. تتيح هذه الميزة الجديدة أيضًا إنشاء عمليات تخصيص من النوع
rs_allocation
من داخل النص البرمجي. أصبح من الممكن الآن
تنفيذ خوارزمية كاملة في نص برمجي فقط، حتى إذا كان مطلوبًا تشغيل نواة متعددة.
تعود هذه الميزة بالفائدة على نحو مزدوج: رمز برمجي أكثر سهولة في القراءة، لأنّه يحافظ على تنفيذ الخوارزمية بلغة واحدة، ورمز برمجي أسرع على الأرجح، بسبب انخفاض عدد عمليات النقل بين Java و
RenderScript على مستوى عمليات تشغيل نواة متعددة.
في Single-Source RenderScript، يمكنك كتابة نوى كما هو موضّح في
كتابة نواة RenderScript. بعد ذلك، يمكنك كتابة دالة قابلة للاستدعاء تستدعي دالة
rsForEach()
لتشغيلها. تأخذ واجهة برمجة التطبيقات هذه دالة نواة كمعلَمة
الأولى، متبوعة بتخصيصات الإدخال والإخراج. تأخذ واجهة برمجة التطبيقات المشابهة
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 مثالاً على ملف برمجي
يعكس صورة. يوسّع المثال أدناه ذلك لتطبيق أكثر من تأثير واحد على صورة،
باستخدام Single-Source RenderScript. ويتضمّن نواة أخرى، وهي greyscale
، التي تحوّل
صورة ملونة إلى صورة بالأبيض والأسود. بعد ذلك، تطبِّق دالة قابلة للاستدعاء process()
هذين النواةَين
بشكل متتالٍ على صورة الإدخال، وتُنشئ صورة ناتجة. يتمّ تمرير الكميات المخصّصة لكلّ من الإدخال
والإخراج كوسائط من النوع
rs_allocation
.
// File: singlesource.rs #pragma version(1) #pragma rs java_package_name(com.android.rssample) static const float4 weight = {0.299f, 0.587f, 0.114f, 0.0f}; uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) { uchar4 out = in; out.r = 255 - in.r; out.g = 255 - in.g; out.b = 255 - in.b; return out; } uchar4 RS_KERNEL greyscale(uchar4 in) { const float4 inF = rsUnpackColor8888(in); const float4 outF = (float4){ dot(inF, weight) }; return rsPackColorTo8888(outF); } void process(rs_allocation inputImage, rs_allocation outputImage) { const uint32_t imageWidth = rsAllocationGetDimX(inputImage); const uint32_t imageHeight = rsAllocationGetDimY(inputImage); rs_allocation tmp = rsCreateAllocation_uchar4(imageWidth, imageHeight); rsForEach(invert, inputImage, tmp); rsForEach(greyscale, tmp, outputImage); }
يمكنك طلب الدالة process()
من Java أو Kotlin على النحو التالي:
Kotlin
val RS: RenderScript = RenderScript.create(context) val script = ScriptC_singlesource(RS) val inputAllocation: Allocation = Allocation.createFromBitmapResource( RS, resources, R.drawable.image ) val outputAllocation: Allocation = Allocation.createTyped( RS, inputAllocation.type, Allocation.USAGE_SCRIPT or Allocation.USAGE_IO_OUTPUT ) script.invoke_process(inputAllocation, outputAllocation)
Java
// File SingleSource.java RenderScript RS = RenderScript.create(context); ScriptC_singlesource script = new ScriptC_singlesource(RS); Allocation inputAllocation = Allocation.createFromBitmapResource( RS, getResources(), R.drawable.image); Allocation outputAllocation = Allocation.createTyped( RS, inputAllocation.getType(), Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT); script.invoke_process(inputAllocation, outputAllocation);
يوضّح هذا المثال كيفية تنفيذ خوارزمية تتضمّن تشغيل نواة نظام التشغيل مرتين بالكامل في لغة RenderScript نفسها. بدون استخدام ميزة Single-Source RenderScript، عليك تشغيل كلا النواة من رمز Java، ما يؤدي إلى فصل عمليات تشغيل النواة عن تعريفات النواة ويصعّب فهم الخوارزمية بأكملها. إنّ رمز RenderScript المكوّن من مصدر واحد ليس أسهل في القراءة فحسب، بل يزيل أيضًا عملية الانتقال بين Java والنص البرمجي في عمليات تشغيل kernel. قد تُطلق بعض الخوارزميات التكرارية النوى مئات المرات، ما يجعل النفقات العامة لهذا الانتقال كبيرة.
المتغيّرات الشاملة للنص البرمجي
المتغيّر العام للنص البرمجي هو متغيّر عام عادي غير static
في ملف نص برمجي (.rs
). بالنسبة إلى نص برمجي
عام باسم var محدّد فيملف
filename.rs
، ستكون هناك get_var
طريقة تظهر في ScriptC_filename
الفئة. ما لم يكن المتغير العميق
هو const
، ستكون هناك أيضًا طريقة
set_var
.
يحتوي متغيّر script global معيّن على قيمتَين منفصلتَين، وهما قيمة Java وقيمة script. تتصرّف هذه القيم على النحو التالي:
- إذا كان المتغيّر var يحتوي على مُنشئ ثابت في النص البرمجي، فإنه يحدّد القيمة الأولية للمتغيّر var في كلّ من Java والنص البرمجي. وبخلاف ذلك، تكون هذه القيمة الأولية صفرًا.
- تؤدي عمليات الوصول إلى var ضمن النص البرمجي إلى قراءة قيمة النص البرمجي وكتابتها.
- تقرأ الطريقة
get_var
قيمة Java. - تُسجِّل طريقة
set_var
(في حال توفّرها) قيمة Java على الفور، وتُسجِّل قيمة النص البرمجي بشكل غير متزامن.
ملاحظة: يعني ذلك أنّ القيم المكتوبة في ملف نصي من علامة "التهيئة الثابتة" ضمن ملف نصي لا تكون مرئية لـ Java، باستثناء أي علامة تهيئة ثابتة في ملف نصي.
نوى الاختزال بالتفصيل
التصغير هو عملية دمج مجموعة من البيانات في قيمة واحدة. هذه عملية أساسية مفيدة في البرمجة الموازية، مع تطبيقات مثل التالية:
- احتساب مجموع أو حاصل ضرب جميع البيانات
- احتساب العمليات المنطقية (
and
وor
وxor
) على جميع البيانات - العثور على الحد الأدنى أو الأقصى للقيمة ضمن البيانات
- البحث عن قيمة معيّنة أو عن إحداثيات قيمة معيّنة ضمن البيانات
في الإصدار 7.0 من نظام Android (المستوى 24 لواجهة برمجة التطبيقات) والإصدارات الأحدث، يتيح RenderScript استخدام نواة التخفيض للسماح باستخدام خوارزميات التخفيض الفعّالة التي يكتبها المستخدم. يمكنك تشغيل نوى التقليل على مدخلات تحتوي على سمة واحدة أو سمتَين أو 3 سمات.
يعرض المثال أعلاه نواة تقليل addint بسيطة.
في ما يلي نواة تقليل findMinAndMax أكثر تعقيدًا
تجد مواضع الحد الأدنى والحد الأقصى لقيم long
في
Allocation
أحادي الأبعاد:
#define LONG_MAX (long)((1UL << 63) - 1) #define LONG_MIN (long)(1UL << 63) #pragma rs reduce(findMinAndMax) \ initializer(fMMInit) accumulator(fMMAccumulator) \ combiner(fMMCombiner) outconverter(fMMOutConverter) // Either a value and the location where it was found, or INITVAL. typedef struct { long val; int idx; // -1 indicates INITVAL } IndexedVal; typedef struct { IndexedVal min, max; } MinAndMax; // In discussion below, this initial value { { LONG_MAX, -1 }, { LONG_MIN, -1 } } // is called INITVAL. static void fMMInit(MinAndMax *accum) { accum->min.val = LONG_MAX; accum->min.idx = -1; accum->max.val = LONG_MIN; accum->max.idx = -1; } //---------------------------------------------------------------------- // In describing the behavior of the accumulator and combiner functions, // it is helpful to describe hypothetical functions // IndexedVal min(IndexedVal a, IndexedVal b) // IndexedVal max(IndexedVal a, IndexedVal b) // MinAndMax minmax(MinAndMax a, MinAndMax b) // MinAndMax minmax(MinAndMax accum, IndexedVal val) // // The effect of // IndexedVal min(IndexedVal a, IndexedVal b) // is to return the IndexedVal from among the two arguments // whose val is lesser, except that when an IndexedVal // has a negative index, that IndexedVal is never less than // any other IndexedVal; therefore, if exactly one of the // two arguments has a negative index, the min is the other // argument. Like ordinary arithmetic min and max, this function // is commutative and associative; that is, // // min(A, B) == min(B, A) // commutative // min(A, min(B, C)) == min((A, B), C) // associative // // The effect of // IndexedVal max(IndexedVal a, IndexedVal b) // is analogous (greater . . . never greater than). // // Then there is // // MinAndMax minmax(MinAndMax a, MinAndMax b) { // return MinAndMax(min(a.min, b.min), max(a.max, b.max)); // } // // Like ordinary arithmetic min and max, the above function // is commutative and associative; that is: // // minmax(A, B) == minmax(B, A) // commutative // minmax(A, minmax(B, C)) == minmax((A, B), C) // associative // // Finally define // // MinAndMax minmax(MinAndMax accum, IndexedVal val) { // return minmax(accum, MinAndMax(val, val)); // } //---------------------------------------------------------------------- // This function can be explained as doing: // *accum = minmax(*accum, IndexedVal(in, x)) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // *accum is INITVAL, then this function sets // *accum = IndexedVal(in, x) // // After this function is called, both accum->min.idx and accum->max.idx // will have nonnegative values: // - x is always nonnegative, so if this function ever sets one of the // idx fields, it will set it to a nonnegative value // - if one of the idx fields is negative, then the corresponding // val field must be LONG_MAX or LONG_MIN, so the function will always // set both the val and idx fields static void fMMAccumulator(MinAndMax *accum, long in, int x) { IndexedVal me; me.val = in; me.idx = x; if (me.val <= accum->min.val) accum->min = me; if (me.val >= accum->max.val) accum->max = me; } // This function can be explained as doing: // *accum = minmax(*accum, *val) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // one of the two accumulator data items is INITVAL, then this // function sets *accum to the other one. static void fMMCombiner(MinAndMax *accum, const MinAndMax *val) { if ((accum->min.idx < 0) || (val->min.val < accum->min.val)) accum->min = val->min; if ((accum->max.idx < 0) || (val->max.val > accum->max.val)) accum->max = val->max; } static void fMMOutConverter(int2 *result, const MinAndMax *val) { result->x = val->min.idx; result->y = val->max.idx; }
ملاحظة: يمكنك الاطّلاع على المزيد من الأمثلة على نوى تقليل البيانات هنا.
لتشغيل نواة تقليل، ينشئ وقت تشغيل RenderScript متغيّرًا واحدًا أو أكثر
يُعرف باسم عناصر data
المركم لحفظ حالة عملية التقليل. يختار وقت تشغيل RenderScript
عدد عناصر بيانات المُجمّع بطريقة تحقِّق أفضل أداء. يتم تحديد نوع
عناصر بيانات المُجمِّع (accumType) من خلال دالة
المُجمِّع في النواة، وتكون الوسيطة الأولى لهذه الدالة هي مؤشر إلى عنصر
بيانات المُجمِّع. يتمّ تلقائيًا ضبط كلّ عنصر من عناصر بيانات المُجمّع على القيمة صفر (كما لو كان
بواسطة memset
)، ولكن يمكنك كتابة دالة مُنشئ لإجراء عملية
مختلفة.
مثال: في نواة addint
، يتم استخدام عناصر بيانات المُجمِّع (من النوع int
) لجمع قيم
الإدخال. لا تتوفّر دالة إعداد، لذا يتم إعداد كل عنصر بيانات في المُجمِّع على القيمة
صفر.
مثال: في قلب findMinAndMax، يتم استخدام عناصر بيانات المُجمِّع (من النوع MinAndMax
) لتتبُّع الحد الأدنى والحد الأقصى للقيم التي تم العثور عليها حتى الآن. هناك دالة تمهيدية لضبط هذه القيم على LONG_MAX
و
LONG_MIN
على التوالي، ولضبط مواضع هذه القيم على -1، ما يشير إلى أنّه
لا تتوفّر القيم في الواقع في الجزء (الفارغ) من الإدخال الذي تمت
معالجته.
تستدعي RenderScript دالة المُجمِّع مرة واحدة لكل إحداثي في المدخلات. وعادةً ما تعمل الوظيفة على تعديل عنصر بيانات المُجمِّع بطريقة ما وفقًا للبيانات المُدخلة.
مثال: في نواة addint ، تضيف دالة المُجمِّع قيمة عنصر إدخال إلى عنصر بيانات المُجمِّع.
مثال: في ملف findMinAndMax، تتحقّق دالة المُجمِّع مما إذا كانت قيمة عنصر الإدخال أقل من أو تساوي الحد الأدنى للقيمة المسجّلة في عنصر بيانات المُجمِّع و/أو أكبر من أو تساوي الحد الأقصى للقيمة المسجّلة في عنصر بيانات المُجمِّع، وتُعدِّل عنصر بيانات المُجمِّع وفقًا لذلك.
بعد استدعاء دالة المُجمِّع مرة واحدة لكل إحداثي في الإدخالات، يجب أن تدمج أداة RenderScript عناصر بيانات المُجمِّع معًا في عنصر بيانات مجمِّع واحد. يمكنك كتابة دالة دمج لإجراء ذلك. إذا كانت دالة المُجمِّع تحتوي على إدخال واحد وليس لديها وسيطات خاصة، لن تحتاج إلى كتابة دالة جمع، وسيستخدم RenderScript دالة المُجمِّع لدمج عناصر data المُجمِّع. (يظل بإمكانك كتابة دالة تركيب إذا لم يكن هذا السلوك التلقائي هو ما تريده).
مثال: في نواة addint ، لا تتوفّر دالة لدمج القيم، لذا سيتم استخدام دالة المُجمِّع. وهذا هو السلوك الصحيح، لأنّه إذا قسمت مجموعة من القيم إلى قطعتَين وجمعت القيم في هاتين القطعتَين بشكل منفصل، فإنّ جمع هاتين القيمتَين هو نفسه جمع المجموعة بأكملها.
مثال: في قلب findMinAndMax، تتحقّق دالة التركيب مما إذا كانت القيمة الدنيا المسجّلة في *val
، عنصر بيانات المُجمِّع "المصدر"، أقل من القيمة الدنيا المسجّلة في *accum
، عنصر بيانات المُجمِّع "الوجهة"، وتُعدِّل *accum
وفقًا لذلك. ويؤدي هذا الإجراء عملًا مشابهًا للقيمة القصوى. يؤدي ذلك إلى تعديل *accum
إلى الحالة التي كانت ستتّخذها إذا تم تجميع جميع قيم الإدخال في
*accum
بدلاً من إدخال بعضها في *accum
وبعضها في
*val
.
بعد دمج جميع عناصر بيانات المُجمِّع، يحدِّد RenderScript نتيجة التخفيض لإعادتها إلى Java. يمكنك كتابة دالة 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)
(اختياري): يحدّد اسم دالة المُنشئ لوحدة معالجة الاختزال هذه. عند تشغيل kernel، يُطلِق RenderScript هذه الدالة مرة واحدة لكل عنصر بيانات المُجمِّع. يجب تعريف الدالة على النحو التالي:static void initializerName(accumType *accum) { … }
accum
هو مؤشر إلى عنصر بيانات مُجمِّع لكي تبدأ هذه الدالة في إعداده.إذا لم تقدِّم وظيفة تمهيدية، تبدأ RenderScript كل عنصر data في المُجمِّع بقيمة الصفر (كما لو كان من خلال
memset
)، وتتصرف كما لو كانت هناك وظيفة تمهيدية تبدو على النحو التالي:static void initializerName(accumType *accum) { memset(accum, 0, sizeof(*accum)); }
accumulator(accumulatorName)
(اختياري): تُحدِّد اسم دالة المُجمِّع لوحدة معالجة قاعدة التخفيض هذه. عند تشغيل kernel، يُطلِق RenderScript هذه الدالة مرة واحدة لكل إحداثي في الإدخالات، لتعديل عنصر بيانات المُجمِّع بطريقة ما وفقًا للإدخالات. يجب تعريف الدالة على النحو التالي:static void accumulatorName(accumType *accum, in1Type in1, …, inNType inN [, specialArguments]) { … }
accum
هو مؤشر إلى عنصر بيانات مُجمّع لكي تتمكّن هذه الدالة من تعديله.in1
إلىinN
هي وسيطة واحدة أو أكثر يتم ملؤها تلقائيًا استنادًا إلى المدخلات التي تم تمريرها إلى عملية تشغيل النواة، وسيطة واحدة لكل مدخل. يمكن أن تأخذ دالة المُجمِّع أيًا من المَعلمات الخاصة اختياريًا.مثال على نواة تتضمّن مدخلات متعددة هو
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)
(اختياري): تُحدِّد اسم دالة outconverter لوحدة معالجة التصغير هذه. بعد أن يجمع RenderScript كل عناصر data المُجمّعة، يستدعي هذه الدالة لتحديد نتيجة الطرح المُراد عرضها في 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 لإطلاق ملف kernel معيّن. لا يمكن ضمان أن يؤدي تشغيل نواة التشغيل نفسها مرتين باستخدام مدخلات البيانات نفسها إلى إنشاء العدد نفسه من عناصر بيانات المُجمِّع.
يجب عدم الاعتماد على الترتيب الذي يستدعي به RenderScript دوالّ الإعداد والتجميع والدمج، فقد يستدعي بعضًا منها بشكل موازٍ. ما مِن ضمان بأنّه عند تنفيذ عملية تشغيل اثنتين للنواة نفسها باستخدام الإدخال نفسه، سيتم اتّباع الترتيب نفسه. الضمان الوحيد هو أنّ دالة الإعداد هي فقط التي سترى عنصر بيانات مجمع لم يتم إعداده. مثلاً:
- لا يمكن ضمان بدء جميع عناصر بيانات المُجمِّع قبل استدعاء دالة المُجمِّع، إلا أنّه لن يتم استدعاؤها إلا على عنصر بيانات مُجمِّع مُبدَّأ.
- لا يمكن ضمان ترتيب تمرير عناصر الإدخال إلى الدالة المُجمِّعة.
- لا يمكن ضمان أنّه تمّ استدعاء دالة المُجمِّع لجميع عناصر الإدخال قبل استدعاء دالة المُجمِّع.
ومن النتائج المترتبة على ذلك أنّ نواة findMinAndMax ليست حتمية: إذا كان الإدخال يحتوي على أكثر من مرّة واحدة من الحد الأدنى أو الحد الأقصى نفسه، لا تتوفّر لك طريقة لمعرفة المرّة التي ستعثر فيها النواة على الحد الأدنى أو الحد الأقصى.
ما الذي يجب أن تضمنه؟
بما أنّ نظام RenderScript يمكنه اختيار تنفيذ نواة بطرق مختلفة، عليك اتّباع قواعد معيّنة لضمان سلوك النواة بالطريقة التي تريدها. في حال عدم اتّباع هذه القواعد، قد تحصل على نتائج غير صحيحة أو سلوك غير محدّد أو أخطاء وقت التشغيل.
غالبًا ما تشير القواعد أدناه إلى أنّه يجب أن يتضمّن عنصرَا بيانات المُجمّع "القيمة نفسها". ماذا يعني ذلك؟ يعتمد ذلك على ما تريد أن يفعله kernel. بالنسبة إلى عملية تقليل رياضية مثل 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، تضيف دالة combiner قيمتَي عنصرَي البيانات المُجمّعة:
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
أيضًا.
يجب أن تلتزم دالة المُجمّع ودالة المُجمِّع معًا بقاعدة folding
الأساسية. وهذا يعني أنّه إذا كان 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 = minmax(B, IndexedVal(V, X))
التي هي نفسهاB = IndexedVal(V, X)
لأنّ B هي القيمة الأولية. - العبارة 4 هي نفسها
A = minmax(A, B)
وهي نفسهاA = minmax(A, IndexedVal(V, X))
تضبط العبارة 1 والعبارة 4 القيمة A
على القيمة نفسها، وبالتالي تلتزم هذه النواة
بقاعدة الطي الأساسية.
استدعاء نواة تقليل من رمز Java
بالنسبة إلى نواة الاختزال التي تحمل الاسم kernelName والمُحدَّدة فيملفfilename.rs
، هناك ثلاث طرق تظهر في الفئةScriptC_filename
:
Kotlin
// Function 1 fun reduce_kernelName(ain1: Allocation, …, ainN: Allocation): javaFutureType // Function 2 fun reduce_kernelName(ain1: Allocation, …, ainN: Allocation, sc: Script.LaunchOptions): javaFutureType // Function 3 fun reduce_kernelName(in1: Array<devecSiIn1Type>, …, inN: Array<devecSiInNType>): javaFutureType
Java
// Method 1 public javaFutureType reduce_kernelName(Allocation ain1, …, Allocation ainN); // Method 2 public javaFutureType reduce_kernelName(Allocation ain1, …, Allocation ainN, Script.LaunchOptions sc); // Method 3 public javaFutureType reduce_kernelName(devecSiIn1Type[] in1, …, devecSiInNType[] inN);
في ما يلي بعض الأمثلة على استدعاء نواة addint:
Kotlin
val script = ScriptC_example(renderScript) // 1D array // and obtain answer immediately val input1 = intArrayOf(…) val sum1: Int = script.reduce_addint(input1).get() // Method 3 // 2D allocation // and do some additional work before obtaining answer val typeBuilder = Type.Builder(RS, Element.I32(RS)).apply { setX(…) setY(…) } val input2: Allocation = Allocation.createTyped(RS, typeBuilder.create()).also { populateSomehow(it) // fill in input Allocation with data } val result2: ScriptC_example.result_int = script.reduce_addint(input2) // Method 1 doSomeAdditionalWork() // might run at same time as reduction val sum2: Int = result2.get()
Java
ScriptC_example script = new ScriptC_example(renderScript); // 1D array // and obtain answer immediately int input1[] = …; int sum1 = script.reduce_addint(input1).get(); // Method 3 // 2D allocation // and do some additional work before obtaining answer Type.Builder typeBuilder = new Type.Builder(RS, Element.I32(RS)); typeBuilder.setX(…); typeBuilder.setY(…); Allocation input2 = createTyped(RS, typeBuilder.create()); populateSomehow(input2); // fill in input Allocation with data ScriptC_example.result_int result2 = script.reduce_addint(input2); // Method 1 doSomeAdditionalWork(); // might run at same time as reduction int sum2 = result2.get();
يحتوي الأسلوب 1 على وسيطة إدخال واحدة Allocation
لكل وسيطة إدخال في دالة المركم
في النواة. يتحقّق وقت تشغيل RenderScript من أنّ جميع عمليات تخصيص الإدخال
لها الأبعاد نفسها وأنّ نوع Element
لكلٍّ من
عمليات تخصيص الإدخال يتطابق مع نوع الوسيطة المقابلة للإدخال في النموذج الأولي للدالة
المُجمِّعة. في حال تعذّر إجراء أيٍّ من عمليات التحقّق هذه، يُرسِل RenderScript استثناءً. يتم تنفيذ ملف برمجي قلبي
على كل إحداثي في هذه الأبعاد.
الطريقة 2 هي نفسها الطريقة 1 باستثناء أنّ الطريقة 2 تأخذ وسيطة إضافية
sc
يمكن استخدامها للحد من تنفيذ النواة إلى مجموعة فرعية من
الإحداثيات.
الطريقة 3 هي نفسها الطريقة 1 باستثناء أنّه بدلاً من استخدام مدخلات "التخصيص"، يتم استخدام مدخلات مصفوفة Java. وهذا من الميزات المريحة التي تُجنّبك كتابة رمز لإنشاء عملية تخصيص صراحةً ونسخ البيانات إليها
من صفيف Java. ومع ذلك، لا يؤدي استخدام الطريقة 3 بدلاً من الطريقة 1 إلى تحسين
أداء الرمز البرمجي. لكل صفيف إدخال، تنشئ الطريقة 3 صفيف تخصيص مؤقتًا
أحادي الأبعاد مع نوع Element
المناسب
وsetAutoPadding(boolean)
مفعَّلَين، وتنسخ الصفيف إلى
التخصيص كما لو كان ذلك من خلال الطريقة copyFrom()
المناسبة من Allocation
. بعد ذلك، تستدعي الطريقة 1، مع تمرير عمليات التخصيص المؤقتة
هذه.
ملاحظة: إذا كان تطبيقك سيُجري عدة طلبات إلى النواة باستخدام الصفيف نفسه، أو باستخدام صفائف مختلفة من السمات ونوع العنصر نفسه، يمكنك تحسين الأداء من خلال إنشاء عمليات التوزيع وتعبئتها وإعادة استخدامها بنفسك بشكل صريح، بدلاً من استخدام الطريقة 3.
javaFutureType،
نوع الإرجاع لطرق التخفيض المُعادِلة، هو فئة مُعادِلة
ثابتة مُدمَجة ضمن فئة ScriptC_filename
. ويمثّل النتيجة المستقبلية لخفض
وقت تشغيل kernel. للحصول على النتيجة الفعلية للتنفيذ، يمكنك استدعاء 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، ونتج عن ملف تعريف
kernel للاختزال قيمة لا يمكن تمثيلها،
يُرسِل 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 استخدام واجهات برمجة التطبيقات التي يتم تناولها في هذه الصفحة.