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

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

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

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

كتابة نواة RenderScript

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

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

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

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

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

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

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

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

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

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

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

      يمكنك الاطّلاع على شرح أكثر تفصيلاً حول عمليات الاختزال هنا.

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

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

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

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

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

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

  • #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 من واجهة برمجة التطبيقات) والإصدارات الأحدث.

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

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

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

لاستخدام واجهات برمجة التطبيقات RenderScript في &quot;مكتبة الدعم&quot;، يجب ضبط بيئة التطوير لتتمكّن من الوصول إليها. يجب توفُّر أدوات Android SDK التالية لاستخدام واجهات برمجة التطبيقات هذه:

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

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

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

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

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

      Groovy

              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 البرمجية

يعتمد استخدام 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 لإنشاء النص البرمجي على النحو التالي:

      Kotlin

      val invert = ScriptC_invert(renderScript)

      Java

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

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

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

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

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

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

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

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

Single-Source RenderScript

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

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

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

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

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

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

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

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

يحتوي عنصر JavaScript الشامل على قيمتَين منفصلتَين، إحداهما Java والأخرى نص برمجي. تتصرّف هذه القيم على النحو التالي:

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

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

Reduction Kernels in Depth

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

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

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

يعرض المثال أعلاه نواة بسيطة لعملية تقليل addint. في ما يلي نواة تقليل أكثر تعقيدًا 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)، ولكن يمكنك كتابة دالة تهيئة لتنفيذ إجراء مختلف.

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

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

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

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

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

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

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

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

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

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

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

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

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

    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

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

مثال: في النواة findMinAndMax، يتم تهيئة عنصر بيانات مجمّع إلى INITVAL.

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

لذلك، INITVAL هي بالفعل قيمة تعريفية.

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

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

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

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

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

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

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

عملية الجمع ترابطية، وبالتالي تكون دالة الدمج كذلك.

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

fMMCombiner(&A, &B)
هي نفسها
A = minmax(A, B)
إذًا، التسلسلان هما
  • A = minmax(A, B)
    A = minmax(A, C)
    // Same as
    //   A = minmax(minmax(A, B), C)
  • B = minmax(B, C)
    A = minmax(A, B)
    // Same as
    //   A = minmax(A, minmax(B, C))
    //   B = minmax(B, C)

minmax هي دمجية، وبالتالي fMMCombiner هي أيضًا دمجية.

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

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

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

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

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

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

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

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

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

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

Kotlin

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

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

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

Java

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

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

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

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

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

الطريقة 3 هي نفسها الطريقة 1، إلا أنّها تستخدم مصفوفة إدخال Java بدلاً من مدخلات التخصيص. وهذه ميزة ملائمة توفّر عليك عناء كتابة رمز لإنشاء Allocation بشكل صريح ونسخ البيانات إليه من مصفوفة Java. ومع ذلك، فإنّ استخدام الطريقة 3 بدلاً من الطريقة 1 لا يؤدي إلى تحسين أداء الرمز. بالنسبة إلى كل مصفوفة إدخال، تنشئ الطريقة 3 Allocation مؤقتًا أحادي الأبعاد مع النوع Element المناسب وتفعيل setAutoPadding(boolean)، كما تنسخ المصفوفة إلى Allocation كما لو كانت تستخدم الطريقة 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 الخاصة بدالة outconverter. ما لم يكن resultType نوعًا غير موقّع (عدد قياسي أو متّجه أو مصفوفة)، يكون javaResultType هو نوع Java المقابل مباشرةً. إذا كان resultType نوعًا غير موقّع وكان هناك نوع Java موقّع أكبر، سيكون javaResultType هو نوع Java الموقّع الأكبر هذا، وإلا سيكون نوع Java المقابل مباشرةً. مثلاً:

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

javaFutureType هو نوع النتيجة المستقبلية المقابل لـ resultType في دالة outconverter.

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

مثلاً:

Kotlin

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

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

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

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

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

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

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

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

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

Java

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

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

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

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

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

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

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

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

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

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