المخزن العمق الهرمي


مراجعة قصيرة


المخزن المؤقت العمق الهرمي هو مخزن مؤقت متعدد المستويات (Z- المخزن المؤقت) يستخدم كبنية تسريع لاستعلامات العمق. كما هو الحال في سلاسل mip النسيجية ، عادة ما تكون أحجام كل مستوى ناتجة عن القسمة بمقدار اثنين على أحجام المخزن المؤقت للدقة الكاملة. في هذه المقالة ، سأتحدث عن طريقتين لإنشاء مخزن مؤقت لعمق هرمي من مخزن مؤقت كامل الدقة.

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

ثم ، في الحالات التي يتطلب فيها مستوى واحد فقط من الاختزال ، سأوضح كيفية إنشاء هذا المستوى من خلال استدعاء واحد لجهاز تظليل الحوسبة الذي يستخدم العمليات الذرية في الذاكرة المشتركة لمجموعة العمل. بالنسبة إلى طلبي ، حيث يلزم دقة 1/16 × 1/16 فقط (مستوى mip 4) ، فإن الطريقة مع جهاز تظليل حسابي تكون أسرع 2-3 مرات من النهج المعتاد مع اختزال سلسلة mip في عدة تمريرات.

المقدمة


الأعماق الهرمية (تسمى أيضًا Hi-Z) هي تقنية تستخدم غالبًا في الرسومات ثلاثية الأبعاد. يتم استخدامه لتسريع تقليم الهندسة غير المرئية (إعدام الإطباق) (في وحدة المعالجة المركزية ، وكذلك في GPU ) ، وحساب الانعكاسات في مساحة الشاشة ، والضباب الحجمي ، وأكثر من ذلك بكثير.

بالإضافة إلى ذلك ، غالبًا ما يتم تنفيذ وحدات معالجة الرسوميات Hi-Z كجزء من خط أنابيب التنقيط. تتيح لك عمليات البحث السريع Hi-Z في ذاكرة التخزين المؤقت على الشريحة تخطي البلاط المجزأ تمامًا إذا كانت مغطاة تمامًا بدائيين سابقين.

تتمثل الفكرة الأساسية لـ Hi-Z في تسريع عمليات الاستعلامات العميقة من خلال القراءة من المخازن المؤقتة ذات الدقة المنخفضة. هذا أسرع من قراءة أعماق الدقة الكاملة من المخزن المؤقت ، لسببين:

  1. يمكن استخدام texel واحد (أو بضع texels) من المخزن المؤقت منخفض الدقة كقيمة تقريبية لعدد وافر من texels من المخزن المؤقت عالي الدقة.
  2. يمكن أن يكون المخزن المؤقت منخفض الدقة صغيرًا بما فيه الكفاية ومخزنًا مؤقتًا ، مما يسرع إلى حد كبير من تنفيذ عمليات البحث (خاصة مع الوصول العشوائي).

محتوى مستويات عازلة مرحبا-Z-عينات أسفل يعتمد على كيفية استخدامها (سواء المخزن المؤقت عمق سيتم "المقلوب" ، ما هي أنواع من الطلبات ينبغي أن تستخدم). بشكل عام ، texel في مخازن مستوى التخزين المؤقت Hi-Z minأو maxجميع texels المقابلة لها في المستوى السابق. أحيانا قيم minو يتم تخزينها في نفس الوقت max. يتم استخدام القيم المتوسطة البسيطة (التي غالبًا ما تستخدم في مستويات mip من القوام العادي) بشكل غير متكرر لأنها نادرًا ما تكون مفيدة لمثل هذه الأنواع من الاستعلامات.

