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

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

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

  • اللغة نفسها هي لغة مشتقة من C99 لكتابة كود الحوسبة عالية الأداء. تصف كتابة نواة RenderScript كيفية استخدامها لكتابة نواة الحوسبة.
  • تُستخدَم control API لإدارة عمر موارد RenderScript وللتحكّم في تنفيذ النواة. وهو متوفر بثلاث لغات مختلفة: 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 Script 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، في هذا المثال) وأسماء وأدوار الدوال التي تكوِّن النواة (دالة accumulator addintAccum، في هذا المثال). يجب أن تكون جميع هذه الدوال static. تتطلب نواة الاختزال دائمًا دالة accumulator، وقد يكون لها أيضًا وظائف أخرى، حسب ما تريد أن تقوم به النواة.

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

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

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

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

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

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

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

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

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

يُرجى العِلم أنّه بدءًا من الإصدار 24.0.0 من Android SDK Build-tools، لن يتم دعم 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.*
    

    Java

    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)
      

      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. شغِّل النواة kernel والدوال القابلة للاستدعاء المناسبة.

    يتم عرض طرق تشغيل نواة معيّنة في الفئة ScriptC_filename نفسها باستخدام الطرق المسماة forEach_mappingKernelName() أو reduce_reductionKernelName(). وتكون عمليات الإطلاق هذه غير متزامنة. استنادًا إلى وسيطات النواة، تأخذ الطريقة تخصيصًا واحدًا أو أكثر، يجب أن يكون لها نفس الأبعاد. يتم تنفيذ النواة تلقائيًا على كل إحداثي في تلك الأبعاد، ولتنفيذ نواة على مجموعة فرعية من هذه الإحداثيات، قم بتمرير 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 أحادي المصدر، ويتم فيها إطلاق النواة من البرنامج النصي عند تحديدها بدلاً من جافا. يقتصر هذا النهج حاليًا على نواة التعيين، والتي يشار إليها ببساطة باسم "النواة" في هذا القسم للإيجاز. تتيح هذه الميزة الجديدة أيضًا إنشاء عمليات توزيع من النوع rs_allocation من داخل النص البرمجي. أصبح من الممكن الآن تنفيذ خوارزمية كاملة ضمن نص برمجي فقط، حتى إذا تطلّب الأمر عمليات تشغيل نواة متعددة. هناك فائدة مضاعفة: استخدام رموز برمجية أكثر قابلية للقراءة لأنّها تحافظ على تنفيذ الخوارزمية بلغة واحدة، وربما تكون أسرع في التعليمات البرمجية بسبب قلة عمليات الانتقال بين Java وRenderScript على مستوى عمليات تشغيل النواة المتعددة.

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

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

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

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

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

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

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

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

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

ملاحظة: هذا يعني أنّه باستثناء أي مهيئ ثابت في النص البرمجي، لا تكون القيم المكتوبة في السلسلة العامة من داخل النص البرمجي مرئية للغة 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 دالة المركم مرة واحدة لكل إحداثي في الإدخالات. عادةً ما يجب أن تعدِّل الدالة عنصر بيانات المركم بطريقة ما وفقًا للمدخل.

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

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

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

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

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

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

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

مثال: في نواة findMinAndMax، تعمل دالة outconversion على إعداد قيمة نتيجة 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;
    }

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

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

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

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

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

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

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

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

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

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

يجب أن تنشئ دالة المُعدّة قيمة هوية. وهذا يعني أنّه إذا كان I وA هما عنصرا بيانات في المركم تم إعدادهما من خلال دالة المُعِدّ، ولم يتم أبدًا تمرير I إلى دالة المركم (ولكن ربما تم إدخال A)، عندها
  • يجب أن يترك combinerName(&A, &I) A كما هو
  • يجب أن يترك combinerName(&I, &A) I كما هو A

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

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

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

ولذلك، تكون INITVAL قيمة هوية.

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

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

مثال: في نواة 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);
    

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

  • 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

Java

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

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

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

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

Kotlin

val script = ScriptC_example(renderScript)

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

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

Java

ScriptC_example script = new ScriptC_example(renderScript);

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

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

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

Java

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

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

  • إذا لم تكن 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، وكانت نواة الاختزال تُنتج قيمة غير قابلة للتمثيل، تقدّم javaFutureType.get() استثناءً.

الطريقة 3 وdevecSiInXType

devecSiInXType هو نوع جافا المقابل لـ 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 استخدام واجهات برمجة التطبيقات المشمولة في هذه الصفحة.