نظرة عامة على RenderScript

‫RenderScript هو إطار عمل لتشغيل المهام التي تتطلّب الكثير من العمليات الحسابية بأداء عالٍ على Android. تم تصميم RenderScript بشكل أساسي للاستخدام مع العمليات الحسابية الموازية للبيانات، على الرغم من أنّه يمكن أن تستفيد أيضًا المهام المتعلّقة بالمعالجة التسلسلية. تعمل بيئة تشغيل RenderScript على تنفيذ المهام بشكل موازٍ على جميع المعالجات المتاحة على الجهاز، مثل وحدات المعالجة المركزية ووحدات معالجة الرسومات متعددة النوى. يتيح لك ذلك التركيز على التعبير عن الخوارزميات بدلاً من جدولة العمل. ويُعدّ RenderScript مفيدًا على وجه الخصوص للتطبيقات التي تُجري معالجة الصور أو التصوير الحاسوبي أو الرؤية الحاسوبية.

لبدء استخدام RenderScript، هناك مفهومان رئيسيان يجب فهمهما:

  • اللغة نفسها هي لغة مشتقة من C99 لكتابة رمز حسابي عالي الأداء. يصف مقالة كتابة نواة RenderScript كيفية استخدامها لكتابة نوى العمليات الحسابية.
  • تُستخدَم واجهة برمجة التطبيقات للتحكّم لإدارة مدة استخدام موارد RenderScript و التحكّم في تنفيذ النواة. وتتوفّر بثلاث لغات مختلفة: Java وC++ في Android NDK ولغة kernel المستندة إلى 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 الكامل، مع تنفيذ دالة kernel مرة واحدة لكل Element في Allocation.

      قد تحتوي نواة الربط على إدخال Allocations واحد أو أكثر أو إخراج واحد Allocation أو كليهما. تُجري معالجة ‎RenderScript أثناء التشغيل عمليات تحقّق للتأكّد من أنّ جميع عمليات تخصيص الإدخال والإخراج لها سمات ‎ متطابقة، وأنّ أنواع Element عمليات تخصيص الإدخال والإخراج تتطابق مع النموذج الأولي للنواة. وفي حال تعذّر إجراء أيّ من عمليات التحقّق هذه، تُرسِل معالجة ‎RenderScript استثناءً.

      ملاحظة: قبل الإصدار 6.0 من نظام التشغيل Android (المستوى 23 من واجهة برمجة التطبيقات)، قد لا يحتوي نواة الربط على أكثر من إدخال واحد Allocation.

      إذا كنت بحاجة إلى المزيد من الإدخالات أو النتائج من Allocations مقارنةً بالنواة، يجب ربط هذه الكائنات بالنصوص البرمجية rs_allocation العامة والوصول إليها من نواة أو دالة قابلة للاستعانة بها من خلال rsGetElementAt_type() أو rsSetElementAt_type().

      ملاحظة: RS_KERNEL هو دالة ماكرو تحدّدها RenderScript تلقائيًا لتسهيل الأمر عليك:

      #define RS_KERNEL __attribute__((kernel))

    نواة الاختزال هي مجموعة من الدوالّ التي تعمل على مجموعة من الإدخال Allocations ذات الأبعاد نفسها. وبشكلٍ تلقائي، يتم تنفيذ دالة المُجمِّع مرة واحدة لكل إحداثي في هذه السمات. ويُستخدَم عادةً (وليس حصريًا) "لتقليل" مجموعة من الإدخال Allocations إلى قيمة واحدة.

    • في ما يلي مثال على نواة تقليل بسيطة تضيف Elements من مدخلها:

      #pragma rs reduce(addint) accumulator(addintAccum)
      
      static void addintAccum(int *accum, int val) {
        *accum += val;
      }

      يتألّف نواة الاختزال من دالة واحدة أو أكثر يكتبها المستخدم. تُستخدم #pragma rs reduce لتعريف النواة من خلال تحديد اسمها (addint، في هذا المثال) وأسماء الدوال التي تتألف منها النواة وأدوارها (وهي دالة 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 عادية يمكن استدعاؤها من أي نواة أو دالة قابلة للاستدعاء في النص البرمجي، ولكن لا يتم عرضها على 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 - تتوفّر واجهات برمجة التطبيقات في حزمة الفئة هذه على الأجهزة التي تعمل بالإصدار 3.0 من نظام التشغيل Android (المستوى 11 لواجهة برمجة التطبيقات) والإصدارات الأحدث.
  • android.support.v8.renderscript: تتوفّر واجهات برمجة التطبيقات في هذه الحزمة من خلال مكتبة دعم، ما يتيح لك استخدامها على الأجهزة التي تعمل بالإصدار 2.3 من Android (المستوى 9 من واجهة برمجة التطبيقات) والإصدارات الأحدث.