غالبًا ما يتم طلب المخازن المؤقتة Hi-Z على الفور تقريبًا عند الخروج لتجنب المزيد من المعالجة وعمليات البحث الأكثر دقة في المخزن المؤقت للدقة الكاملة. على سبيل المثال ، إذا قمنا بتخزين القيمmaxبالنسبة إلى المخزن المؤقت للعمق غير المقلوب (حيث كلما زادت قيمة العمق ، كلما كان الكائن أبعد) ، يمكننا بسرعة تحديد ما إذا كان موضع معين في مساحة الشاشة مغطى بمخزن مؤقت للعمق (إذا كانت إحداثياته ​​Z> هي القيمة (الحد الأقصى) المخزنة في بعض مستوى أعلى (أي دقة أقل) للمخزن المؤقت Hi-Z).

يرجى ملاحظة أنني استخدمت العبارة "بالضبط": إذا كانت الإحداثيات Z <= القيمة المستلمة (الحد الأقصى) ، فمن غير المعروف ما إذا كان تداخل المخزن المؤقت الخاص بها. في بعض التطبيقات ، في حالات عدم اليقين ، قد يكون من الضروري البحث في المخزن المؤقت عن أعماق الدقة الكاملة ؛ في حالات أخرى ، هذا غير مطلوب (على سبيل المثال ، إذا كانت الحسابات غير الضرورية فقط على المحك ، وليس العرض الصحيح).

طلبي: تقديم الجسيمات في تظليل حسابي


لقد واجهت الحاجة إلى استخدام Hi-Z عند تنفيذ عرض الجسيمات في تظليل حسابي في محرك تطبيق PARTICULATE VR الخاص بي . نظرًا لأن تقنية العرض هذه لا تستخدم التنقيط مع الوظائف الثابتة ، فإنها تحتاج إلى استخدام فحص العمق الخاص بها لكل جسيم بحجم بكسل واحد. وبما أن الجسيمات لا يتم فرزها بأي شكل من الأشكال ، فإن الوصول إلى المخزن المؤقت للعمق يكون (في أسوأ الحالات) عشوائيًا تقريبًا.

إن عمليات البحث على نسيج ذي وصول عشوائي بملء الشاشة هي الطريق إلى الأداء الضعيف. لتقليل الحمل ، أقوم أولاً بالبحث عن الأعماق في المخزن المؤقت للعمق المخفض بدقة 1/16 × 1/16 من الأصل. يحتوي هذا المخزن المؤقت على قيم عمق.min، والذي يسمح لتظليل العرض الحسابي للغالبية العظمى من الجسيمات المرئية بتخطي اختبار عمق الدقة الكاملة. (إذا كان عمق الجسيمات <الحد الأدنى للعمق المخزن في المخزن المؤقت بدقة أقل ، فإننا نعلم أنه مرئي تمامًا. إذا كان>> دقيقة ، فإننا نحتاج إلى التحقق من المخزن المؤقت لعمق كامل الدقة.)

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

نظرًا لحقيقة أن البحث يتم إجراؤه أولاً في المخزن المؤقت لأعماق الدقة الأقل (كما ذكر أعلاه) ، يتم تقليل وقت عرض الجسيمات بحد أقصى 35٪مقارنة بالحالة عندما يتم إجراء البحث فقط في المخزن المؤقت للدقة الكاملة. لذلك ، بالنسبة لطلبي ، Hi-Z مفيد جدًا.

سنلقي الآن نظرة على طريقتين لتوليد مخزن مؤقت لعمق هرمي.

التقنية 1: إنشاء سلسلة Mip كاملة


في العديد من تطبيقات Hi-Z ، يلزم إنشاء سلسلة كاملة لذاكرة التخزين المؤقت للعمق. على سبيل المثال ، عند تنفيذ عملية إعدام الإطباق باستخدام Hi-Z ، يتم عرض حجم المحيط في مساحة الشاشة ويتم استخدام الحجم المسقط لتحديد مستوى mip المناسب (بحيث يتم تضمين عدد ثابت من texels في كل اختبار تداخل).

