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

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

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

  • اللغة نفسها هي لغة مشتقة من C99 لكتابة رموز حوسبة عالية الأداء. توضّح كتابة Kernel في RenderScript طريقة استخدامها لكتابة نواة الحوسبة.
  • يتم استخدام control API لإدارة موارد RenderScript الدائمة والتحكّم في تنفيذ النواة. وهو متوفر بثلاث لغات مختلفة: Java، وC++ في Android NDK، ولغة kernel المستمدة من C99 نفسها. يصف استخدام RenderScript من رمز Java وRenderScript أحادي المصدر الخيارين الأول والثالث على التوالي.

كتابة نواة RenderScript

يوجد نواة RenderScript عادةً في ملف .rs في الدليل <project_root>/src/rs، ويُعرف كل ملف .rs باسم نص برمجي. يحتوي كل نص برمجي على مجموعة النواة والدوال والمتغيرات الخاصة به. يمكن أن يحتوي النص على:

  • يشير هذا المصطلح إلى بيان براغما (#pragma version(1)) يعرّف عن إصدار لغة النواة في RenderScript المستخدَم في هذا النص البرمجي. وفي الوقت الحالي، الرقم 1 هو القيمة الصالحة الوحيدة.
  • إعلان براغما (#pragma rs java_package_name(com.example.app)) يوضح اسم حزمة فئات Java المنعكسة من هذا النص البرمجي. يُرجى العِلم أنّ ملف .rs يجب أن يكون جزءًا من حزمة التطبيق، وليس في مشروع مكتبة.
  • لا يتم توفير أي دوال قابلة للاستدعاء أو أكثر. الدالة القابلة للاستدعاء هي دالة RenderScript أحادية سلاسل يمكنك استدعاؤها من رمز Java باستخدام وسيطات عشوائية. وهي غالبًا ما تكون مفيدة للإعداد الأولي أو العمليات الحسابية التسلسلية داخل مسار معالجة أكبر.
  • لا يتم إدخال أي عبارات عامة أو أكثر. يشبه النص البرمجي العمومي المتغير العمومي في C. يمكنك الوصول إلى المتغيرات الشاملة للنص البرمجي من رمز Java، وغالبًا ما تُستخدم هذه للانتقال إلى نواة RenderScript. يمكنك الاطّلاع على شرح مفصَّل للنصوص العامة هنا.

  • صفر أو أكثر من نواة الحوسبة. النواة الحوسبية هي دالة أو مجموعة دوال يمكنك توجيه وقت تشغيل RenderScript لتنفيذها بالتوازي على مستوى مجموعة من البيانات. هناك نوعان من النواة الحوسبية: نواة التعيين (تُسمّى أيضًا نواة لكلّ) ونواة الخفض.

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

    • في ما يلي مثال على نواة ربط بسيطة:

      uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) {
        uchar4 out = in;
        out.r = 255 - in.r;
        out.g = 255 - in.g;
        out.b = 255 - in.b;
        return out;
      }

      وفي معظم الجوانب، تطابق هذه الدالة مع الدالة C العادية. تحدّد السمة RS_KERNEL المطبّقة على دالة النموذج الأوّلي أنّ الدالة هي نواة ربط لـ RenderScript بدلاً من دالة قابلة للاستعانة بها. يتم ملء الوسيطة in تلقائيًا استنادًا إلى الإدخال Allocation الذي تم تمريره إلى تشغيل النواة. وتتم مناقشة الوسيطات x وy أدناه. تُكتب القيمة المعروضة من النواة تلقائيًا في المكان المناسب في الناتج Allocation. يتم تشغيل هذه النواة تلقائيًا على مستوى الإدخال Allocation الكامل، مع تنفيذ دالة kernel مرة واحدة لكل Element في Allocation.

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

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

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

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

      #define RS_KERNEL __attribute__((kernel))
      

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

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

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

      تتكوّن نواة الاختزال من دالة واحدة أو أكثر من الدوال المكتوبة من قِبل المستخدم. تُستخدم #pragma rs reduce لتعريف النواة من خلال تحديد اسمها (في هذا المثال addint) وأسماء الدوال التي تتألف منها النواة وأدوارها (وهي الدالة accumulator addintAccum في هذا المثال). يجب أن تكون جميع هذه الدوال static. تتطلب نواة الاختزال دائمًا دالة accumulator، وقد يكون لها أيضًا وظائف أخرى، بناءً على ما تريد أن تفعله النواة.

      يجب أن تعرض دالة مركم نواة التقليل void وأن تحتوي على وسيطتين على الأقل. الوسيطة الأولى (accum، في هذا المثال) هي مؤشر لعنصر بيانات المركم، والوسيطة الثانية (في هذا المثال: val) يتم ملؤها تلقائيًا استنادًا إلى الإدخال Allocation الذي تم تمريره إلى تشغيل النواة. ويتم إنشاء عنصر بيانات المركم من خلال وقت تشغيل RenderScript، ويتم إعداده تلقائيًا على صفر. يتم تشغيل هذه النواة تلقائيًا على مستوى الإدخال Allocation بالكامل، مع تنفيذ دالة المركم واحدة في كل Element في Allocation. وبشكلٍ تلقائي، يتم التعامل مع القيمة النهائية لعنصر بيانات المركم كنتيجة للخفض، ويتم إرجاعها إلى Java. يفحص وقت تشغيل RenderScript للتأكّد من تطابق النوع Element من تخصيص الإدخال مع النموذج الأوّلي لدالة المركم. وفي حال عدم التطابق، يقدّم RenderScript استثناءً.

      تحتوي نواة التقليل على إدخال Allocations واحد أو أكثر ولكن لا تحتوي على ناتج Allocations.

      يمكنك الاطّلاع على مزيد من التفاصيل هنا حول نواة تقليل الانحدار.

      تتوفّر نواات الانخفاض في الإصدار 7.0 (المستوى 24 من واجهة برمجة التطبيقات) والإصدارات الأحدث من نظام التشغيل Android.

    يمكن لدالة kernel للتعيين أو دالة مركم نواة الاختزال الوصول إلى إحداثيات التنفيذ الحالي باستخدام الوسيطات الخاصة x وy وz، والتي يجب أن تكون من النوع int أو uint32_t. هذه الوسيطات اختيارية.

    قد تستخدم أيضًا دالة kernel للتعيين أو دالة مركم نواة الاختزال الوسيطة الخاصة الاختيارية context من النوع rs_kernel_context. وهو مطلوب من قِبل مجموعة من واجهات برمجة التطبيقات لوقت التشغيل التي يتم استخدامها للاستعلام عن خصائص معيّنة لعملية التنفيذ الحالية، مثل rsGetDimX. (تتوفّر الوسيطة context في الإصدار Android 6.0 (المستوى 23 من واجهة برمجة التطبيقات) والإصدارات الأحدث.)

  • دالة init() اختيارية. الدالة init() هي نوع خاص من الدوال القابلة للاستدعاء التي يشغّلها RenderScript عند إنشاء مثيل للنص البرمجي لأول مرة. يسمح هذا بإجراء بعض العمليات الحوسبة تلقائيًا عند إنشاء البرنامج النصي.
  • ليس هناك أو أكثر من عمليات عمومية ودوال نصية ثابتة. يتساوى النص البرمجي الثابت العمومي مع نص برمجي عمومي باستثناء أنه لا يمكن الوصول إليه من رمز Java. الدالة الثابتة هي دالة C عادية يمكن استدعاؤها من أي دالة kernel أو دالة قابلة للاستدعاء في النص البرمجي، ولكنها غير معرَّضة لواجهة برمجة تطبيقات Java. إذا لم يكن هناك حاجة إلى الوصول إلى نص برمجي عام أو دالة من خلال رمز Java، ننصحك بشدة بتعريفه static.

ضبط دقة النقطة العائمة

يمكنك التحكّم في المستوى المطلوب من دقة النقطة العائمة في نص برمجي. يكون هذا مفيدًا إذا لم يكن المعيار الكامل IEEE 754-2008 (المستخدم افتراضيًا) مطلوبًا. يمكن للبراغا التالية تعيين مستوى مختلف من دقة النقطة العائمة:

  • #pragma rs_fp_full (الإعداد التلقائي في حال عدم تحديد أي شيء): للتطبيقات التي تتطلّب دقة النقطة العائمة على النحو الموضّح في معيار IEEE 754-2008.
  • #pragma rs_fp_relaxed: للتطبيقات التي لا تتطلب امتثالاً صارمًا للمعيار IEEE 754-2008 ويمكنها أن تقبل درجة دقة أقل. ويتيح هذا الوضع التدفق من التدفق إلى صفر للقيم الرقمية للعريضين والتقريب نحو الصفر.
  • #pragma rs_fp_imprecise: للتطبيقات التي لا تتطلب دقةً صارمة يتيح هذا الوضع تفعيل كل الميزات في rs_fp_relaxed بالإضافة إلى ما يلي:
    • يمكن أن تعرض العمليات التي تنتج عنها -0.0 +0.0 بدلاً من ذلك.
    • العمليات على INF وNAN غير محددة.

يمكن أن تستخدم معظم التطبيقات rs_fp_relaxed بدون أي آثار جانبية. وقد يكون ذلك مفيدًا جدًا في بعض البُنى الأساسية بسبب التحسينات الإضافية التي لا تتوفر إلا بدقة مريحة (مثل تعليمات وحدة المعالجة المركزية SIMD).

الوصول إلى واجهات برمجة تطبيقات RenderScript من Java

عند تطوير تطبيق Android يستخدم RenderScript، يمكنك الوصول إلى واجهة برمجة التطبيقات الخاصة به من Java بإحدى الطريقتين التاليتين:

  • android.renderscript - تتوفّر واجهات برمجة التطبيقات في حزمة الفئة هذه على الأجهزة التي تعمل بالإصدار Android 3.0 (المستوى 11 من واجهة برمجة التطبيقات) والإصدارات الأحدث.
  • android.support.v8.renderscript - تتوفر واجهات برمجة التطبيقات في هذه الحزمة من خلال مكتبة الدعم، التي تتيح لك استخدامها على الأجهزة التي تعمل بالإصدار 2.3 من نظام التشغيل Android (المستوى 9 من واجهة برمجة التطبيقات) والإصدارات الأحدث.

في ما يلي الإيجابيات والسلبيات:

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

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

لاستخدام واجهات برمجة تطبيقات Support Library RenderScript، يجب إعداد بيئة التطوير فيها حتى تتمكَّن من الوصول إليها. الأدوات التالية لحزمة تطوير البرامج (SDK) لنظام التشغيل Android مطلوبة لاستخدام واجهات برمجة التطبيقات هذه:

  • الإصدار 22.2 من أدوات حزمة تطوير البرامج (SDK) لنظام التشغيل Android أو إصدار أحدث
  • الإصدار 18.1.0 أو إصدار أحدث من أدوات إنشاء حزمة تطوير البرامج (SDK) لنظام التشغيل Android

يُرجى العلم أنّه بدءًا من الإصدار 24.0.0 من Android SDK Build-tools، لن يعود الإصدار Android 2.2 (المستوى 8 من واجهة برمجة التطبيقات) متاحًا.

يمكنك التحقّق من الإصدار المثبَّت من هذه الأدوات وتحديثه في إدارة حزمة تطوير البرامج (SDK) لنظام التشغيل Android.

لاستخدام واجهات برمجة تطبيقات Support Library RenderScript، اتّبِع الخطوات التالية:

  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 - لتحديد أنّ رمز البايت الذي تم إنشاؤه يجب أن يعود إلى إصدار متوافق إذا كان الجهاز الذي يتم تشغيله عليه لا يتوافق مع الإصدار المستهدف.
  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: هذه هي النصوص البرمجية التي يحددها المستخدم كما هو موضح في كتابة Kernel RenderScript أعلاه. يحتوي كل نص برمجي على فئة Java التي يعكسها المحول البرمجي لـ RenderScript من أجل تسهيل الوصول إلى النص البرمجي من رمز Java، وتحمل هذه الفئة اسم ScriptC_filename. على سبيل المثال، إذا كانت نواة الربط أعلاه موجودة في invert.rs وكان هناك سياق RenderScript في mRenderScript، سيكون رمز Java أو Kotlin لإنشاء مثيل للنص البرمجي على النحو التالي:

      Kotlin

      val invert = ScriptC_invert(renderScript)
      

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic: هي نواة RenderScript مدمجة من أجل العمليات الشائعة، مثل التمويه الغاوسي والالتفاف ودمج الصور. لمزيد من المعلومات، راجِع الفئات الفرعية لـ ScriptIntrinsic.
  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(). عمليات الإطلاق هذه غير متزامنة. اعتمادًا على وسيطات النواة، تتخذ الطريقة تخصيصًا واحدًا أو أكثر، ويجب أن يكون لكل منها الأبعاد نفسها. يتم بشكل تلقائي تنفيذ kernel على كل إحداثي في هذه الأبعاد. ولتنفيذ kernel على مجموعة فرعية من هذه الإحداثيات، يُرجى تمرير 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 طرق "النسخ" لنسخ البيانات من "التخصيصات" وإليها. تكون طريقة "النسخ" متزامنة، ويتم ترتيبها بشكل تسلسلي فيما يتعلق بأي من الإجراءات غير المتزامنة أعلاه التي تلمس التخصيص نفسه.

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

نص RenderScript أحادي المصدر

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

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

لبدء احتساب RenderScript، عليك استدعاء الدالة القابلة للاستدعاء من Java. اتّبِع الخطوات الواردة في استخدام RenderScript من Java Code. في خطوة إطلاق النواة المناسبة، يمكنك استدعاء الدالة غير القابلة للاستدعاء باستخدام invoke_function_name()، والتي ستبدأ العملية الحسابية بالكامل، بما في ذلك إطلاق النواة.

غالبًا ما تكون هناك حاجة إلى التخصيصات لحفظ النتائج المتوسطة من إطلاق نواة إلى أخرى وتمريرها. يمكنك إنشاؤها باستخدام rsCreateAllocation(). أحد أشكال واجهة برمجة التطبيقات هذه السهلة الاستخدام هو rsCreateAllocation_<T><W>(…)، حيث يكون T هو نوع البيانات لأحد العناصر، وW هو عرض المتجه للعنصر. تستخدم واجهة برمجة التطبيقات الأحجام بالأبعاد X وY وZ كوسيطات. بالنسبة إلى تخصيصات البُعد الواحد أو الثنائي الأبعاد، يمكن حذف حجم البُعد "ص" أو "ع". على سبيل المثال، تنشئ rsCreateAllocation_uchar4(16384) عملية تخصيص أحادية الأبعاد لـ 16384 عنصرًا، يكون كل عنصر منها من النوع uchar4.

يدير النظام عمليات التخصيص تلقائيًا. وليس عليك إصدار هذه الرسائل أو إتاحتها بشكل صريح. مع ذلك، يمكنك طلب الرمز rsClearObject(rs_allocation* alloc) للإشارة إلى أنّك لم تعُد بحاجة إلى الاسم المعرِّف alloc في التخصيص الأساسي، كي يتمكّن النظام من إخلاء الموارد في أقرب وقت ممكن.

يحتوي القسم كتابة Kernel في RenderScript على مثال لنواة تُقلب صورة. يوسّع المثال أدناه ذلك لتطبيق أكثر من تأثير على الصورة، باستخدام RenderScript أحادي المصدر. ويتضمّن نواة أخرى، وهي greyscale، التي تحوِّل الصورة الملونة إلى اللونَين الأسود والأبيض. بعد ذلك، تطبّق دالة process() قابلة للاستدعاء هاتين النواتين بشكل متتالٍ على صورة مُدخلة، وتُنتج صورة الناتج. يتم تمرير تخصيصات لكل من الإدخال والمخرجات كوسيطات من النوع rs_allocation.

// File: singlesource.rs

#pragma version(1)
#pragma rs java_package_name(com.android.rssample)

static const float4 weight = {0.299f, 0.587f, 0.114f, 0.0f};

uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) {
  uchar4 out = in;
  out.r = 255 - in.r;
  out.g = 255 - in.g;
  out.b = 255 - in.b;
  return out;
}