في ما يلي المفاضلات:

  • في حال استخدام واجهات برمجة تطبيقات "مكتبة الدعم"، سيكون جزء RenderScript من تطبيقك متوافقًا مع الأجهزة التي تعمل بنظام التشغيل Android 2.3 (المستوى 9 من واجهة برمجة التطبيقات) والإصدارات الأحدث، بغض النظر عن ميزات RenderScript التي تستخدمها. يتيح ذلك لتطبيقك العمل على عدد أكبر من الأجهزة مقارنةً باستخدام واجهات برمجة التطبيقات الأصلية (android.renderscript).
  • لا تتوفّر بعض ميزات RenderScript من خلال واجهات برمجة تطبيقات مكتبة الدعم.
  • في حال استخدام واجهات برمجة تطبيقات مكتبة الدعم، ستحصل على حِزم APK أكبر (ربما بشكل كبير) مقارنةً بحال استخدام واجهات برمجة التطبيقات الأصلية (android.renderscript).

استخدام واجهات برمجة تطبيقات مكتبة دعم RenderScript

لاستخدام واجهات برمجة تطبيقات 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 من واجهة برمجة التطبيقات) متوافقًا.

يمكنك التحقّق من الإصدار المثبّت من هذه الأدوات وتحديثه في Android SDK Manager.

لاستخدام واجهات برمجة التطبيقات RenderScript في مكتبة الدعم:

  1. تأكَّد من تثبيت الإصدار المطلوب من حزمة تطوير البرامج (SDK) لنظام التشغيل Android.
  2. عدِّل إعدادات عملية إنشاء تطبيق 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 الذي تم إنشاؤه إذا كان الجهاز الذي يتم تشغيله عليه لا يتوافق مع الإصدار المستهدَف.
  3. في فئات تطبيقك التي تستخدم 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. تتبع معظم التطبيقات نمط الاستخدام الأساسي نفسه:

  1. ابدأ سياق RenderScript. يضمن سياق RenderScript، الذي تم إنشاؤه باستخدام create(Context)، إمكانية استخدام RenderScript ويقدّم عنصرًا للتحكّم في مدة صلاحية جميع عناصر RenderScript اللاحقة. يجب اعتبار عملية إنشاء السياق عملية قد تستغرق وقتًا طويلاً، لأنّها قد تنشئ موارد على مختلف أجزاء الأجهزة، ويجب ألا تكون في المسار الحرج للتطبيق إن أمكن ذلك. وعادةً ما يحتوي التطبيق على سياق RenderScript واحد فقط في كل مرة.
  2. أنشئ Allocation واحدة على الأقل ليتم تمريرها إلى النص البرمجي. Allocation هو عنصر RenderScript يقدّم مساحة تخزين لكمية ثابتة من البيانات. تستخدم النواة في النصوص البرمجية كائنات Allocation كمدخلات ومخرجات، ويمكن الوصول إلى كائنات Allocation في النواة باستخدام rsGetElementAt_type() وrsSetElementAt_type() عند ربطها ككائنات عامة للنص البرمجي. تسمح عناصر Allocation بتمرير المصفوفات من رمز Java إلى رمز RenderScript والعكس صحيح. يتم عادةً إنشاء عناصر Allocation باستخدام createTyped() أو createFromBitmap().
  3. أنشِئ النصوص البرمجية اللازمة. يتوفّر نوعان من النصوص البرمجية لإستخدامك عند استخدام 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.
  4. تعبئة "عمليات التوزيع" بالبيانات: باستثناء عمليات التوزيع التي تم إنشاؤها باستخدام createFromBitmap()، تتم تعبئة عملية التوزيع ببيانات فارغة عند إنشائها لأول مرة. لتعبئة عملية تخصيص، استخدِم إحدى طرق "النسخ" في Allocation. طرق "النسخ" متزامنة.
  5. اضبط أي متغيّرات عامة للنص البرمجي ضرورية. يمكنك ضبط المتغيّرات الشاملة باستخدام طرق في فئة ScriptC_filename نفسها التي تحمل الاسم set_globalname. على سبيل المثال، لضبط متغيّر int باسم threshold، استخدِم طريقة Java set_threshold(int)، ولضبط متغيّر rs_allocation باسم lookup، استخدِم طريقة Java set_lookup(Allocation). طريقة set غير متزامنة.
  6. ابدأ تشغيل النواة المناسبة والدوال القابلة للاستدعاء.

    تظهر طرق تشغيل نواة معيّنة في فئة ScriptC_filename نفسها باستخدام طرق باسم forEach_mappingKernelName() أو reduce_reductionKernelName(). تكون عمليات الإطلاق هذه غير متزامنة. اعتمادًا على وسيطات النواة، تتخذ الطريقة تخصيصًا واحدًا أو أكثر، ويجب أن يكون لكل منها الأبعاد نفسها. يتم تنفيذ ملف التمويه تلقائيًا على كل إحداثي في هذه السمات. لتنفيذ ملف تمويه على مجموعة فرعية من هذه الإحداثيات، يجب تمرير Script.LaunchOptions مناسب كوسيطة أخيرة لطريقة forEach أو reduce.

    ابدأ تشغيل الدوال القابلة للاستدعاء باستخدام طرق invoke_functionName المنعكسة في فئة ScriptC_filename نفسها. تكون عمليات الإطلاق هذه غير متزامنة.

  7. يمكنك استرداد البيانات من كائنات Allocation وكائنات javaFutureType. للوصول إلى البيانات من Allocation من رمز Java، يجب نسخ هذه البيانات مرة أخرى إلى Java باستخدام إحدى طرق "النسخ" في Allocation. للحصول على نتيجة نواة الاختزال، يجب استخدام الطريقة javaFutureType.get(). إنّ الطريقتَين "copy" وget() متزامنتَان.
  8. أزِل سياق RenderScript. يمكنك إتلاف سياق RenderScript باستخدام destroy() أو من خلال السماح بجمع المهملات لعنصر سياق RenderScript. يؤدي ذلك إلى طرح أي استثناء عند استخدام أي عنصر ينتمي إلى هذا السياق.