عادة ما يكون إنشاء سلسلة mip من المخزن المؤقت لعمق الدقة الكاملة مهمة بسيطة - لكل texel في المستوى N نأخذ max(أو min، أو كلاهما) 4 texels المقابلة في المستوى الذي تم إنشاؤه سابقًا N-1. نحن نجري تمريرات متتالية (في كل مرة نخفض الحجم إلى النصف) حتى نحصل على آخر مستوى mip 1x1 في الحجم.

ومع ذلك ، في حالة المخازن المؤقتة للعمق ، التي لا تتوافق أحجامها مع سلطات اثنين ، فإن كل شيء أكثر تعقيدًا. نظرًا لأن Hi-Z للمخازن المؤقتة للعمق يتم غالبًا بناء على دقة الشاشة القياسية (التي نادرًا ما تكون قوة من اثنين) ، فنحن بحاجة إلى إيجاد حل موثوق لهذه المشكلة.

دعنا أولاً نقرر ما الذي سيعني قيمة كل مخزن مؤقت لعمق العمق texel. في هذه المقالة ، سنفترض أن سلسلة mip تخزن القيم min. يجب أن تستخدم عمليات البحث في العمق تصفية أقرب الجيران ، لأن استيفاء القيم لا minفائدة لنا وسيضر بالطبيعة الهرمية لسلسلة الأعماق التي تم إنشاؤها.

إذن ، ما الذي ستعنيه بالضبط قيمة texel الفردية عند مستوى mip التي حصلنا عليها؟ يجب أن يكون هذا هو الحد الأدنى للقيمة (min) من جميع أعمدة texels للمخزن المؤقت لعمق كامل الشاشة الذي يشغل نفس المساحة في مساحة إحداثيات النسيج (المقيسة).

وبعبارة أخرى ، إذا كان هناك إحداثية منفصلة للنسيج (في الفاصل الزمني ) تم تعيينه (من خلال تصفية أقرب الجيران) إلى texel فردي من المخزن المؤقت للدقة الكاملة ، ثم يجب اعتبار هذا texel من الدقة الكاملة كمرشح للقيمةالمحسوبة لـ texel عند كل مستوى mip لاحق يتم فيه تعيين نفس الإحداثيات القوام. إذا كانت هذه المراسلات مضمونة ، فسنكون على يقين من أن عملية البحث عند مستويات mip عالية لن تُرجع قيمة العمق> قيمة texel في نفس إحداثيات النسيج التي تتوافق مع المخزن المؤقت للدقة الكاملة (المستوى 0). في حالة N منفصلة ، يتم الحفاظ على هذا الضمان لجميع المستويات التي دونها (<N).[0,1]2min



بالنسبة للأبعاد الزوجية (وفي حالة المخازن المؤقتة ذات الدقة الكاملة ، والتي تمثل قوى ذات بعدين ، حتى الأبعاد في كل مستوى حتى آخر المستويات ، حيث تصبح الأبعاد تساوي 1) ، سيكون من السهل القيام بذلك. في حالة البعد الواحد ، للتكسل مع فهرسi في المستوى N ، نحتاج إلى أخذ texels عند المستوى N-1 مع المؤشرات2 و2i+1 وتجد قيمتهاmin. بمعنى آخرDN[i]=min(DN1[2i],DN1[2i+1]) . يمكننا أن نقارن مباشرة texels في نسبة "2 إلى 1" (وبالتالي حجم إحداثيات النسيج) ، لأن حجم كل مستوى أصغر مرتين بالضبط من المستوى السابق.


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

في حالة أحجام المستوى الفردي (والمخازن المؤقتة كاملة الدقة التي ليست قوة من اثنين سيكون لها مستوى واحد على الأقل بحجم فردي) كل شيء تزداد صعوبة. للمستوى N-1 من حجم فرديdimN1 حجم المستوى التالي (N) متساويًا، أيdimN=dimN12 . هذا يعني أنه لا يوجد لدينا الآن تعيين واضح 2 إلى 1 من texels من المستوى N-1 إلى texels من المستوى N. والآن يتم فرض حجم إحداثيات النسيج لكل texel على المستوى N على حجم3texels في المستوى N-1.dimN12