uchar4 RS_KERNEL greyscale(uchar4 in) {
  const float4 inF = rsUnpackColor8888(in);
  const float4 outF = (float4){ dot(inF, weight) };
  return rsPackColorTo8888(outF);
}

void process(rs_allocation inputImage, rs_allocation outputImage) {
  const uint32_t imageWidth = rsAllocationGetDimX(inputImage);
  const uint32_t imageHeight = rsAllocationGetDimY(inputImage);
  rs_allocation tmp = rsCreateAllocation_uchar4(imageWidth, imageHeight);
  rsForEach(invert, inputImage, tmp);
  rsForEach(greyscale, tmp, outputImage);
}

يمكنك استدعاء الدالة process() من Java أو Kotlin على النحو التالي:

Kotlin

val RS: RenderScript = RenderScript.create(context)
val script = ScriptC_singlesource(RS)
val inputAllocation: Allocation = Allocation.createFromBitmapResource(
        RS,
        resources,
        R.drawable.image
)
val outputAllocation: Allocation = Allocation.createTyped(
        RS,
        inputAllocation.type,
        Allocation.USAGE_SCRIPT or Allocation.USAGE_IO_OUTPUT
)
script.invoke_process(inputAllocation, outputAllocation)

Java

// File SingleSource.java

RenderScript RS = RenderScript.create(context);
ScriptC_singlesource script = new ScriptC_singlesource(RS);
Allocation inputAllocation = Allocation.createFromBitmapResource(
    RS, getResources(), R.drawable.image);
