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

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

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

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

كتابة نواة RenderScript

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

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

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

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

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

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

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

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

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

      إذا كنت بحاجة إلى إدخال أو إخراج Allocations أكثر من النواة، يجب ربط هذه الكائنات بـ rs_allocation النصية globals والوصول إليها من 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، في هذا المثال) وأسماء وأدوار الوظائف التي تشكل النواة (an accumulator الدالة addintAccum، في هذا المثال). يجب أن تكون كل هذه الدوال static. تتطلب نواة الاختزال دائمًا دالة accumulator، وقد تكون لها وظائف أخرى أيضًا، بناءً على ما تريد أن تفعله النواة.

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

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

      يتم توضيح نواة الحد بمزيد من التفاصيل هنا.

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

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

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

  • دالة init() اختيارية. الدالة init() هي نوع خاص من الدوال القابلة للاستدعاء والتي يشغّلها RenderScript عند إنشاء مثيل للنص البرمجي لأول مرة. وهذا يسمح بإجراء بعض العمليات الحسابية تلقائيًا عند إنشاء النص البرمجي.
  • لا تتوفّر دوال ودوال النصوص البرمجية الثابتة أو أكثر. يعادل النص البرمجي الثابت العمومي مع نص برمجي عمومي باستثناء أنه لا يمكن الوصول إليه من رمز جافا. الدالة الثابتة هي دالة C عادية يمكن استدعاؤها من أي نواة kernel أو دالة قابلة للاستدعاء في البرنامج النصي، ولكن لا يتم عرضها لواجهة برمجة تطبيقات 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 من خلال واجهات برمجة تطبيقات Support Library.
  • إذا كنت تستخدم واجهات برمجة التطبيقات لمكتبة الدعم، ستحصل على حِزم APK (ربما بشكل كبير) أكبر مما إذا كنت تستخدم واجهات برمجة التطبيقات الأصلية (android.renderscript).

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

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

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

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

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

لاستخدام واجهات برمجة تطبيقات 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، يمكنك إضافة عملية استيراد لفئات Support Library:

    لغة Kotlin

    import android.support.v8.renderscript.*
    

    جافا

    import android.support.v8.renderscript.*;
    

استخدام RenderScript من لغة Java أو لغة Kotlin Code

يعتمد استخدام 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() عند ربطها كـ globals النصوص البرمجية. تسمح كائنات 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)
      

      جافا

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic: هي نواة RenderScript مضمّنة للعمليات الشائعة، مثل التمويه والالتفاف ودمج الصور بنمط غاوس. لمزيد من المعلومات، راجِع الفئات الفرعية ScriptIntrinsic.
  4. تعبئة التخصيصات بالبيانات: باستثناء عمليات التوزيع التي تم إنشاؤها باستخدام createFromBitmap()، تتم تعبئة عملية التخصيص ببيانات فارغة عند إنشائها لأول مرة. لتعبئة تخصيص، استخدِم إحدى طرق "النسخ" في Allocation. طرق "النسخ" متزامنة.
  5. اضبط أي script globals ضروري. يمكنك ضبط العوالم العالمية باستخدام طُرق من الفئة ScriptC_filename نفسها المسماة set_globalname. على سبيل المثال، لضبط متغيّر int باسم threshold، استخدِم طريقة Java set_threshold(int). ولضبط متغيّر rs_allocation باسم lookup، استخدِم طريقة Java set_lookup(Allocation). إنّ طُرق set غير متزامنة.
  6. شغِّل النواة kernel والدوال القابلة للاستدعاء المناسبة.

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

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

  7. استرجع البيانات من كائنات Allocation وكائنات javaFutureType. للوصول إلى البيانات من Allocation من رمز Java، يجب نسخ هذه البيانات مرة أخرى إلى Java باستخدام إحدى طرق "النسخ" في Allocation. للحصول على نتيجة نواة الاختزال، يجب استخدام الطريقة javaFutureType.get(). طريقة "النسخ" و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() لتشغيلها. تأخذ واجهة برمجة التطبيقات هذه دالة النواة كمعلمة أولى، تليها تخصيصات المدخلات والمخرجات. تستخدم واجهة برمجة التطبيقات المشابهة rsForEachWithOptions() وسيطة إضافية من النوع rs_script_call_t، والتي تحدد مجموعة فرعية من العناصر من عمليات تخصيص الإدخالات والمخرجات لدالة النواة لمعالجتها.

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

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

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