مثال على حجم المستوى الفردي: 7 texels من هذا المستوى يتم تخفيضه إلى 3 texels في المستوى التالي. يتم فرض أبعاد إحداثيات الملمس للنصوص الثلاث عالية المستوى على أحجام النصوص الثلاث من المستوى الأدنى.

بالتالي . وهذا يعني أن texel واحد على مستوى N-1 يؤثر أحيانًا على القيمةالمحسوبة لـ2texels على مستوى N. وهذا ضروري للحفاظ على المقارنة الموضحة أعلاه.DN[i]=min(DN1[2i],DN1[2i+1],DN1[2i+2])min

تم تقديم الوصف أعلاه في بُعد واحد فقط للبساطة. في بعدين ، إذا كان كلا البعدين لمستوى N-1 متساويين ، فسيتم تعيين منطقة texel 2x2 على مستوى N-1 إلى texel واحد على مستوى N. إذا كان أحد الأبعاد غريبًا ، فسيتم تعيين منطقة 2x3 أو 3x2 على مستوى N-1 على واحد texel at level N. إذا كان كلا البعدين غريبًا ، فيجب أيضًا أخذ texel "الزاوي" في الاعتبار ، أي أنه يتم مقارنة منطقة 3x3 عند المستوى N-1 مع texel واحد عند المستوى N.

مثال على الرمز


يطبق كود تظليل GLSL الموضح أدناه الخوارزمية التي وصفناها. يجب تنفيذه لكل mip لاحق ، بدءًا من المستوى 1 (المستوى 0 هو مستوى الدقة الكاملة).

uniform sampler2D u_depthBuffer;
uniform int u_previousLevel;
uniform ivec2 u_previousLevelDimensions;

void main() {
	ivec2 thisLevelTexelCoord = ivec2(gl_FragCoord);
	ivec2 previousLevelBaseTexelCoord = 2 * thisLevelTexelCoord;

	vec4 depthTexelValues;
	depthTexelValues.x = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord,
                                    u_previousLevel).r;
	depthTexelValues.y = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord + ivec2(1, 0),
                                    u_previousLevel).r;
	depthTexelValues.z = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord + ivec2(1, 1),
                                    u_previousLevel).r;
	depthTexelValues.w = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord + ivec2(0, 1),
                                    u_previousLevel).r;

	float minDepth = min(min(depthTexelValues.x, depthTexelValues.y),
                         min(depthTexelValues.z, depthTexelValues.w));

    // Incorporate additional texels if the previous level's width or height (or both) 
    // are odd. 
	bool shouldIncludeExtraColumnFromPreviousLevel = ((u_previousLevelDimensions.x & 1) != 0);
	bool shouldIncludeExtraRowFromPreviousLevel = ((u_previousLevelDimensions.y & 1) != 0);
	if (shouldIncludeExtraColumnFromPreviousLevel) {
		vec2 extraColumnTexelValues;
		extraColumnTexelValues.x = texelFetch(u_depthBuffer,
                                              previousLevelBaseTexelCoord + ivec2(2, 0),
                                              u_previousLevel).r;
		extraColumnTexelValues.y = texelFetch(u_depthBuffer,
                                              previousLevelBaseTexelCoord + ivec2(2, 1),
                                              u_previousLevel).r;

		// In the case where the width and height are both odd, need to include the 
        // 'corner' value as well. 
		if (shouldIncludeExtraRowFromPreviousLevel) {
			float cornerTexelValue = texelFetch(u_depthBuffer,
                                                previousLevelBaseTexelCoord + ivec2(2, 2),
                                                u_previousLevel).r;
			minDepth = min(minDepth, cornerTexelValue);
		}
		minDepth = min(minDepth, min(extraColumnTexelValues.x, extraColumnTexelValues.y));
	}
	if (shouldIncludeExtraRowFromPreviousLevel) {
		vec2 extraRowTexelValues;
		extraRowTexelValues.x = texelFetch(u_depthBuffer,
                                           previousLevelBaseTexelCoord + ivec2(0, 2),
                                           u_previousLevel).r;
		extraRowTexelValues.y = texelFetch(u_depthBuffer,
                                           previousLevelBaseTexelCoord + ivec2(1, 2),
                                           u_previousLevel).r;
		minDepth = min(minDepth, min(extraRowTexelValues.x, extraRowTexelValues.y));
	}

	gl_FragDepth = minDepth;
}