Allocation outputAllocation = Allocation.createTyped(
    RS, inputAllocation.getType(),
    Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT);
script.invoke_process(inputAllocation, outputAllocation);

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

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

النص البرمجي العمومي هو متغيّر عمومي عادي غير static في ملف نص برمجي (.rs). بالنسبة إلى نص برمجي عالمي يحمل الاسم var تم تحديده في الملف filename.rs، ستظهر الطريقة get_var في الفئة ScriptC_filename. ما لم تكن القيمة العامة هي const، ستكون هناك أيضًا طريقة set_var.

يحتوي نص برمجي محدد عام على قيمتين منفصلتين، قيمة Java وقيمة script. ويكون سلوك هذه القيم على النحو التالي:

  • إذا كانت var تحتوي على أداة إعداد ثابتة في النص البرمجي، سيتم تحديد القيمة الأولية var في كل من Java والنص البرمجي. وبخلاف ذلك، تكون هذه القيمة الأولية صفرًا.
  • تؤدي هذه السياسة إلى الوصول إلى var ضمن النص البرمجي لقراءة قيمة النص البرمجي وكتابتها.
  • تقرأ الطريقة get_var قيمة Java.
  • تكتب الطريقة set_var (إذا كانت متوفّرة) قيمة Java على الفور وتكتب قيمة النص البرمجي بشكل غير متزامن.

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