يحتوي القسم كتابة نواة RenderScript على نموذج نواة (kernel) يقلب الصورة. ويوسِّع المثال التالي هذا التأثير لتطبيق أكثر من تأثير على صورة، وذلك باستخدام نص 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)

جافا

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

نصوص برمجية عمومية

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

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

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

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

نواة تقليل العمق

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

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

في الإصدار 7.0 من نظام التشغيل Android (مستوى واجهة برمجة التطبيقات 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 kernel، تُستخدم عناصر بيانات المركم (من النوع MinAndMax) لتتبُّع القيم الدنيا والقصوى التي تم العثور عليها حتى الآن. هناك دالة مبدئية لضبط هذه القيم على LONG_MAX وLONG_MIN، على التوالي، ولضبط مواقع هاتين القيمتين على -1، ما يشير إلى أن القيم ليست متوفرة فعليًا في الجزء (الفارغ) من الإدخال الذي تمت معالجته.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    static void initializerName(accumType *accum) { … }

    مؤشر accum هو مؤشر إلى عنصر بيانات المركم لهذه الدالة للتهيئة.

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

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

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

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

    من أمثلة النواة ذات المدخلات المتعددة dotProduct.

  • combiner(combinerName)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

وبالتالي، فإنّ minmax ترتبط ارتباطًا وثيقًا، وبالتالي فإنّ fMMCombiner أيضًا.

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

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

مثال: في نواة الإضافة، لقيمة الإدخال 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

جافا

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

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

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

في ما يلي بعض الأمثلة على استدعاء نواة addint:

لغة Kotlin

val script = ScriptC_example(renderScript)

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

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

جافا

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 استثناءً. وتُنفذ النواة kernel على كل إحداثي في تلك الأبعاد.

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

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

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

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

لغة Kotlin

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

جافا

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

يتم تحديد javaResultType من resultType لدالة outconversionType. ما لم تكن 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 لدالة outconversion.

  • إذا لم يكن 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> = …
    }
}

جافا

public class ScriptC_filename extends ScriptC {
  // for kernels with int result
  public static class result_int {
    public int get() { … }
  }

  // for kernels with int[10] result
  public static class resultArray10_int {
    public int[] get() { … }
  }

  // for kernels with int2 result
  //   note that the Java type name "Int2" is not the same as the script type name "int2"
  public static class result_int2 {
    public Int2 get() { … }
  }

  // for kernels with int2[10] result
  //   note that the Java type name "Int2" is not the same as the script type name "int2"
  public static class resultArray10_int2 {
    public Int2[] get() { … }
  }

  // for kernels with uint result
  //   note that the Java type "long" is a wider signed type than the unsigned script type "uint"
  public static class result_uint {
    public long get() { … }
  }

  // for kernels with uint[10] result
  //   note that the Java type "long" is a wider signed type than the unsigned script type "uint"
  public static class resultArray10_uint {
    public long[] get() { … }
  }

  // for kernels with uint2 result
  //   note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2"
  public static class result_uint2 {
    public Long2 get() { … }
  }

  // for kernels with uint2[10] result
  //   note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2"
  public static class resultArray10_uint2 {
    public Long2[] get() { … }
  }
}

إذا كان javaResultType عبارة عن نوع كائن (بما في ذلك نوع مصفوفة)، سيؤدي كل استدعاء إلى javaFutureType.get() على المثيل نفسه إلى عرض الكائن نفسه.

إذا لم تتمكّن javaResultType من تمثيل جميع قيم النوع resultType، وكانت نواة الاختزال تُنتج قيمة غير قابلة للتمثيل، تُطرح javaFutureType.get() استثناءً.

الطريقة 3 وdevecSiInXType