العيوب في هذا الرمز


أولاً ، في حالة المخازن المؤقتة لعمق كامل الدقة التي يزيد بُعد أحد أبعادها عن ضعف حجم بُعد آخر ، texelFetchقد تتجاوز مؤشرات الاستدعاء u_depthBuffer. (في مثل هذه الحالات ، يتحول البُعد الأصغر إلى 1 قبل الآخر.) كنت أرغب في استخدامه في هذا المثال texelFetch(باستخدام إحداثيات عدد صحيح) ، بحيث يكون ما كان واضحًا قدر الإمكان ، ولم يواجه شخصيًا مثل هذه المخازن المؤقتة العريضة / العالية العمق. إذا واجهت مثل هذه المشاكل ، يمكنك تحديد ( clamp) texelFetchالإحداثيات المرسلة أو استخدام textureالإحداثيات المقيسة للنسيج (في العينة ، قم بتعيين حد على الحافة). عند الحساب minأو maxيجب دائمًا مراعاة texel عدة مرات لوجود حالات الحدود.

ثانيًا ، على الرغم من أن المكالمات الأربعة الأولى texelFetchيمكن استبدالها بمكالمة واحدة textureGather، فإن هذا يعقد الأمور (حيث textureGatherلا يمكن تحديد مستوى mip) ؛ بالإضافة إلى ذلك ، لم ألاحظ زيادة في السرعة عند الاستخدام textureGather.

أداء


لقد استخدمت جهاز تظليل الشظايا أعلاه لإنشاء سلسلتي mip كاملتين لمخازن عمق عميقة (واحدة لكل عين) في محرك VR الخاص بي. في الاختبار ، كانت دقة كل عين هي 1648x1776 ، مما أدى إلى إنشاء 10 مستويات mip مخفضة إضافية (مما يعني 10 تمريرات). استغرق الأمر 0.25 مللي ثانية على NVIDIA GTX 980 و 0.30 مللي ثانية على AMD R9 290 لإنشاء سلسلة كاملة لكلتا العينين.



Mip- 4, 5 6, , . ( , , , .) Mip- 4 — , (103x111) 2.

mip-


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

لاحظ أنه ، مثل الطريقة السابقة ، تم تصميم هذه الطريقة البديلة للعمل مع المخازن المؤقتة ذات الدقة الكاملة التي لا تكون أحجامها من صلاحيتين (ولكن ، بالطبع ، تعمل بأحجام تساوي صلاحيتين).

في هذه الطريقة البديلة ، عند اختزال مستوى بعرض فردي (أو ارتفاع) بدلاً من إضافة عمود (أو صف) إضافي لكل مادة إخراج من المستوى (السفلي) السابق ، فإننا نجري هذه العملية فقط لمخرجات texels ذات الحد الأقصى للمؤشرات (texels "القصوى") ) الشيء الوحيد الذي يتغير في تظليل الأجزاء المعروض أعلاه هو تحديد القيم shouldIncludeExtraColumnFromPreviousLevelو shouldIncludeExtraRowFromPreviousLevel:

// If the previous level's width is odd and this is the highest-indexed "edge" texel for 
// this level, incorporate the rightmost edge texels from the previous level. The same goes 
// for the height. 
bool shouldIncludeExtraColumnFromPreviousLevel =
    (previousMipLevelBaseTexelCoords.x == u_previousLevelDimensions.x - 3);