نواة الانزلاق بالعمق

الخفض هو عملية دمج مجموعة من البيانات في قيمة واحدة. هذه قاعدة أساسية مفيدة في البرمجة المتوازية، مع تطبيقات مثل ما يلي:

  • حساب المجموع أو الناتج على جميع البيانات
  • احتساب العمليات المنطقية (and، or، xor) على جميع البيانات
  • إيجاد القيمة الصغرى أو القصوى ضمن البيانات
  • تبحث عن قيمة معينة أو عن إحداثي قيمة معينة داخل البيانات

في الإصدار Android 7.0 (المستوى 24 لواجهة برمجة التطبيقات) والإصدارات الأحدث، تتوافق RenderScript مع نواة التقليل للسماح بخوارزميات التقليل الفعّالة التي يكتبها المستخدم. يمكنك إطلاق نواة التقليل في المدخلات ذات الأبعاد 1 أو 2 أو 3.

يوضِّح أحد الأمثلة أعلاه نواة تخفيض بسيطة للإضافة. في ما يلي نواة اختزال findMinAndMax أكثر تعقيدًا تحدد مواقع القيم الدنيا والقصوى لـ long في Allocation ذو بُعد واحد:

#define LONG_MAX (long)((1UL << 63) - 1)
#define LONG_MIN (long)(1UL << 63)

#pragma rs reduce(findMinAndMax) \
  initializer(fMMInit) accumulator(fMMAccumulator) \
  combiner(fMMCombiner) outconverter(fMMOutConverter)

// Either a value and the location where it was found, or INITVAL.
typedef struct {
  long val;
  int idx;     // -1 indicates INITVAL
} IndexedVal;