نموذج التنفيذ غير المتزامن

إنّ طُرق forEach وinvoke وreduce وset المُعاد توجيهها غير متزامنة، وقد تعود كلّ طريقة إلى Java قبل إكمال الإجراء المطلوب. ومع ذلك، يتم تسلسل الإجراءات الفردية بالترتيب الذي يتم إطلاقها به.

توفّر فئة Allocation طرق "النسخ" لنسخ البيانات من وإلى "عمليات التوزيع". تكون طريقة "copy" متزامنة، ويتم تسلسلها بالنسبة إلى أي من الإجراءات غير المتزامنة أعلاه التي تؤثر في عملية التوزيع نفسها.

توفّر فئات javaFutureType المُعاد تمثيلها طريقة get() للحصول على نتيجة عملية تقليل. get() هو متزامن، ويتم تسلسله بالنسبة إلى التخفيض (الذي يكون غير متزامن).

Single-Source RenderScript

يقدّم نظام Android 7.0 (المستوى 24 من واجهة برمجة التطبيقات) ميزة برمجة جديدة تُعرف باسم Single-Source RenderScript، حيث يتم تشغيل النوى من النص البرمجي الذي تم تحديدها فيه، بدلاً من Java. يقتصر هذا النهج حاليًا على نوى الربط، والتي يُشار إليها ببساطة باسم "النوى" في هذا القسم لمزيد من الإيجاز. تتيح هذه الميزة الجديدة أيضًا إنشاء عمليات تخصيص من النوع rs_allocation من داخل النص البرمجي. أصبح من الممكن الآن تنفيذ خوارزمية كاملة في نص برمجي فقط، حتى إذا كان مطلوبًا تشغيل نواة متعددة. تعود هذه الميزة بالفائدة على نحو مزدوج: رمز برمجي أكثر سهولة في القراءة، لأنّه يحافظ على تنفيذ الخوارزمية بلغة واحدة، ورمز برمجي أسرع على الأرجح، بسبب انخفاض عدد عمليات النقل بين Java و RenderScript على مستوى عمليات تشغيل نواة متعددة.

