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

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

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

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

كتابة نواة RenderScript

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

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

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

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

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

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

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

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

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

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

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

      #define RS_KERNEL __attribute__((kernel))

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

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

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

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

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

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

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

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

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

  1. تأكَّد من تثبيت الإصدار المطلوب من حزمة تطوير البرامج (SDK) لنظام التشغيل Android.
  2. عدِّل إعدادات عملية إنشاء تطبيق Android لتضمين إعدادات RenderScript:
    • افتح ملف build.gradle في مجلد التطبيق الخاص بوحدة التطبيق.
    • أضِف إعدادات RenderScript التالية إلى الملف:
      رائعKotlin
              android {
                  compileSdkVersion 33
      
                  defaultConfig {
                      minSdkVersion 9
                      targetSdkVersion 19
      
                      renderscriptTargetApi 18
                      renderscriptSupportModeEnabled true
                  }
              }
              
              android {
                  compileSdkVersion(33)
      
                  defaultConfig {
                      minSdkVersion(9)
                      targetSdkVersion(19)
      
                      renderscriptTargetApi = 18
                      renderscriptSupportModeEnabled = true
                  }
              }
              

      تتحكّم الإعدادات المُدرَجة أعلاه في سلوك معيّن في عملية إنشاء نظام Android:

      • renderscriptTargetApi: لتحديد إصدار رمز الآلة الذي سيتم إنشاؤه ننصحك بضبط هذه القيمة على أدنى مستوى لواجهة برمجة التطبيقات يمكنه توفير جميع الوظائف التي تستخدمها وضبط renderscriptSupportModeEnabled على true. القيم الصالحة لهذا الإعداد هي أي قيمة عددية من 11 إلى أحدث مستوى لواجهة برمجة تطبيقات تم إصداره إذا تم ضبط الحد الأدنى لإصدار حزمة SDK المحدَّد في ملف بيان التطبيق على قيمة مختلفة، يتم تجاهل تلك القيمة ويتم استخدام القيمة المستهدَفة في ملف الإنشاء لضبط الحد الأدنى لإصدار حزمة SDK.
      • renderscriptSupportModeEnabled: لتحديد أنّه يجب الرجوع إلى إصدار متوافق من رمز bytecode الذي تم إنشاؤه إذا كان الجهاز الذي يتم تشغيله عليه لا يتوافق مع الإصدار المستهدَف.
  3. في فئات التطبيقات التي تستخدم RenderScript، أضِف عملية استيراد إلى "مكتبة الدعم". الفئات:
    KotlinJava
    import android.support.v8.renderscript.*
    import android.support.v8.renderscript.*;

استخدام RenderScript من رمز Java أو Kotlin

يعتمد استخدام RenderScript من رمز Java أو Kotlin على فئات واجهة برمجة التطبيقات المتوفّرة في حزمة android.renderscript أو android.support.v8.renderscript. معظم الأقسام تتبع التطبيقات نمط الاستخدام الأساسي نفسه:

  1. إعداد سياق RenderScript: يضمن سياق RenderScript، الذي تم إنشاؤه باستخدام create(Context)، إمكانية استخدام RenderScript ويقدّم عنصرًا للتحكّم في مدة صلاحية جميع عناصر RenderScript اللاحقة. يجب اعتبار عملية إنشاء السياق عملية قد تستغرق وقتًا طويلاً، لأنّها قد تنشئ موارد على مختلف أجزاء الأجهزة، ويجب ألّا تكون في المسار الحرج للتطبيق إن أمكن ذلك. وعادةً ما يحتوي التطبيق على سياق RenderScript واحد فقط في كل مرة.
  2. عليك إنشاء Allocation واحد على الأقل ليتم تمريره إلى النص البرمجي Allocation هو عنصر RenderScript يقدّم مساحة تخزين لكمية ثابتة من البيانات. تستغرق النواة في النصوص البرمجية Allocation. الكائنات كمدخلات ومخرجات محددة، ويمكن تحويل كائنات Allocation يتم الوصول إليها في النواة باستخدام rsGetElementAt_type() rsSetElementAt_type() عند ربطه كنصوص عامة في النص البرمجي. تسمح كائنات Allocation بتمرير الصفائف من رمز Java إلى RenderScript والعكس صحيح. يتم عادةً إنشاء عناصر Allocation باستخدام createTyped() أو createFromBitmap().
  3. أنشِئ النصوص البرمجية اللازمة. هناك نوعان من النصوص البرمجية المتاحة إليك عند استخدام RenderScript:
    • ScriptC: هذه هي النصوص البرمجية التي يحدّدها المستخدم كما هو موضّح في مقالة كتابة نواة RenderScript أعلاه. كل نص برمجي له فئة Java يعكسه المحول البرمجي لـ RenderScript لتسهيل الوصول إلى النص البرمجي من رمز Java؛ هذا الصف يحمل الاسم ScriptC_filename. على سبيل المثال، إذا كان نواة الربط المذكورة أعلاه متوفّرة في invert.rs وكان سياق RenderScript متوفّرًا في mRenderScript، سيكون رمز Java أو Kotlin لإنشاء مثيل للنص البرمجي على النحو التالي:
      KotlinJava
      val invert = ScriptC_invert(renderScript)
      ScriptC_invert invert = new ScriptC_invert(renderScript);
    • ScriptIntrinsic: هذه هي نوى RenderScript المضمّنة للعمليات الشائعة، مثل التمويه باستخدام مقياس غوس وعمليات التفاف الصور ومزجها. لمزيد من المعلومات، يُرجى الاطّلاع على الفئات الفرعية لمحاولة ScriptIntrinsic.
  4. ملء عمليات التخصيص بالبيانات: باستثناء عمليات التوزيع التي تم إنشاؤها باستخدام createFromBitmap()، تتم تعبئة عملية التوزيع ببيانات فارغة عند إنشائها لأول مرة. لتعبئة عملية تخصيص، استخدِم إحدى طرق "النسخ" في Allocation. طرق "النسخ" متزامنة.
  5. اضبط أي متغيّرات عامة للنص البرمجي ضرورية. يمكنك تعيين دوال عمومية باستخدام الطرق الموجودة في الفئة ScriptC_filename نفسها باسم set_globalname. بالنسبة على سبيل المثال، لضبط متغيّر int باسم threshold، استخدِم السمة طريقة Java set_threshold(int) ومن أجل تحديد متغير rs_allocation اسمه lookup، استخدم Java الطريقة set_lookup(Allocation). طرق set غير متزامنة.
  6. ابدأ تشغيل النواة المناسبة والدوال القابلة للاستدعاء.

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