bool shouldIncludeExtraRowFromPreviousLevel =
    (previousMipLevelBaseTexelCoords.y == u_previousLevelDimensions.y - 3);

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

عيب هذا النهج هو أنه يصبح من الصعب إجراء استعلامات عمق مستويات mip عالية. بدلاً من مجرد استخدام إحداثيات الملمس الطبيعية ، نحتاج أولاً إلى تحديد texel عالي الدقة المطابق لهذه الإحداثيات ، ثم نقل إحداثيات هذا texel إلى إحداثيات مستوى mip المقابل ، الذي يتم تنفيذ الطلب الخاص به.

يتم نقل مقتطف الشفرة أدناه من مساحة NDC إلى إحداثيات texel على مستوى mip:[1,1]2higherMipLevel

vec2 windowCoords = (0.5 * ndc.xy + vec2(0.5)) * textureSize(u_depthBuffer, 0);
// Account for texel centers being halfway between integers. 
ivec2 texelCoords = ivec2(round(windowCoords.xy - vec2(0.5)));
ivec2 higherMipLevelTexelCoords =
    min(texelCoords / (1 << higherMipLevel),
        textureSize(u_depthBuffer, higherMipLevel).xy - ivec2(1));

التقنية 2: قم بتوليد مستوى Hi-Z واحد باستخدام تظليل الحوسبة


يعد إنشاء سلسلة mip كاملة سريعًا جدًا ، لكنه أزعجني قليلاً أن تطبيقي يولد كل هذه المستويات ويستخدم واحدًا منها فقط (المستوى 4). بالإضافة إلى التخلص من هذا القصور الطفيف ، كنت أرغب أيضًا في معرفة مقدار كل شيء يمكن تسريعه إذا استخدمت مكالمة تظليل حاسوبية واحدة فقط لإنشاء المستوى الذي أحتاجه. (تجدر الإشارة إلى أن تطبيقي يمكن أن يتوقف عند المستوى 4 عند استخدام حل مع تظليل جزء تمرير متعدد ، لذلك في نهاية هذا القسم استخدمته كأساس لمقارنة أوقات التشغيل.)

في معظم تطبيقات Hi-Z ، يلزم مستوى عمق واحد فقط ، لذلك أجد هذا الموقف شائعًا. لقد كتبت تظليلًا حسابيًا لمتطلباتي الخاصة (توليد المستوى 4 ، الذي يحتوي على دقة 1/16 × 1/16 من الأصل). يمكن استخدام رمز مماثل لتوليد مستويات مختلفة.

جهاز التظليل الحسابي مناسب تمامًا لهذه المهمة ، لأنه يمكنه استخدام ذاكرة مجموعة العمل المشتركة لتبادل البيانات بين سلاسل العمليات. كل مجموعة عمل مسؤولة عن texel ناتج واحد (يتم تقليله باختزال اختزال المخزن المؤقت) ، وتتشارك خيوط مجموعة العمل في عمل حساب mintexels المقابلة ذات الدقة الكاملة ، ومشاركة النتائج من خلال الذاكرة المشتركة.

لقد جربت حلين رئيسيين يعتمدان على التظليل الحسابي. في البداية ، دعا كل خيط atomicMinإلى متغير ذاكرة مشترك واحد.

يرجى ملاحظة أنه نظرًا لأن المبرمجين لا يمكنهم (بدون امتدادات للأجهزة الخاصة بمصنع معين) إجراء عمليات ذرية على قيم غير صحيحة (ويتم تخزين أعماقي على النحو التالي float) ، هناك حاجة إلى بعض الحيل هنا. نظرًا لأن قيم الفاصلة العائمة غير السالبة لمعيار IEEE 754 تحتفظ بترتيبها عند معالجة وحدات البت الخاصة بها كقيم عدد صحيح غير موقعة ، يمكننا استخدام floatBitsToUintإحضار (باستخدام إعادة تفسير التفسير) لقيم العمق floatإلى uint، ثم استدعاء atomicMin(للتنفيذ بعد ذلك uintBitsToFloatللحصول على القيمة الدنيا النهائية uint) .