في Single-Source RenderScript، يمكنك كتابة نوى كما هو موضّح في كتابة نواة RenderScript. بعد ذلك، يمكنك كتابة دالة قابلة للاستدعاء تستدعي دالة rsForEach() لتشغيلها. تأخذ واجهة برمجة التطبيقات هذه دالة نواة كمعلَمة الأولى، متبوعة بتخصيصات الإدخال والإخراج. تستخدم واجهة برمجة تطبيقات مشابهة rsForEachWithOptions() وسيطة إضافية من النوع rs_script_call_t، والتي تحدد مجموعة فرعية من العناصر من عمليات توزيع الإدخال والمخرجات لمعالجتها دالة kernel.

لبدء احتساب 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 والنص البرمجي عبر عمليات تشغيل النواة. قد تُطلق بعض الخوارزميات التكرارية النوى مئات المرات، ما يجعل النفقات العامة لهذا الانتقال كبيرة.

نص برمجي عالمي

المتغيّر العام للنص البرمجي هو متغيّر عام عادي غير 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) على جميع البيانات
  • العثور على الحد الأدنى أو الأقصى للقيمة ضمن البيانات
  • البحث عن قيمة معيّنة أو عن إحداثيات قيمة معيّنة ضمن البيانات

في الإصدار Android 7.0 (المستوى 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 سيستخدم دالة المُجمِّع لدمج عناصر بيانات المُجمِّع. (يظل بإمكانك كتابة دالة تركيب إذا لم يكن هذا السلوك التلقائي هو ما تريده).

مثال: في نواة 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) (اختياري): تُحدِّد اسم دالة المُجمِّع لوحدة معالجة قاعدة التخفيض هذه. عند تشغيل النواة، يستدعي 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 جميع عناصر بيانات المركم، فإنه يستدعي هذه الدالة لتحديد نتيجة الانخفاض والعودة إلى Java. يجب تعريف الدالة على النحو التالي:

    static void outconverterName(resultType *result, const accumType *accum) {  }

    result هو مؤشر إلى عنصر بيانات نتيجة (تم تخصيصه ولكن لم يتم بدء تشغيله من خلال وقت تشغيل RenderScript) لتتم بدء تشغيل هذه الدالة بنتيجة التقليل. resultType هو نوع عنصر البيانات هذا، والذي لا يلزم أن يكون مماثلاً لنوع accumType. accum هو مؤشر إلى عنصر بيانات المركم النهائي الذي حسبه دالة التجميع.

    إذا لم تقدِّم دالة outconverter، ينسخ RenderScript عنصر بيانات المُجمِّع النهائي إلى عنصر بيانات النتيجة، ويتصرف كما لو كانت هناك دالة outconverter تشبه النحو التالي:

    static void outconverterName(accumType *result, const accumType *accum) {
      *result = *accum;
    }

    إذا كنت تريد نوع نتيجة مختلفًا عن نوع بيانات المُجمِّع، تكون دالة outconverter إلزامية.

تجدر الإشارة إلى أنّ النواة تحتوي على أنواع إدخال ونوع عنصر بيانات المُجمِّع ونوع النتيجة، ولا يلزم أن يكون أيّ منها متطابقًا. على سبيل المثال، في قلب findMinAndMax، يختلف نوع الدخل long ونوع عنصر بيانات المُجمِّعMinAndMax ونوع النتيجة int2.

ما هي الخطوات التي لا يمكنك اتّخاذها؟

يجب عدم الاعتماد على عدد عناصر بيانات المُجمِّع التي أنشأها RenderScript لإطلاق ملف تعريف برمجي معيّن. لا يمكن ضمان أن يؤدي تشغيل نواة التشغيل نفسها مرتين باستخدام مدخلات البيانات نفسها إلى إنشاء العدد نفسه من عناصر بيانات المُجمِّع.

يجب عدم الاعتماد على الترتيب الذي يستدعي RenderScript دوال الإعداد والمراكم ودوال الدمج، بل قد يطلب بعضها بعضًا بشكل متوازٍ. وليس هناك ما يضمن تنفيذ عمليتَي إطلاق للنواة نفسها بالإدخال نفسه وفق الترتيب نفسه. الضمان الوحيد هو أنّ دالة الإعداد هي فقط التي سترى عنصر بيانات مجمع لم يتم إعداده. مثلاً:

  • ما مِن ضمانة على إعداد جميع عناصر بيانات المركم قبل استدعاء دالة المركم، علمًا بأنّه سيتم طلبها فقط على عنصر بيانات تم إعداده في هذا المركم.
  • لا يمكن ضمان ترتيب تمرير عناصر الإدخال إلى الدالة المُجمِّعة.
  • لا يمكن ضمان أنّه تمّ استدعاء دالة المُجمِّع لجميع عناصر الإدخال قبل استدعاء دالة المُجمِّع.

