การสร้างแคชท่อส่งข้อมูลที่แข็งแกร่งด้วย Vulkan

มีแนวคิดใหม่ ๆ มากมายที่ต้องเรียนรู้เมื่อสร้างตัวเรนเดอร์ Vulkan บางแนวคิดนั้นง่ายต่อการจัดการมากกว่าแนวคิดอื่น ๆ และหนึ่งในส่วนเสริมที่ตรงไปตรงมาที่สุดคือแคชของไพพ์ไลน์ เพื่อให้การสร้างไพพ์ไลน์มีประสิทธิภาพสูงสุด คุณจำเป็นต้องสร้างแคชของไพพ์ไลน์และใช้มันทุกครั้งที่คุณต้องการสร้างไพพ์ไลน์ใหม่ เพื่อให้แน่ใจว่าการทำงานของแอปพลิเคชันของคุณในครั้งต่อ ๆ ไปไม่ต้องเสียเวลาในการคอมไพล์ไมโครโค้ดของเชดเดอร์ซ้ำ ๆ คุณจำเป็นต้องบันทึกข้อมูลแคชของไพพ์ไลน์ลงในไฟล์ จากนั้นโหลดข้อมูลนี้เมื่อแอปพลิเคชันของคุณเริ่มทำงานในครั้งถัดไป มันจะยากแค่ไหนกันเชียว?
ค่อนข้างยากทีเดียว
มีอะไรอยู่ในแคชของท่อส่งข้อมูล?
ข้อมูลแคชของท่อส่งข้อมูลเป็นบล็อบที่ไม่โปร่งใส (ส่วนใหญ่) คุณสร้างวัตถุ VkPipelineCache โดยอาจให้บล็อบเริ่มต้นเพื่อเริ่มต้นด้วย และจากนั้นในบางจุดคุณสามารถดึงบล็อบข้อมูลจากวัตถุนี้ได้
แม้ว่าเราจะไม่รู้อะไรมากเกี่ยวกับเนื้อหาของไฟล์ blob นอกจากการอ่านซอร์สโค้ดของไดรเวอร์กราฟิก1 แต่ข้อมูลแคชของ pipeline นั้นรับประกันว่าจะเริ่มต้นด้วยโครงสร้างที่ระบุอุปกรณ์และมีลักษณะประมาณนี้:
struct VkPipelineCacheHeaderOne<br>
{
uint32_t length; // == sizeof(VkPipelineCacheHeaderOne)
uint32_t version; // == VK_PIPELINE_CACHE_HEADER_VERSION_ONE
uint32_t vendorID;
uint32_t deviceID;
uint8_t uuid[VK_UUID_SIZE];
}; ส่วนหัวจะตามมาด้วยข้อมูลเฉพาะไดรเวอร์ซึ่งโดยทั่วไปจะประกอบด้วยชิ้นส่วนของไมโครโค้ดเชดเดอร์ (รูปแบบของไมโครโค้ดจะขึ้นอยู่กับ GPU) และข้อมูลเสริมที่อาจประกอบด้วยโครงสร้างที่กำหนดโดยไดรเวอร์ซึ่งสามารถกำหนดได้ตามต้องการ ไดรเวอร์บางตัวจะปฏิบัติต่อข้อมูลก้อนนี้เสมือนเป็นสตรีมไฟล์ที่มีโครงสร้างและอ่านข้อมูลจากมัน บางไดรเวอร์จะเก็บโครงสร้างดิบที่กำหนดไว้ในซอร์สโค้ดของไดรเวอร์ไว้ในก้อนข้อมูลนั้นและใช้การแปลงชนิดข้อมูลแบบ memcpy หรือการแปลงค่าชี้เพื่อนำทางข้อมูล ไม่จำเป็นต้องกล่าวว่าการอัปเดตไดรเวอร์อาจทำให้วิธีการจัดเก็บข้อมูลนั้นใช้ไม่ได้
ในทางทฤษฎี แอปพลิเคชันเพียงแค่ต้องใช้ vkGetPipelineCacheData เพื่อดึงข้อมูล blob หลังจากที่แอปพลิเคชันเข้าสู่สถานะคงที่ (เช่น ก่อนที่แอปพลิเคชันจะสิ้นสุดการทำงาน...) จากนั้นบันทึก blob ลงในไฟล์ และส่งต่อ blob นี้โดยใช้ VkPipelineCacheCreateInfo::pInitialData เมื่อสร้างแคชของ pipeline ในการทำงานครั้งถัดไป หากเนื้อหาของบล็อบไม่ทำงานกับเวอร์ชันปัจจุบันของไดรเวอร์ — อาจเป็นเพราะไดรเวอร์ได้รับการอัปเดต หรือผู้ใช้เปลี่ยนไปใช้ GPU ตัวอื่น — ไดรเวอร์ควรละเว้นข้อมูลเริ่มต้นและสร้างแคชของท่อประมวลผลว่างเปล่า
แต่ทฤษฎีกับการปฏิบัติจริงนั้นแตกต่างกันเล็กน้อย กฎทั่วไปในการปฏิบัติคือคนขับจะสามารถจัดการกับกลุ่มข้อมูลที่เหมือนกันได้อย่างถูกต้องเฉพาะกลุ่มข้อมูลที่เหมือนกันกับที่ผู้ขับขี่คนเดียวกันเคยให้แอปพลิเคชันของคุณมาก่อนเท่านั้น ซึ่งเป็นจุดเริ่มต้นของปัญหา
คนขับเป็นคนเดียวกันหรือไม่?
ข้อกำหนดนี้ตั้งสมมติฐานว่าแคชไม่สามารถใช้งานร่วมกันได้ระหว่างอุปกรณ์ต่าง ๆ (ซึ่งเป็นเหตุผลที่ vendorID และ deviceID ปรากฏอยู่ในส่วนหัว) และอาศัยไดรเวอร์ในการสร้าง UUID ของ pipeline (ซึ่งเป็น GUID ขนาด 16 ไบต์) ที่ระบุชุดปัจจัยทั้งหมดอย่างถูกต้องซึ่งนำไปสู่การตีความ blob ของแคช pipeline ได้อย่างถูกต้อง — คุณสามารถนึกถึงสิ่งนี้ได้ว่าเป็นหมายเลขเวอร์ชันของรูปแบบแคช pipeline ในระหว่างการอัปเกรดไดรเวอร์ ตัวอย่างเช่น อาจเป็นไปได้ว่ารูปแบบแคชของพายป์ไลน์ไม่ได้รับการอัปเดต ซึ่งในกรณีนี้ UUID โดยทั่วไปไม่ควรเปลี่ยนแปลง และแอปพลิเคชันไม่จำเป็นต้องคอมไพล์เชดเดอร์ใหม่ทั้งหมด
อย่างไรก็ตาม ผู้ขับขี่ในสภาพการใช้งานจริงมักมีปัญหาอยู่สองประเภท
ผู้ขับขี่บางราย (ที่มีอายุมากกว่า) อาจละเลยการตรวจสอบ UUID อย่างถูกต้อง ส่งผลให้ในระหว่างการอัปเดตไดร์เวอร์ แอปพลิเคชันอาจพยายามส่งบล็อบที่มี UUID ที่ล้าสมัยไปยังไดร์เวอร์ ไดร์เวอร์จะพยายามตีความข้อมูลนี้เป็นข้อมูลล่าสุด และส่งผลให้ vkCreatePipelineCache อาจเกิดการขัดข้อง โปรดทราบว่าโดยทั่วไป vkCreatePipelineCache ไม่รับประกันว่าจะยอมรับข้อมูลที่ไม่รู้จักและสามารถจัดการได้อย่างถูกต้อง
ผู้ขับขี่บางคน รวมถึงผู้ขับขี่ที่เพิ่งติดตั้งไม่นานนี้ อาจละเลยการอัปเดต UUID ในการอัปเดตไดร์เวอร์ที่ทำให้ไฟล์ไบนารีของ shader pipeline ไม่สามารถใช้งานร่วมกันได้ ซึ่งอาจเกิดขึ้นระหว่างการอัปเดตเวอร์ชันของไดร์เวอร์ (แม้ว่าจะเกิดขึ้นน้อยมาก) หรือ (สิ่งที่เกิดขึ้นได้โดยง่ายบนไดร์เวอร์ปัจจุบันของผู้ผลิตอย่างน้อยหนึ่งราย) ระหว่างไฟล์ไบนารีของไดร์เวอร์ที่สร้างจากเวอร์ชันเดียวกันแต่สำหรับ ABI ที่ต่างกัน หากไดรเวอร์ 32 บิตและไดรเวอร์ 64 บิตที่จัดส่งในระบบเดียวกันมี UUID ของท่อประมวลผลเดียวกัน การบันทึกแคชจากแอปพลิเคชันเวอร์ชัน 32 บิตและโหลดจากเวอร์ชัน 64 บิตอาจทำให้ไดรเวอร์เกิดข้อขัดข้องได้ — ซึ่งเป็นสิ่งที่เกิดขึ้นเมื่อคุณจัดส่งแอปพลิเคชันเวอร์ชัน 32 บิตและอัปเดตเป็นเวอร์ชัน 64 บิตตามแนวทางของ Google
ข้อมูลเหมือนกันหรือไม่?
ตอนนี้ที่เราทราบแล้วว่าอะไรรอเราอยู่เมื่อพูดถึงการตรวจสอบความถูกต้องของส่วนหัว ขั้นตอนต่อไปคือการตรวจสอบความถูกต้องของข้อมูล หลังจากเรียกใช้ vkGetPipelineCacheData แล้ว แอปพลิเคชันจะบันทึก blob และโหลด blob เดียวกันนี้อีกครั้งในครั้งถัดไป
ปรากฏว่าการบันทึกข้อมูลลงในไฟล์นั้นแทบจะเป็นไปไม่ได้เลยที่จะทำได้อย่างมีประสิทธิภาพ ปัญหาเกี่ยวกับระบบไฟล์และความเสถียรของกระบวนการอาจนำไปสู่ไฟล์ที่เขียนไม่สมบูรณ์ มีบางส่วนที่เต็มไปด้วยเลขศูนย์ (หรือแม้กระทั่งขยะข้อมูล) หรือ (ในกรณีพิเศษ) ถูกสร้างขึ้นแต่มีขนาดเป็นศูนย์ บนมือถือ อาจมีความซับซ้อนเพิ่มขึ้นเนื่องจากแอปพลิเคชันอาจถูกยกเลิกการใช้งานอย่างกะทันหันโดยผู้ใช้หรือระบบปฏิบัติการ ณ จุดเวลาใด ๆ ก็ตาม ซึ่งสิ่งนี้เกิดขึ้นน้อยกว่าบนเดสก์ท็อป บนระบบแอนดรอยด์ ยังเป็นเรื่องปกติที่จะใช้แอปพลิเคชันแบบหลายกระบวนการ (หลายกิจกรรม) และหากโค้ดสำหรับแคชในไพป์ไลน์ของคุณทำงานในทั้งสองกระบวนการและใช้ไฟล์ผลลัพธ์เดียวกัน ปัญหาเหล่านี้ก็จะยิ่งยากขึ้นในการแก้ไข
เหตุผลที่ไฟล์ขนาดศูนย์มีความน่าสนใจเป็นพิเศษก็เพราะว่า มีเวอร์ชันไดรเวอร์อย่างน้อยหนึ่งเวอร์ชันที่เราเคยพบ ซึ่งเมื่อส่งค่า pInitialData และ initialDataSize == 0 ที่ไม่ใช่ค่า NULL จะทำให้เกิดข้อผิดพลาดระหว่างการสร้างแคชในท่อประมวลผล ซึ่งนำไปสู่ข้อควรระวังสุดท้าย
การจัดการข้อผิดพลาดเป็นเรื่องยาก
แม้ว่าข้อกำหนดจะระบุว่า vkCreatePipelineCache ควรจะทำงานได้เกือบตลอดเวลา ยกเว้นในกรณีที่หน่วยความจำหมด แต่ข้อความเช่นนี้ในข้อกำหนดมักจะไม่ถูกต้องเสมอไป เมื่อสร้างแคชของท่อประมวลผล ไดรเวอร์ควรละเว้นข้อมูลเริ่มต้นหากข้อมูลนั้นไม่เข้ากัน ซึ่งอาจเกิดขึ้นได้หากข้อมูลมีขนาดเป็นศูนย์ หาก UUID ที่เก็บไว้ไม่ตรงกับ UUID ที่คาดหวัง หรือหากการแปลงข้อมูลกลับเป็นรูปแบบเดิมล้มเหลวด้วยเหตุผลอื่นใดก็ตาม ไดรเวอร์บางตัวอาจล้มเหลวในการสร้างแคชของท่อประมวลผลแทน
ผู้ใช้ไม่ได้เป็นฝ่ายผิดอย่างแน่นอน ดังนั้นการยกเลิกแอปพลิเคชันจึงไม่เหมาะสม แม้ว่าจะสามารถดำเนินการต่อได้โดยไม่มีแคชของไพพ์ไลน์โดยทั่วไป แต่นั่นมักเป็นความคิดที่แย่มาก เพราะนั่นหมายความว่าไพพ์ไลน์แต่ละตัวจะต้องถูกคอมไพล์ใหม่ทั้งหมด นั่นคือ แคชของไพพ์ไลน์มีประโยชน์แม้ว่าจะไม่ได้ถูกซีเรียลไลซ์ไปยังดิสก์ เพราะมันช่วยให้ไดรเวอร์สามารถแคชผลลัพธ์ของการคอมไพล์ข้ามอ็อบเจ็กต์ของไพพ์ไลน์ในหน่วยความจำได้
ทั้งหมดนี้ย่อมนำไปสู่...
มันไม่ใช่ความหวาดระแวงถ้าพวกเขาตั้งใจจะเล่นงานคุณจริงๆ
… ทางออก เมื่อทำการจัดเก็บข้อมูลแคชของไปป์ไลน์เป็นลำดับไปยังไฟล์ เราจะใช้ส่วนหัวที่บรรจุข้อมูลเพียงพอเพื่อให้สามารถตรวจสอบความถูกต้องของข้อมูลได้ โดยมีข้อมูลแคชของไปป์ไลน์ตามหลังมาทันที:
struct PipelineCachePrefixHeader<br>
{<br>
uint32_t magic; // an arbitrary magic header to make sure this is actually our file<br>
uint32_t dataSize; // equal to *pDataSize returned by vkGetPipelineCacheData<br>
uint64_t dataHash; // a hash of pipeline cache data, including the header<br>
uint32_t vendorID; // equal to VkPhysicalDeviceProperties::vendorID<br>
uint32_t deviceID; // equal to VkPhysicalDeviceProperties::deviceID<br>
uint32_t driverVersion; // equal to VkPhysicalDeviceProperties::driverVersion<br>
uint32_t driverABI; // equal to sizeof(void*)<br>
uint8_t uuid[VK_UUID_SIZE]; // equal to VkPhysicalDeviceProperties::pipelineCacheUUID<br>
};แฮชของข้อมูลแคชของท่อส่งข้อมูลจะช่วยให้เราสามารถตรวจสอบความถูกต้องของข้อมูลได้ เพื่อลดโอกาสที่ข้อผิดพลาด I/O จะทำให้เกิดปัญหาความถูกต้องของข้อมูล เราจึงสร้างไฟล์ชั่วคราวและเขียนส่วนหัวนี้ลงในไฟล์ ตามด้วยข้อมูลแคชของท่อส่งข้อมูล จากนั้นย้ายไฟล์ไปยังตำแหน่งเป้าหมายโดยใช้การเปลี่ยนชื่อไฟล์
เมื่อทำการโหลดแคชของท่อส่งข้อมูล เราจะอ่านส่วนหัว อ่านข้อมูล ตรวจสอบความถูกต้องของข้อมูลที่อ่านโดยใช้ dataSize และ dataHash จากนั้นตรวจสอบว่าข้อมูลสามารถส่งต่อไปยังไดรเวอร์ได้อย่างปลอดภัยโดยการเปรียบเทียบฟิลด์ที่เหลืออยู่กับคุณสมบัติของอุปกรณ์
หากข้อมูลถูกต้อง จะมีการเรียกใช้ฟังก์ชัน vkCreatePipelineCache โดยใช้ข้อมูลเริ่มต้นที่ถูกต้อง ที่สำคัญคือ หากการเรียกนี้ล้มเหลว แสดงว่าไดรเวอร์ได้ดำเนินการตรวจสอบเพิ่มเติมที่ตรรกะของเราไม่ได้ตรวจพบด้วยตัวเอง ดังนั้นแทนที่จะดำเนินการต่อโดยไม่มีแคชของพายป์ไลน์ ในกรณีนี้เราจะสร้างแคชของพายป์ไลน์ว่างโดยการเรียกใช้ฟังก์ชัน vkCreatePipelineCache อีกครั้งโดยไม่มีข้อมูลเริ่มต้น
เรายังสร้างแคชท่อส่งข้อมูลว่างหากไม่พบไฟล์แคชท่อส่งข้อมูลหรือตรรกะการตรวจสอบของเราจัดประเภทข้อมูลว่าไม่สามารถใช้งานได้
หมายเหตุ: เนื่องจากเราได้รวม driverVersion ไว้ในส่วนหัว การอัปเดตไดรเวอร์ใดๆ จะทำให้แคชของไปป์ไลน์ถูกสร้างใหม่ เราได้รวมการตรวจสอบนี้ไว้เพราะจะช่วยขจัดปัญหาที่ UUID ของแคชไปป์ไลน์ไม่ได้รับการอัปเดตแม้ว่าควรจะอัปเดตก็ตาม โดยปกติแล้ว driverVersion จะได้รับการอัปเดตเป็นส่วนหนึ่งของกระบวนการสร้าง ในขณะที่การอัปเดต UUID มักจะต้องทำด้วยตนเอง สำหรับแอปพลิเคชันที่มุ่งเน้นการใช้งานบนเดสก์ท็อปเท่านั้น สิ่งนี้อาจรุนแรงเกินไป — โดยทั่วไปแล้ว ไดรเวอร์สำหรับเดสก์ท็อปมักจะมีการจัดการความถูกต้องของแคชในกระบวนการได้ดีกว่า ดังนั้นคำแนะนำเหล่านี้จึงอาจไม่เหมาะสมทั้งหมด
สรุป
น่าเสียดายที่ไดรเวอร์ Vulkan ไม่ได้ถูกต้องเสมอไปและไม่ได้ปฏิบัติตามข้อกำหนดอย่างเคร่งครัดเสมอไป ข้อมูลแคชของท่อส่งข้อมูลเป็นส่วนที่เปราะบางเป็นพิเศษของตัวเรนเดอร์ Vulkan เนื่องจากการจัดการ I/O นั้นเป็นเรื่องที่ท้าทาย และมักมีการตรวจสอบความถูกต้องเพียงเล็กน้อยหรือไม่มีเลยในไดรเวอร์ อย่างไรก็ตาม ด้วยการตรวจสอบความถูกต้องจากฝั่งแอปพลิเคชันที่เพียงพอ คุณสามารถขจัดปัญหาความเสถียรที่เกิดจากการจัดการแคชของท่อส่งข้อมูลในทางปฏิบัติได้ — เพียงแค่ต้องใช้ความพยายามเท่านั้น
- ซึ่งคุณสามารถทำได้แน่นอนในปัจจุบัน! ตัวอย่างเช่น นี่คือตัวอย่างการใช้งาน vkGetPipelineCacheData สำหรับ radv ↩
- ส่วนที่เหลือของบทความนี้อ้างอิงจากประสบการณ์ในการพัฒนาและปล่อย Roblox client บน Android ที่รองรับ Vulkan อย่างต่อเนื่อง พร้อมทั้งรับมือกับการอัปเดตระบบปฏิบัติการ Android, การอัปเดตไดรเวอร์ และปัญหาต่าง ๆ ที่เกิดขึ้นจากการใช้งานไดรเวอร์ Vulkan ทั้งรุ่นแรกและรุ่นปัจจุบันจากผู้ผลิตชั้นนำทุกเจ้า ↩
- ในทางทฤษฎี การเปลี่ยนชื่อควรจะเป็นกระบวนการที่สมบูรณ์ในตัวเอง แต่ในทางปฏิบัติ ความหมายและการรับประกันที่แน่นอนจะแตกต่างกันไปตามระบบไฟล์ การใช้แฮชเป็นวิธีที่มีประโยชน์ในการเปรียบเทียบอย่างแม่นยำ ↩
- ขึ้นอยู่กับแอปพลิเคชัน คุณอาจต้องการใช้ชื่อไฟล์ที่แตกต่างกันโดยอิงตาม เช่น vendorID หรือ driverABI; สิ่งนี้น่าสนใจมากขึ้นบนเดสก์ท็อปและน่าสนใจน้อยลงบนมือถือ ↩
เผยแพร่ครั้งแรกที่: https://zeux.io/2019/07/17/serializing-pipeline-cache/
อาร์เซนี คาปูลคิน ได้ทำงานเกี่ยวกับเทคโนโลยีเกมมาเป็นเวลาสิบปีแล้ว เขาได้ทำงานเกี่ยวกับการเรนเดอร์, การจำลองฟิสิกส์, ระบบรันไทม์ของภาษา, การประมวลผลแบบมัลติเธรด และอีกมากมายหลายสาขา แต่เขายังคงค้นพบปัญหาที่น่าตื่นเต้นในด้านการพัฒนาเกมที่ต้องการการคิดในระดับต่ำอยู่ตลอดเวลา หลังจากที่เขาช่วยส่งมอบเกมหลายเกมบน PS3 รวมถึงเกม FIFA หลายภาค เขาได้เข้าร่วมกับ Roblox ในปี 2012 และทำงานกับเอนจินภายในองค์กรมาจนถึงปัจจุบัน โดยช่วยเหลือให้นักพัฒนาเกมรุ่นใหม่สามารถทำตามความฝันของพวกเขาได้
ทั้งบริษัท Roblox Corporation และบล็อกนี้ไม่ได้รับรองหรือสนับสนุนบริษัทหรือบริการใด ๆ ทั้งสิ้น นอกจากนี้ ไม่มีการรับประกันหรือคำมั่นสัญญาใด ๆ เกี่ยวกับความถูกต้อง ความน่าเชื่อถือ หรือความสมบูรณ์ของข้อมูลที่ปรากฏในบล็อกนี้
©2021 บริษัท Roblox Corporation. Roblox, โลโก้ Roblox และ Powering Imagination เป็นเครื่องหมายการค้าจดทะเบียนและไม่ได้จดทะเบียนของเราในสหรัฐอเมริกาและประเทศอื่น ๆ