الحل الأكثر وضوحًا atomicMinهو إنشاء مجموعات مؤشر ترابط 16 × 16 حيث يتلقى كل مؤشر ترابط texel واحدًا ثم ينفذها atomicMinبقيمة في الذاكرة المشتركة. قارنت هذا النهج باستخدام كتل تيار أصغر (8x8 ، 4x8 ، 4x4 ، 2x4 ، 2x2) ، حيث يتلقى كل دفق منطقة texel ويحسب الحد الأدنى المحلي الخاص به ، ثم المكالمات atomicMin.

أسرع من كل هذه الحلول المختبرةatomicMinاتضح أن كل من NVIDIA و AMD لديهم حل مع كتل تيار 4x4 (حيث يتلقى كل تيار نفسه منطقة 4x4 texel). لا أفهم تمامًا لماذا تبين أن هذا الخيار هو الأسرع ، ولكن ربما يعكس حلاً وسطًا بين التنافس بين العمليات الذرية والحسابات في التدفقات المستقلة. من الجدير بالذكر أيضًا أن حجم مجموعة العمل 4 × 4 يستخدم 16 خيطًا فقط لكل التفاف / موجة (ومن الممكن أيضًا استخدام 32 أو 64) ، وهو أمر مثير للاهتمام. المثال أدناه ينفذ هذا النهج.

كبديل للاستخدام ، atomicMinحاولت إجراء تخفيض موازٍ باستخدام التقنيات المستخدمة في عرض NVIDIA الذي تم الاستشهاد به بنشاط. (الفكرة الأساسية هي استخدام مصفوفة ذاكرة مشتركة بنفس حجم عدد مؤشرات الترابط في مجموعة العمل ، وكذلكlog2(n)يمر سجل 2 ( ن ) لحساب مشترك متسلسلminلقيعان كل مؤشر ترابط حتى يتم الحصول على الحد الأدنى النهائي لمجموعة العمل بأكملها.)

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

مثال على الرمز


باستخدام هذه الطريقة ، فإن أسهل طريقة هي عدم محاولة الحفاظ على المراسلات في المساحة الطبيعية لإحداثيات النسيج بين texels ذات المخازن المؤقتة المخفّضة والدقة الكاملة. يمكنك ببساطة إجراء تحويلات من إحداثيات texel ذات الدقة الكاملة إلى إحداثيات texel ذات الدقة المنخفضة:

ivec2 reducedResTexelCoords = texelCoords / ivec2(downscalingFactor);

في حالتي (توليد ما يعادل مستوى mip 4) downscalingFactorهو 16.

كما ذكرنا أعلاه ، فإن جهاز تظليل الحوسبة GLSL هذا يطبق حلاً atomicMinبأحجام مجموعة عمل 4x4 ، حيث يتلقى كل خيط مساحة texel 4x4 من المخزن المؤقت للدقة الكاملة. المخزن المؤقت لعمق القيمة المخفّضة الناتج minهو 1/16 × 1/16 من حجم المخزن المؤقت كامل الدقة (يتم تقريبه لأعلى عندما لا يتم تقسيم أحجام الدقة الكاملة على 16 بالكامل).

uniform sampler2D u_inputDepthBuffer;
uniform restrict writeonly image2DArray u_outputDownsampledMinDepthBufferImage;
// The dimension in normalized texture coordinate space of a single texel in 
// u_inputDepthBuffer. 
uniform vec2 u_texelDimensions;

// Resulting image is 1/16th x 1/16th resolution, but we fetch 4x4 texels per thread, hence 
// the divisions by 4 here. 
layout(local_size_x = 16/4, local_size_y = 16/4, local_size_z = 1) in;

// This is stored as uint because atomicMin only works on integer types. Luckily 
// (non-negative) floats maintain their order when their bits are interpreted as uint (using 
// floatBitsToUint). 
shared uint s_workgroupMinDepthEncodedAsUint;