ومن النتائج المترتبة على ذلك أنّ نواة findMinAndMax ليست حتمية: إذا كان الإدخال يحتوي على أكثر من مرّة واحدة من الحد الأدنى أو الحد الأقصى نفسه، لا تتوفّر لك طريقة لمعرفة المرّة التي ستعثر فيها النواة على الحد الأدنى أو الحد الأقصى.

ما الذي يجب أن تضمنه؟

بما أنّ نظام RenderScript يمكنه اختيار تنفيذ نواة بطرق مختلفة، عليك اتّباع قواعد معيّنة لضمان سلوك النواة بالطريقة التي تريدها. في حال عدم اتّباع هذه القواعد، قد تحصل على نتائج غير صحيحة أو سلوك غير محدّد أو أخطاء وقت التشغيل.

غالبًا ما تشير القواعد أدناه إلى أنّه يجب أن يتضمّن عنصرَا بيانات المُجمّع "القيمة نفسها". ماذا يعني ذلك؟ يعتمد ذلك على ما تريد أن يفعله kernel. بالنسبة إلى عملية تقليل رياضية مثل addint، من المنطقي عادةً أن تعني "القيمة نفسها" المساواة الحسابية. بالنسبة إلى البحث من النوع "اختيار أيّ"، مثل findMinAndMax ("العثور على موضع الحد الأدنى والحد الأقصى لقيم الإدخال") حيث قد يكون هناك أكثر من مرّة لقيم إدخال متطابقة، يجب اعتبار جميع مواضع قيمة إدخال معيّنة "متطابقة". يمكنك كتابة نواة مشابهة لدالة "العثور على موضع الحد الأدنى والحد الأقصى لأقرب قيم الإدخال" حيث (على سبيل المثال) يُفضَّل استخدام الحد الأدنى للقيمة في الموضع 100 بدلاً من الحد الأدنى للقيمة نفسه في الموضع 200. بالنسبة إلى هذه النواة، سيعني "القيمة نفسها" الموقع نفسه، وليس القيمة نفسها فقط، ويجب أن تكون دالتَا المُجمِّع والمُجمِّع مختلفتَين عن دالتَي findMinAndMax.

يجب أن تنشئ دالة الإعداد قيمة هوية. وهذا يعني أنّه إذا كان 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، تضيف دالة 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)
  • العبارة 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 لكل وسيطة إدخال في دالة المركم في النواة. يتحقّق وقت تشغيل RenderScript من أنّ جميع عمليات تخصيص الإدخال لها الأبعاد نفسها وأنّ نوع Element لكلٍّ من عمليات تخصيص الإدخال يتطابق مع نوع الوسيطة المقابلة للإدخال في النموذج الأولي للدالة المُجمِّعة. في حال تعذّر إجراء أيٍّ من عمليات التحقّق هذه، يُرسِل RenderScript استثناءً. ويتم تنفيذ النواة على كل إحداثي في تلك الأبعاد.

الطريقة 2 هي نفسها الطريقة 1 باستثناء أنّ الطريقة 2 تأخذ وسيطة إضافية sc يمكن استخدامها للحد من تنفيذ النواة إلى مجموعة فرعية من الإحداثيات.

الطريقة 3 هي نفسها الطريقة 1 باستثناء أنّه بدلاً من استخدام مدخلات "التخصيص"، يتم استخدام مدخلات مصفوفة Java. وهذا من الميزات المريحة التي تُجنّبك كتابة رمز لإنشاء عملية تخصيص صراحةً ونسخ البيانات إليها من صفيف Java. ومع ذلك، لا يؤدي استخدام الطريقة 3 بدلاً من الطريقة 1 إلى تحسين أداء الرمز البرمجي. بالنسبة إلى كل مصفوفة إدخال، تنشئ الطريقة الثالثة تخصيصاً مؤقتًا أحادي البُعد مع تفعيل النوع 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 كيفية استخدام واجهات برمجة التطبيقات الواردة في هذه الصفحة.