typedef struct {
  IndexedVal min, max;
} MinAndMax;

// In discussion below, this initial value { { LONG_MAX, -1 }, { LONG_MIN, -1 } }
// is called INITVAL.
static void fMMInit(MinAndMax *accum) {
  accum->min.val = LONG_MAX;
  accum->min.idx = -1;
  accum->max.val = LONG_MIN;
  accum->max.idx = -1;
}

//----------------------------------------------------------------------
// In describing the behavior of the accumulator and combiner functions,
// it is helpful to describe hypothetical functions
//   IndexedVal min(IndexedVal a, IndexedVal b)
//   IndexedVal max(IndexedVal a, IndexedVal b)
//   MinAndMax  minmax(MinAndMax a, MinAndMax b)
//   MinAndMax  minmax(MinAndMax accum, IndexedVal val)
//
// The effect of
//   IndexedVal min(IndexedVal a, IndexedVal b)
// is to return the IndexedVal from among the two arguments
// whose val is lesser, except that when an IndexedVal
// has a negative index, that IndexedVal is never less than
// any other IndexedVal; therefore, if exactly one of the
// two arguments has a negative index, the min is the other
// argument. Like ordinary arithmetic min and max, this function
// is commutative and associative; that is,
//
//   min(A, B) == min(B, A)               // commutative
//   min(A, min(B, C)) == min((A, B), C)  // associative
//
// The effect of
//   IndexedVal max(IndexedVal a, IndexedVal b)
// is analogous (greater . . . never greater than).
//
// Then there is
//
//   MinAndMax minmax(MinAndMax a, MinAndMax b) {
//     return MinAndMax(min(a.min, b.min), max(a.max, b.max));
//   }
//
// Like ordinary arithmetic min and max, the above function
// is commutative and associative; that is:
//
//   minmax(A, B) == minmax(B, A)                  // commutative
//   minmax(A, minmax(B, C)) == minmax((A, B), C)  // associative
//
// Finally define
//
//   MinAndMax minmax(MinAndMax accum, IndexedVal val) {
//     return minmax(accum, MinAndMax(val, val));
//   }
//----------------------------------------------------------------------

// This function can be explained as doing:
//   *accum = minmax(*accum, IndexedVal(in, x))
//
// This function simply computes minimum and maximum values as if
// INITVAL.min were greater than any other minimum value and
// INITVAL.max were less than any other maximum value.  Note that if
// *accum is INITVAL, then this function sets
//   *accum = IndexedVal(in, x)
//
// After this function is called, both accum->min.idx and accum->max.idx
// will have nonnegative values:
// - x is always nonnegative, so if this function ever sets one of the
//   idx fields, it will set it to a nonnegative value
// - if one of the idx fields is negative, then the corresponding
//   val field must be LONG_MAX or LONG_MIN, so the function will always
//   set both the val and idx fields
static void fMMAccumulator(MinAndMax *accum, long in, int x) {
  IndexedVal me;
  me.val = in;
  me.idx = x;

  if (me.val <= accum->min.val)
    accum->min = me;
  if (me.val >= accum->max.val)
    accum->max = me;
}

// This function can be explained as doing:
//   *accum = minmax(*accum, *val)
//
// This function simply computes minimum and maximum values as if
// INITVAL.min were greater than any other minimum value and
// INITVAL.max were less than any other maximum value.  Note that if
// one of the two accumulator data items is INITVAL, then this
// function sets *accum to the other one.
static void fMMCombiner(MinAndMax *accum,
                        const MinAndMax *val) {
  if ((accum->min.idx < 0) || (val->min.val < accum->min.val))
    accum->min = val->min;
  if ((accum->max.idx < 0) || (val->max.val > accum->max.val))
    accum->max = val->max;
}

static void fMMOutConverter(int2 *result,
                            const MinAndMax *val) {
  result->x = val->min.idx;
  result->y = val->max.idx;
}

ملاحظة: هناك المزيد من الأمثلة على نواة التقليل هنا.

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

مثال: في النواة الإضافية، تُستخدم عناصر بيانات المركم (من النوع int) لإضافة قيم الإدخال. ولا تتوفّر دالة مهيأة، وبالتالي يتم إعداد كل عنصر من عناصر بيانات المركم إلى الصفر.

مثال: في النواة findMinAndMax، تُستخدم عناصر بيانات المركم (من النوع MinAndMax) لتتبُّع القيم الدنيا والحد الأقصى التي تم العثور عليها حتى الآن. تتوفر دالة إعداد لضبط هذه القيم على LONG_MAX وLONG_MIN، على التوالي، ولضبط مواقع هذه القيم على -1، ما يشير إلى أنّ القيم ليست موجودة فعليًا في الجزء (الفارغ) من الإدخال الذي تمت معالجته.

يستدعي RenderScript وظيفة المركم مرة واحدة لكل إحداثي في الإدخالات. في العادة، يجب أن تعدّل الدالة عنصر بيانات المركم بطريقة ما وفقًا للإدخال.

مثال: في النواة addint، تضيف دالة المركم قيمة "عنصر إدخال" إلى عنصر بيانات المركم.

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

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

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