Single-Source RenderScript

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

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

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

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

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

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

// File: singlesource.rs

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

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

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

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

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

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

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

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

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

المتغيّرات الشاملة للنص البرمجي

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

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

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

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

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

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

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

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

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

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

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

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

typedef struct {
  IndexedVal min, max;
} MinAndMax;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    static void initializerName(accumType *accum) {  }

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

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

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

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

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

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

  • combiner(combinerName)

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

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

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

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

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

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

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

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

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

يجب أن تنشئ دالة الإعداد قيمة تعريف. وهذا يعني أنّه إذا كان 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);

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

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

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

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

fMMCombiner(&A, &B)
هو نفسه
A = minmax(A, B)
ويكون التسلسلان على النحو التالي:
  • A = minmax(A, B)
    A = minmax(A, C)
    // Same as
    //   A = minmax(minmax(A, B), C)
  • B = minmax(B, C)
    A = minmax(A, B)
    // Same as
    //   A = minmax(A, minmax(B, C))
    //   B = minmax(B, C)

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

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

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

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

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

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

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

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

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

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

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

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

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

// Function 3
fun reduce_kernelName(in1: Array<devecSiIn1Type>, ,
                               inN: Array<devecSiInNType>): javaFutureType
// Method 1
public javaFutureType reduce_kernelName(Allocation ain1, ,
                                        Allocation ainN);

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

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

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

KotlinJava
val script = ScriptC_example(renderScript)

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

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

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

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

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

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

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

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

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

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

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

  • إذا كان resultType يساوي int أو int2 أو int[15]، يكون javaResultType يساوي int أو Int2 أو int[]. يمكن تمثيل جميع قيم resultType باستخدام javaResultType.
  • إذا كانت قيمة resultType هي uint أو uint2 أو uint[15]، ثم javaResultType هي long، Long2، أو long[]. يمكن تمثيل جميع قيم resultType باستخدام javaResultType.
  • إذا كانت resultType هي ulong، ulong2، أو ulong[15]، ثم javaResultType هي long أو Long2 أو long[]. هناك قيم معيّنة من resultType لا يمكن تمثيلها من خلال javaResultType.

javaFutureType هو نوع النتيجة المتجاوب في المستقبل إلى resultType من outconversioner .

  • إذا لم تكن resultType هو نوع مصفوفة، حينئذٍ javaFutureType يبلغ result_resultType.
  • إذا كان resultType مصفوفة بطول Count تتضمّن عناصر من النوع memberType، تكون قيمة javaFutureType هي resultArrayCount_memberType.

مثلاً:

KotlinJava
class ScriptC_filename(rs: RenderScript) : ScriptC(…) {

    // for kernels with int result
    object result_int {
        fun get(): Int =     }

    // for kernels with int[10] result
    object resultArray10_int {
        fun get(): IntArray =     }

    // for kernels with int2 result
    //   note that the Kotlin type name "Int2" is not the same as the script type name "int2"
    object result_int2 {
        fun get(): Int2 =     }

    // for kernels with int2[10] result
    //   note that the Kotlin type name "Int2" is not the same as the script type name "int2"
    object resultArray10_int2 {
        fun get(): Array<Int2> =     }

    // for kernels with uint result
    //   note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint"
    object result_uint {
        fun get(): Long =     }

    // for kernels with uint[10] result
    //   note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint"
    object resultArray10_uint {
        fun get(): LongArray =     }

    // for kernels with uint2 result
    //   note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2"
    object result_uint2 {
        fun get(): Long2 =     }

    // for kernels with uint2[10] result
    //   note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2"
    object resultArray10_uint2 {
        fun get(): Array<Long2> =     }
}
public class ScriptC_filename extends ScriptC {
  // for kernels with int result
  public static class result_int {
    public int get() {}
  }

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

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

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

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

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

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

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

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

إذا لم تتمكّن javaResultType من تمثيل جميع قيم النوع resultType، ونتج عن ملف تعريف kernel للاختزال قيمة لا يمكن تمثيلها، يُرسِل javaFutureType.get() استثناءً.

الطريقة 3 وdevecSiInXType

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

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

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

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

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

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

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

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

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

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

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

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

#define BUCKETS 256
typedef uint32_t Histogram[BUCKETS];

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

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

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

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

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

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

BasicRenderScript، RenderScriptIntrinsic، ومرحبًا بك في Compute لتوضيح استخدام واجهات برمجة التطبيقات التي تتناولها هذه الصفحة.