void main() {
	if (gl_LocalInvocationIndex == 0) {
        // Initialize to 1.0 (max depth) before performing atomicMin's. 
		s_workgroupMinDepthEncodedAsUint = floatBitsToUint(1.0);
	}

	memoryBarrierShared();
	barrier();

	// Fetch a 4x4 texel region per thread with 4 calls to textureGather. 'gatherCoords' 
    // are set up to be equidistant from the centers of the 4 texels being gathered (which 
    // puts them on integer values). In my tests textureGather was not faster than 
    // individually fetching each texel - I use it here only for conciseness. 
    // 
    // Note that in the case of the full-res depth buffer's dimensions not being evenly 
    // divisible by the downscaling factor (16), these textureGather's may try to fetch 
    // out-of-bounds coordinates - that's fine as long as the texture sampler is set to 
    // clamp-to-edge, as redundant values don't affect the resulting min. 

	uvec2 baseTexelCoords = 4 * gl_GlobalInvocationID.xy;
	vec2 gatherCoords1 = (baseTexelCoords + uvec2(1, 1)) * u_texelDimensions;
	vec2 gatherCoords2 = (baseTexelCoords + uvec2(3, 1)) * u_texelDimensions;
	vec2 gatherCoords3 = (baseTexelCoords + uvec2(1, 3)) * u_texelDimensions;
	vec2 gatherCoords4 = (baseTexelCoords + uvec2(3, 3)) * u_texelDimensions;

	vec4 gatheredTexelValues1 = textureGather(u_inputDepthBuffer, gatherCoords1);
	vec4 gatheredTexelValues2 = textureGather(u_inputDepthBuffer, gatherCoords2);
	vec4 gatheredTexelValues3 = textureGather(u_inputDepthBuffer, gatherCoords3);
	vec4 gatheredTexelValues4 = textureGather(u_inputDepthBuffer, gatherCoords4);

	// Now find the min across the 4x4 region fetched, and apply that to the workgroup min 
    // using atomicMin. 
	vec4 gatheredTexelMins = min(min(gatheredTexelValues1, gatheredTexelValues2),
                                 min(gatheredTexelValues3, gatheredTexelValues4));
	float finalMin = min(min(gatheredTexelMins.x, gatheredTexelMins.y),
                         min(gatheredTexelMins.z, gatheredTexelMins.w));
	atomicMin(s_workgroupMinDepthEncodedAsUint, floatBitsToUint(finalMin));

	memoryBarrierShared();
	barrier();

    // Thread 0 writes workgroup-wide min to image. 
	if (gl_LocalInvocationIndex == 0) {
		float workgroupMinDepth = uintBitsToFloat(s_workgroupMinDepthEncodedAsUint);

		imageStore(u_outputDownsampledMinDepthBufferImage,
		           ivec2(gl_WorkGroupID.xy),
                   // imageStore can only be passed vec4, but only a float is stored. 
				   vec4(workgroupMinDepth));
	}
}

أداء


لقد استخدمت التظليل الحسابي أعلاه لمعالجة المخزن المؤقت لعمق الدقة الكاملة بنفس الأبعاد التي تم استخدامها لإنشاء سلسلة mip الكاملة (1648x1776 مخازن لكل عين). يتم تشغيله في 0.12 مللي ثانية على NVIDIA GTX 980 و 0.08 مللي ثانية على AMD R9290. إذا قارنا بوقت إنشاء مستويات mip فقط 1-4 (0.22 مللي ثانية على NVIDIA ، 0.25 مللي ثانية AMD) ، ثم الحل مع تظليل الحسابية تبين أن 87٪ أسرع مع NVIDIA وحدات معالجة الرسومات و 197٪ أسرع من AMD وحدات معالجة الرسومات .

من حيث القيمة المطلقة ، فإن التسارع ليس كبيرًا جدًا ، ولكن كل 0.1 مللي ثانية مهم ، خاصة في VR :)

All Articles