مثال: في النواة findMinAndMax، تتحقّق دالة الدمج لمعرفة ما إذا كان الحدّ الأدنى للقيمة المسجّلة في عنصر بيانات المركم "المصدر" *val أقل من الحدّ الأدنى للقيمة المسجّلة في عنصر بيانات المركم "الوجهة"، ويتم تعديلها *accum وفقًا لذلك.*accum وهي تقوم بعمل مماثل للقيمة القصوى. يؤدي هذا إلى تعديل *accum إلى الحالة التي كان من الممكن الحصول عليها إذا تم تجميع جميع قيم الإدخال في *accum بدلاً من تجميع بعضها في *accum والبعض الآخر في *val.

بعد دمج جميع عناصر بيانات المركم، يحدِّد RenderScript نتيجة التقليل للرجوع إلى Java. يمكنك كتابة دالة تحويل خارجي للقيام بذلك. ولا تحتاج إلى كتابة دالة outconversion إذا كنت تريد أن تكون القيمة النهائية لعناصر بيانات المركم المُدمجة هي نتيجة التخفيض.

مثال: في النواة addint، لا تتوفّر دالة outconversion. القيمة النهائية لعناصر البيانات المجمّعة هي مجموع كل عناصر الإدخال، وهي القيمة التي نريد عرضها.

مثال: في النواة findMinAndMax، تضبط دالة outconverter قيمة نتيجة int2 للاحتفاظ بمواقع القيم الدنيا والقصوى الناتجة عن دمج كل عناصر بيانات المركم.

كتابة نواة الاختزال

تُحدِّد #pragma rs reduce النواة الصغرى من خلال تحديد اسمها وأسماء الدوال التي تشكل النواة وأدوارها. يجب أن تكون جميع هذه الدوال static. تتطلب نواة الاختزال دائمًا دالة accumulator، ويمكنك حذف بعض الدوال الأخرى أو جميعها حسب ما تريد أن تفعله النواة.

#pragma rs reduce(kernelName) \
  initializer(initializerName) \
  accumulator(accumulatorName) \
  combiner(combinerName) \
  outconverter(outconverterName)

في ما يلي معنى العناصر في #pragma:

  • reduce(kernelName) (إلزامية): تشير إلى أنّه يتم تحديد نواة الاختزال. ستؤدي طريقة Java المنعكسة reduce_kernelName إلى تشغيل النواة.
  • initializer(initializerName) (اختيارية): تحدِّد هذه السمة اسم دالة المهيأ لنواة التقليل هذه. عند تشغيل النواة، يستدعي RenderScript هذه الدالة مرة واحدة لكل عنصر بيانات في المركم. يجب تحديد الدالة على النحو التالي:

    static void initializerName(accumType *accum) { … }

    يشير accum إلى عنصر بيانات مُجمّع لهذه الدالة من أجل إعدادها.

    إذا لم توفّر وظيفة أداة الإعداد، يهيئ RenderScript كل عنصر من عناصر بيانات المركم إلى صفر (كما لو كان بحلول memset)، ويتصرف كما لو كانت هناك دالة تهيئة تبدو كما يلي:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName) (إلزامية): تحدّد اسم وظيفة المركم الخاصة بنواة الخفض هذه. عند تشغيل النواة، يستدعي RenderScript هذه الدالة مرة واحدة لكل إحداثي في الإدخالات، لتحديث عنصر بيانات المركم بطريقة ما وفقًا للمدخلات. يجب تعريف الدالة على النحو التالي:

    static void accumulatorName(accumType *accum,
                                in1Type in1, …, inNType inN
                                [, specialArguments]) { … }
    

    يشير accum إلى عنصر بيانات مراكم لهذه الدالة يمكن تعديله. من in1 إلى inN هي وسيطة واحدة أو أكثر يتم ملؤها تلقائيًا استنادًا إلى المدخلات التي تم تمريرها إلى تشغيل kernel، ووسيطة واحدة لكل إدخال. قد تستخدم دالة المركم أيًّا من الوسيطات الخاصة بشكل اختياري.

    مثال على النواة التي تتضمّن إدخالات متعددة هي dotProduct.

  • combiner(combinerName)

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

    static void combinerName(accumType *accum, const accumType *other) { … }

    ويشير accum إلى عنصر بيانات مجمّع "الوجهة" الذي تريد تعديله. يشير other إلى عنصر بيانات أحد المُراكم "المصدر" لهذه الدالة بهدف "دمجه" في *accum.

    ملاحظة: من المحتمل أن يكون قد تم إعداد *accum أو *other أو كليهما ولكن لم يتم تمريرهما مطلقًا إلى دالة المركم، وهذا يعني أنّه لم يسبق تعديل أحدهما أو كليهما وفقًا لأي بيانات إدخال. على سبيل المثال، في النواة findMinAndMax، تبحث دالة الدمج fMMCombiner بشكل صريح عن idx < 0 لأنّها تشير إلى عنصر بيانات المركم الذي تكون قيمته INITVAL.

    إذا لم توفّر دالة الدمج، سيستخدم RenderScript وظيفة المركم بدلاً منها، ويتصرف كما لو كانت هناك دالة دمج تبدو كما يلي:

    static void combinerName(accumType *accum, const accumType *other) {
      accumulatorName(accum, *other);
    }

    تكون دالة الدمج إلزامية إذا كانت النواة تحتوي على أكثر من إدخال واحد أو إذا كان نوع بيانات الإدخال غير مطابق لنوع بيانات المركم أو إذا كانت دالة المركم تستقبل وسيطة خاصة واحدة أو أكثر.

  • outconverter(outconverterName) (اختياري): تحدّد اسم دالة التحويل الخارجي لنواة الخفض هذه. وبعد أن يجمع RenderScript جميع عناصر بيانات المركم، فإنه يستدعي هذه الدالة لتحديد نتيجة الانخفاض والعودة إلى Java. يجب تحديد الدالة على النحو التالي:

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

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

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

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

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

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

