نظرة عامة على 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. يمكنك الوصول إلى النص البرمجي globals من حيث رمز 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. يتم تلقائيًا تشغيل هذه النواة في كامل مدخلها Allocation، مع إمكانية تنفيذ دالة kernel مرة واحدة فقط لكل Element في Allocation.

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

      ملاحظة: قبل استخدام Android 6.0 (المستوى 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.

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

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

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

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

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

تعيين دقة النقطة العائمة

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. تأكَّد من تثبيت الإصدار المطلوب لحزمة تطوير البرامج (SDK) لنظام التشغيل Android.
  2. عدِّل إعدادات عملية إصدار Android لتشمل إعدادات RenderScript:
    • افتح ملف build.gradle في مجلد التطبيقات ضِمن وحدة تطبيقك.
    • أضف إعدادات RenderScript التالية إلى الملف:

      رائع

              android {
                  compileSdkVersion 33
      
                  defaultConfig {
                      minSdkVersion 9
                      targetSdkVersion 19
      
                      renderscriptTargetApi 18
                      renderscriptSupportModeEnabled true
                  }
              }
              

      Kotlin

              android {
                  compileSdkVersion(33)
      
                  defaultConfig {
                      minSdkVersion(9)
                      targetSdkVersion(19)
      
                      renderscriptTargetApi = 18
                      renderscriptSupportModeEnabled = true
                  }
              }
              

      تتحكّم الإعدادات المذكورة أعلاه في سلوك معيّن في عملية إصدار Android:

      • renderscriptTargetApi: تحدّد هذه السمة إصدار رمز البايت الذي سيتم إنشاؤه. ننصحك بضبط هذه القيمة على أدنى مستوى لواجهة برمجة التطبيقات لتوفير جميع الوظائف التي تستخدمها وضبط renderscriptSupportModeEnabled على true. وتتمثل القيم الصالحة لهذا الإعداد في أي قيمة لعدد صحيح تبدأ من 11 إلى أحدث مستوى لواجهة برمجة التطبيقات تم إصدارها. في حال ضبط الحدّ الأدنى لإصدار حزمة تطوير البرامج (SDK) المحدّد في بيان التطبيق على قيمة مختلفة، سيتم تجاهل هذه القيمة واستخدام القيمة المستهدَفة في ملف الإصدار لضبط الحدّ الأدنى لإصدار حزمة تطوير البرامج (SDK).
      • renderscriptSupportModeEnabled: تحدّد هذه السياسة أنّه يجب إرجاع رمز البايت الذي تم إنشاؤه إلى إصدار متوافق إذا كان الجهاز الذي يعمل عليه لا يتوافق مع الإصدار المستهدَف.
  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. ضبط أي script globals ضروري يمكنك ضبط نظام التشغيل globals باستخدام طُرق ضِمن فئة ScriptC_filename نفسها المسماة set_globalname. على سبيل المثال، لضبط متغيّر int باسم threshold، استخدِم طريقة Java set_threshold(int)، ولإعداد متغيّر rs_allocation باسم lookup، استخدِم طريقة Java set_lookup(Allocation). إنّ طرق set غير متزامنة.
  6. شغِّل الوظائف المناسبة للنواة والقابلة للاستدعاء.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// File: singlesource.rs

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

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

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

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

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

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

Kotlin

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

Java

// File SingleSource.java

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

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

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

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

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

مثال: في نواة 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)، يعني ذلك أنّه
  • على 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 هما عنصرا بيانات المركم تم إعداده من خلال دالة المُنشئ، وكان ذلك قد تم تمريره إلى دالة المركم صفرًا أو أكثر، يجب أن تضبط السمة combinerName(&A, &B) القيمة A على القيمة نفسها التي يضبطها combinerName(&B, &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 هو نوع جافا المقابل مباشرةً. إذا كان نوع resultType غير موقَّع وكان هناك نوع أكبر مُوقَّع بلغة Java، يكون نوع 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 استخدام واجهات برمجة التطبيقات المشمولة في هذه الصفحة.