devecSiInXType هو نوع Java المقابل لـ inXType للوسيطة المقابلة في دالة المركم. ما لم يكن inXType نوعًا غير موقَّع أو نوع متجه، يكون devecSiInXType هو نوع Java المقابل مباشرةً. إذا كان inXType نوعًا مقياسيًا غير موقَّع، يكون devecSiInXType هو نوع Java المقابل مباشرةً للنوع القياسي الموقّع من الحجم نفسه. إذا كان inXType من نوع متجه مُوقَّع، يكون devecSiInXType هو نوع Java المقابل مباشرةً لنوع مكوّن المتجه. إذا كان inXType عبارة عن نوع متجه غير موقَّع، يكون devecSiInXType هو نوع Java المقابل مباشرةً للنوع المقياسي المُوقَّع والحجم نفسه لنوع مكوّن المتجه. على سبيل المثال:

  • إذا كانت قيمة inXType هي int، تكون قيمة devecSiInXType هي int.
  • إذا كانت قيمة inXType هي int2، تكون قيمة devecSiInXType int. الصفيف هو تمثيل مسطح، أي أنّه يحتوي على ضعف عدد العناصر العددية الذي يشمله التخصيص المكوّن من عناصر متجه مكوّن من مكوّنَين. وهذه هي الطريقة نفسها التي تعمل بها طُرق copyFrom() في Allocation.
  • إذا كانت قيمة inXType هي uint، تكون قيمة deviceSiInXType هي int. يتم تفسير القيمة الموقّعة في صفيف Java على أنها قيمة غير موقّعة لنمط البت نفسه في التخصيص. وهذه هي الطريقة نفسها التي تعمل بها طُرق copyFrom() في Allocation.
  • إذا كانت قيمة inXType هي uint2، تكون قيمة deviceSiInXType هي int. هذه تركيبة من طريقة التعامل مع int2 وuint: الصفيف هو تمثيل مسطح، ويتم تفسير القيم الموقَّعة لصفيف Java على أنّها قيم عناصر غير موقَّعة في RenderScript.

بالنسبة إلى الطريقة 3، يتم التعامل مع أنواع الإدخال بشكل مختلف عن أنواع النتائج:

  • يتم مسطحة إدخال المتجه للنص البرمجي على جانب Java، في حين أن نتيجة المتجه للنص البرمجي ليست كذلك.
  • يتم تمثيل الإدخال غير الموقَّع للنص البرمجي كإدخال موقَّع بالحجم نفسه على جانب Java، في حين يتم تمثيل النتيجة غير الموقَّعة للنص البرمجي كنوع موقَّع موسّع على جانب Java (إلا في حالة ulong).

مزيد من الأمثلة على نواة الاختزال

#pragma rs reduce(dotProduct) \
  accumulator(dotProductAccum) combiner(dotProductSum)

// Note: No initializer function -- therefore,
// each accumulator data item is implicitly initialized to 0.0f.

static void dotProductAccum(float *accum, float in1, float in2) {
  *accum += in1*in2;
}

// combiner function
static void dotProductSum(float *accum, const float *val) {
  *accum += *val;
}
// Find a zero Element in a 2D allocation; return (-1, -1) if none
#pragma rs reduce(fz2) \
  initializer(fz2Init) \
  accumulator(fz2Accum) combiner(fz2Combine)

static void fz2Init(int2 *accum) { accum->x = accum->y = -1; }

static void fz2Accum(int2 *accum,
                     int inVal,
                     int x /* special arg */,
                     int y /* special arg */) {
  if (inVal==0) {
    accum->x = x;
    accum->y = y;
  }
}

static void fz2Combine(int2 *accum, const int2 *accum2) {
  if (accum2->x >= 0) *accum = *accum2;
}
// Note that this kernel returns an array to Java
#pragma rs reduce(histogram) \
  accumulator(hsgAccum) combiner(hsgCombine)

#define BUCKETS 256
typedef uint32_t Histogram[BUCKETS];

// Note: No initializer function --
// therefore, each bucket is implicitly initialized to 0.

static void hsgAccum(Histogram *h, uchar in) { ++(*h)[in]; }

static void hsgCombine(Histogram *accum,
                       const Histogram *addend) {
  for (int i = 0; i < BUCKETS; ++i)
    (*accum)[i] += (*addend)[i];
}

// Determines the mode (most frequently occurring value), and returns
// the value and the frequency.
//
// If multiple values have the same highest frequency, returns the lowest
// of those values.
//
// Shares functions with the histogram reduction kernel.
#pragma rs reduce(mode) \
  accumulator(hsgAccum) combiner(hsgCombine) \
  outconverter(modeOutConvert)

static void modeOutConvert(int2 *result, const Histogram *h) {
  uint32_t mode = 0;
  for (int i = 1; i < BUCKETS; ++i)
    if ((*h)[i] > (*h)[mode]) mode = i;
  result->x = mode;
  result->y = (*h)[mode];
}

نماذج تعليمات برمجية إضافية

توضّح نماذج PrimaryRenderScript وRenderScriptIntrinsic وHello Compute استخدام واجهات برمجة التطبيقات المشمولة في هذه الصفحة.