ما الذي لا يمكنك افتراضه؟

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

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

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

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

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

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

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

يجب أن تنشئ دالة الإعداد قيمة هوية. وهذا يعني أنّه إذا كان I وA هما عنصرَي بيانات مراكم تم إعدادهما من خلال دالة الإعداد، ولم يتم أبدًا تمرير السمة I إلى دالة المركم (ولكن ربما تم نقل السمة A)، يتم عندها:

مثال: في النواة الإضافة، يتم إعداد عنصر بيانات المُجمّع إلى صفر. تؤدي دالة الدمج الخاصة بالنواة هذه عملية الإضافة، والصفر هو قيمة الهوية للجمع.

مثال: في النواة findMinAndMax يتم إعداد عنصر بيانات مركم على INITVAL.

  • تترك fMMCombiner(&A, &I) القيمة A كما هي، لأنّ قيمة I هي INITVAL.
  • تضبط fMMCombiner(&I, &A) I على A، لأن قيمة I هي INITVAL.

ولذلك، فإن INITVAL هي بالفعل قيمة هوية.

يجب أن تكون دالة الدمج متغيرة. وهذا يعني أنّه إذا كان A وB هما عنصرَي بيانات المركم الذي تم إعداده من خلال دالة الإعداد، وربما تم تمرير هذه البيانات إلى دالة المركم أو أكثر، يجب أن يضبط combinerName(&A, &B) A على القيمة نفسها التي تضبطها combinerName(&B, &A) على B.

مثال: في النواة addint، تضيف دالة الدمج قيمتَي عنصر بيانات المركم، وتكون الإضافة بديلة.

مثال: في النواة findMinAndMax، تكون fMMCombiner(&A, &B) هي نفسها A = minmax(A, B) وminmax قيمة تبديلية، وبالتالي تكون fMMCombiner أيضًا.

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

  • combinerName(&A, &B);
    combinerName(&A, &C);
    
  • combinerName(&B, &C);
    combinerName(&A, &B);
    

مثال: في النواة addint، تضيف دالة الدمج قيمتَي عناصر بيانات المركم:

  • A = A + B
    A = A + C
    // Same as
    //   A = (A + B) + C
    
  • B = B + C
    A = A + B
    // Same as
    //   A = A + (B + C)
    //   B = B + C
    

الجمع ترابطي، ولذلك فإن دالة الدمج هي أيضًا.

مثال: في النواة findMinAndMax،

fMMCombiner(&A, &B)
هي نفسها
A = minmax(A, B)
فالتسلسلان هما

  • A = minmax(A, B)
    A = minmax(A, C)
    // Same as
    //   A = minmax(minmax(A, B), C)
    
  • B = minmax(B, C)
    A = minmax(A, B)
    // Same as
    //   A = minmax(A, minmax(B, C))
    //   B = minmax(B, C)
    

minmax ارتباطية، وكذلك fMMCombiner.

يجب أن تتّبع وظيفة المركم ووظيفة الدمج معًا قاعدة الطي الأساسية. وهذا يعني أنّه إذا كان A وB هما عنصرَي بيانات المركم، يعني ذلك أنّه تم إعداد A من خلال دالة الإعداد وربما يكون قد تم تمريره إلى دالة المركم صفر أو أكثر، علمًا أنّ B لم يتم إعداده، والوسيطات هي قائمة وسيطات الإدخال والوسيطات الخاصة لاستدعاء معيّن لدالة المركم، ثم يجب ضبط تسلسلَي الرموز التاليين على A

  • accumulatorName(&A, args);  // statement 1
    
  • initializerName(&B);        // statement 2
    accumulatorName(&B, args);  // statement 3
    combinerName(&A, &B);       // statement 4
    

مثال: في النواة addint، لقيمة الإدخال V:

  • العبارة 1 هي نفسها A += V
  • العبارة 2 هي نفسها B = 0
  • العبارة 3 هي نفسها B += V، والتي تماثل B = V
  • العبارة 4 هي نفسها A += B، والتي تماثل A += V

تعين العبارتين 1 و4 A على القيمة ذاتها، وبالتالي تمتثل هذه النواة لقاعدة الطي الأساسية.

مثال: في النواة findMinAndMax، لقيمة الإدخال V في الإحداثي X:

  • العبارة 1 هي نفسها A = minmax(A, IndexedVal(V, X))
  • العبارة 2 هي نفسها B = INITVAL
  • العبارة 3 هي نفسها
    B = minmax(B, IndexedVal(V, X))
    
    التي، لأن B هي القيمة الأولية، وهي نفسها
    B = IndexedVal(V, X)
    
  • العبارة 4 هي نفسها
    A = minmax(A, B)
    
    والتي تتشابه في
    A = minmax(A, IndexedVal(V, X))
    

تعين العبارتين 1 و4 A على القيمة ذاتها، وبالتالي تمتثل هذه النواة لقاعدة الطي الأساسية.

استدعاء نواة اختزال من رمز Java

بالنسبة إلى نواة الاختزال المسماة kernelName والمحددة في الملف filename.rs، تظهر ثلاث طرق تظهر في الفئة ScriptC_filename:

Kotlin

// Function 1
fun reduce_kernelName(ain1: Allocation, …,
                               ainN: Allocation): javaFutureType

// Function 2
fun reduce_kernelName(ain1: Allocation, …,
                               ainN: Allocation,
                               sc: Script.LaunchOptions): javaFutureType

// Function 3
fun reduce_kernelName(in1: Array<devecSiIn1Type>, …,
                               inN: Array<devecSiInNType>): javaFutureType

Java

// Method 1
public javaFutureType reduce_kernelName(Allocation ain1, …,
                                        Allocation ainN);

// Method 2
public javaFutureType reduce_kernelName(Allocation ain1, …,
                                        Allocation ainN,
                                        Script.LaunchOptions sc);

// Method 3
public javaFutureType reduce_kernelName(devecSiIn1Type[] in1, …,
                                        devecSiInNType[] inN);

في ما يلي بعض الأمثلة على طلب النواة المكوّن الإضافي:

Kotlin

val script = ScriptC_example(renderScript)

// 1D array
//   and obtain answer immediately
val input1 = intArrayOf()
val sum1: Int = script.reduce_addint(input1).get()  // Method 3

// 2D allocation
//   and do some additional work before obtaining answer
val typeBuilder = Type.Builder(RS, Element.I32(RS)).apply {
    setX()
    setY()
}
val input2: Allocation = Allocation.createTyped(RS, typeBuilder.create()).also {
    populateSomehow(it) // fill in input Allocation with data
}
val result2: ScriptC_example.result_int = script.reduce_addint(input2)  // Method 1
doSomeAdditionalWork() // might run at same time as reduction
val sum2: Int = result2.get()

Java

ScriptC_example script = new ScriptC_example(renderScript);

// 1D array
//   and obtain answer immediately
int input1[] = ;
int sum1 = script.reduce_addint(input1).get();  // Method 3

// 2D allocation
//   and do some additional work before obtaining answer
Type.Builder typeBuilder =
  new Type.Builder(RS, Element.I32(RS));
typeBuilder.setX();
typeBuilder.setY();
Allocation input2 = createTyped(RS, typeBuilder.create());
populateSomehow(input2);  // fill in input Allocation with data
ScriptC_example.result_int result2 = script.reduce_addint(input2);  // Method 1
doSomeAdditionalWork(); // might run at same time as reduction
int sum2 = result2.get();

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

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

الطريقة 3 هي نفسها الطريقة 1، إلا أنّه بدلاً من أخذ مدخلات التخصيص، يتم استخدام إدخالات صفيف من Java. وهو ما يسهّل عليك كتابة رمز برمجي لإنشاء تخصيص بشكل صريح ونسخ البيانات إليه من مصفوفة Java. مع ذلك، لن يؤدي استخدام الطريقة الثالثة بدلاً من الطريقة 1 إلى زيادة أداء الرمز. بالنسبة إلى كل صفيف إدخال، تنشئ الطريقة الثالثة عملية تخصيص مؤقتة أحادية البُعد مع تفعيل النوع Element وsetAutoPadding(boolean) المناسبين، وتنسخ المصفوفة إلى التخصيص كما لو تم ذلك باستخدام طريقة copyFrom() المناسبة في Allocation. وتستدعي بعد ذلك الطريقة 1، مع اجتياز تلك التخصيصات المؤقتة.

ملاحظة: إذا كان تطبيقك سيُجري طلبات kernel متعددة باستخدام المصفوفة نفسها أو باستخدام صفائف مختلفة بالأبعاد ونوع العنصر نفسه، يمكنك تحسين الأداء عن طريق إنشاء "التخصيصات" وملؤها وإعادة استخدامها بشكل صريح بنفسك، بدلاً من استخدام الطريقة الثالثة.

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

Kotlin

class ScriptC_filename(rs: RenderScript) : ScriptC(…) {
    object javaFutureType {
        fun get(): javaResultType { … }
    }
}

Java

public class ScriptC_filename extends ScriptC {
  public static class javaFutureType {
    public javaResultType get() { … }
  }
}

يتم تحديد javaResultType من resultType في outconverter. ما لم تكن resultType هو نوع غير موقَّع (مقياس أو متّجه أو مصفوفة)، يكون javaResultType هو نوع Java المقابل مباشرةً. إذا كان resultType غير موقَّع وهناك نوع أكبر من Java، يكون javaResultType هو نوع Java الأكبر حجمًا، وإلّا يكون نوع Java المقابل مباشرةً. على سبيل المثال:

  • إذا كانت قيمة resultType هي int أو int2 أو int[15]، تكون 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 كيفية استخدام واجهات برمجة التطبيقات الواردة في هذه الصفحة.