#2 एक दुनिया, सेव, रीप्ले, और सेव फ़ॉर्मेट में छिपा एक बग
· Nick
पहली पोस्ट के बाद से कोर में चीज़ें रखने के लिए एक दुनिया आ गई है, उसे सँभालने के लिए फ़ाइलें, और यह साबित करने का तरीक़ा कि दो रन एक जैसे हैं। अब भी न ग्राफ़िक्स है, न किरदार। इस पोस्ट का ज़्यादातर हिस्सा अंदरूनी पाइपलाइन के बारे में है, और उसका एक हिस्सा ऐसे बग के बारे में है जिसकी कहानी सुनाने लायक निकली।
टाइलों से बनी दुनिया
दुनिया अलग-अलग स्तरों पर 1×1 मीटर की टाइलों का ग्रिड है, जिनकी गिनती शून्य से ऊपर की ओर होती है। दीवार या दरवाज़ा एक टाइल है; सीढ़ी दो स्तरों के बीच एक ख़ास कड़ी होगी, लेकिन सीढ़ियाँ अभी बनी नहीं हैं। अंदर से दुनिया पूरे नक़्शे के लिए सपाट परतों (फ़र्श, ढाँचा, ढाँचे के फ़्लैग) का सेट है, साथ में चलने-योग्यता की एक व्युत्पन्न परत। नक़्शे को वर्शन काउंटर वाले 16×16 चंक्स में ट्रैक किया जाता है, ताकि रेंडरर या पाथ सर्च टाइलों की तुलना किए बिना पूछ सके “क्या यह इलाक़ा बदला?”।
दुनिया का आकार उसे बनाते समय चुना जाता है। निर्माण ऐसे कमांड से होता है जो एक आयत लेते हैं: किसी इलाक़े पर फ़र्श, दीवार या दरवाज़ा लगाना, या उन्हें हटाना। अगर आयत का कुछ हिस्सा रुका हुआ हो, तो बाक़ी फिर भी बन जाता है और कमांड बताता है कि उसने क्या छोड़ा। अभी तीन बिल्ट-इन टाइल प्रकार हैं: फ़र्श, दीवार, दरवाज़ा। अब से नक़्शा दुनिया के हैश और सेव का हिस्सा है। हज़ारों रैंडम कमांड पर इसकी जाँच एक धीमे, साफ़-साफ़ सही रेफ़रेंस मॉडल से की जाती है।
सेव और रीप्ले
सेव और रीप्ले एक ही फ़ाइल कंटेनर साझा करते हैं। यह लेबल वाले सेक्शन में बँटा है, हर सेक्शन का एक टाइप और वर्शन है, ताकि दुनिया के नए हिस्से (किरदार, ज़रूरतें, रिश्ते) बाद में पुरानी फ़ाइलें तोड़े बिना नए सेक्शन बन सकें। हेडर और बॉडी पर चेकसम हैं, बॉडी कंप्रेस की जाती है, और कोई भी मेमोरी लेने से पहले हर आकार की तुलना एक सीमा से होती है, ताकि ख़राब या शरारती फ़ाइल गेम से आपकी पूरी RAM न खिलवा सके। टाइल प्रकार नंबर के बजाय नाम से सहेजे जाते हैं, ताकि प्रकारों की सूची बदलने पर भी सेव काम करता रहे।
रीप्ले वही है जो मैं शुरू से चाहता था: सीड और कमांड लॉग। रिकॉर्डिंग के दौरान कोर हर 100 टिक पर एक कंट्रोल हैश भी लिखता है। रीप्ले चलाते समय वह रास्ते भर हर कमांड का नतीजा और हर कंट्रोल हैश मिलाता है, और जिस पहले टिक पर वे मेल न खाएँ, वहीं रुक जाता है। रीप्ले यह भी याद रखता है कि कोर के किस बिल्ड ने उसे रिकॉर्ड किया, ताकि दूसरे बिल्ड का रीप्ले चुपचाप भटकने के बजाय अस्वीकार हो जाए।
कोर ख़ुद कभी फ़ाइलों को नहीं छूता: वह स्ट्रीम पढ़ता और लिखता है, और गेम तय करता है कि सेव कहाँ रहें। यह सीमा उसी प्रतिबंधित-API व्यवस्था से लागू होती है जो पहले से है।
अभी बाक़ी: रीप्ले फ़ाइल चलाने वाला कमांड-लाइन टूल, और सेव का गेम वाला हिस्सा (सेव फ़ोल्डर, ऑटोसेव, सुरक्षित लिखना)।
साबित करना कि दो रन एक जैसे हैं
डिटरमिनिज़्म टेस्ट अब एक सिनेरियो को दो अलग-अलग प्रोसेस में चलाता है: एक सीड, निर्माण कमांड की एक स्क्रिप्ट (कुछ जानबूझकर अमान्य), और एक टेस्ट सिस्टम जो हर रैंडम स्ट्रीम से निकालता है। हर प्रोसेस इसे तीन तरह चलाता है — सीधे शुरू से अंत तक, सेव और रीस्टोर के ज़रिए, और रिकॉर्ड किए गए बाइट्स के रीप्ले के ज़रिए — और हर टिक पर हैश मिलाता है। फिर दोनों प्रोसेस हर 10वें टिक पर अपने हैश आपस में मिलाते हैं।
यह पक्का करने के लिए कि टेस्ट फ़ेल हो सकता है, मैंने उसे जानबूझकर तोड़ा। टिक में चुपके से डाला गया बिना सीड का Random टेस्ट को लाल कर गया। रैंडमाइज़्ड स्ट्रिंग हैश, जिसे .NET हर प्रोसेस में अलग सीड देता है, एक प्रोसेस के भीतर पकड़ में नहीं आया और सिर्फ़ दो प्रोसेस की तुलना में पकड़ा गया। दो प्रोसेस की पूरी वजह यही है।
मैंने जो नियम लिखा है, वह यह है कि कोर का एक बिल्ड x64 और arm64 प्रोसेसर पर एक जैसे हैश दे, यानी Apple चिप वाले Mac पर रिकॉर्ड किया रीप्ले Windows PC पर चलना चाहिए। ठीक इसी जाँच के लिए पक्के किए गए रेफ़रेंस हैश और सेव व रीप्ले की उदाहरण फ़ाइलें मौजूद हैं। फ़्लोटिंग-पॉइंट त्रिकोणमिति और एक्सपोनेंशियल अलग-अलग प्लैटफ़ॉर्म पर अलग नतीजे देते हैं, इसलिए दुनिया पर असर डालने वाली किसी भी चीज़ में उन पर रोक है; जिस दिन किसी सिस्टम को इनकी ज़रूरत होगी, मुझे एक डिटरमिनिस्टिक विकल्प चाहिए होगा। ये रेफ़रेंस फ़ाइलें सिर्फ़ जानबूझकर बदलती हैं: अगर कोई हैश खिसके, तो बदलाव को मंज़ूर करने से पहले उसकी वजह बतानी होती है।
ऐसा बेंचमार्क जो मना कर सके
अब एक बिना इंटरफ़ेस का रनर है जो सिनेरियो, सीड और टिकों की संख्या लेकर JSON रिपोर्ट छापता है: प्रति टिक समय का मीडियन और 99वाँ परसेंटाइल, एजेंटों की संख्या, दुनिया का हैश, और प्रति टिक कितने बाइट मेमोरी ली गई। पहला सिनेरियो लगभग 52,000 निर्माण कमांड से 16 स्तरों पर 512×512 का नक़्शा बनाता है।
बेसलाइन हर मशीन के लिए अलग रखी जाती है, क्योंकि लैपटॉप के समय डेस्कटॉप पर कुछ नहीं बताते। रिग्रेशन तब है जब मीडियन बेसलाइन से 10% से ज़्यादा या 99वाँ परसेंटाइल 35% से ज़्यादा ऊपर हो, और तभी जब बढ़त 0.05 ms से ज़्यादा हो, क्योंकि लगभग ख़ाली टिक टाइमर की सटीकता की सीमा पर होता है। एक शर्त हर मशीन पर लागू है: प्रति टिक शून्य बाइट मेमोरी। मुझे ईमानदारी से बताना चाहिए कि यह आज क्या मापता है। बिना सिस्टम और किरदारों के एक टिक लगभग 35 नैनोसेकंड लेता है, इसलिए समय वाली शर्त ज़्यादातर सोई रहती है, जब तक आवाजाही वाले माइलस्टोन में पहले असली सिस्टम नहीं आते। अभी काम मेमोरी वाली शर्त कर रही है।
वह अस्थिर टेस्ट जो अस्थिर नहीं था
बेंचमार्क बनाते समय सेव फ़ॉर्मेट का एक टेस्ट कुछ बिल्ड पर फ़ेल होने लगा, जबकि कोर बदला ही नहीं था। टेस्ट कंप्रेस किए गए रीप्ले के हर बाइट को बारी-बारी से पलटता है और माँगता है कि लोडर हर एक को अस्वीकार करे। एक बिल्ड पर फ़ाइल के अंत के पास का एक बाइट स्वीकार हो गया। पिछले बिल्ड पर वही टेस्ट पास हुआ था।
यह आता-जाता क्यों था: रीप्ले में उसे लिखने वाले बिल्ड की पहचान होती है, इसलिए कंप्रेस किए गए बाइट हर बिल्ड में अलग होते हैं, और उनके साथ वह बाइट भी जो पलटा जाता है। खामी हमेशा से थी; टेस्ट उसे तभी पकड़ता था जब पासा उस तरफ़ गिरता।
खामी ख़ुद यह थी: चेकसम डीकंप्रेशन के बाद के डेटा को कवर करता था, और .NET का डीकंप्रेसर नरम निकला। वह ऐसी स्ट्रीम स्वीकार कर लेता है जो कभी नहीं बताती कि वह ख़त्म हो गई, और अंत के बाद की हर चीज़ को अनदेखा करता है। आख़िरी बाइट में एक पैडिंग बिट पलटने से एक अलग, फिर भी मान्य स्ट्रीम बनती है जो उसी डेटा में डीकंप्रेस होती है, इसलिए चेकसम मेल खाता रहा और ख़राब फ़ाइल सही की तरह लोड हो गई।
समाधान कंटेनर का नया वर्शन है। चेकसम अब बाइट्स को उसी रूप में कवर करता है जैसे वे फ़ाइल में रखे हैं, और डीकंप्रेशन से पहले जाँचा जाता है, और डीकंप्रेशन ठीक वहीं ख़त्म होना चाहिए जहाँ हेडर बताता है। ख़ाली बॉडी बिना कंप्रेशन के सहेजी जाती है, क्योंकि ख़ाली इनपुट के लिए कंप्रेसर कुछ नहीं लिखता। जाँचें अब ऐसी तय कंप्रेस्ड स्ट्रीम इस्तेमाल करती हैं जो बिल्ड पर निर्भर नहीं, साथ में ख़राब फ़ाइलों की फ़ज़िंग भी। नए वर्शन के लिए उदाहरण फ़ाइलें फिर से बनाई गईं; दुनिया के हैश नहीं बदले।
बाक़ी जगहों पर
यह वेबसाइट, जिसे आप पढ़ रहे हैं, अब मुख्य पेज पर दो विज़िटर काउंटर दिखाती है: अब तक के यूनिक विज़िटर और पिछले 24 घंटों के। उनकी गिनती वेब सर्वर के पेज लॉग से होती है, बिना JavaScript, बिना कुकी और बिना बाहरी सेवाओं के। पता सिर्फ़ नमक मिले हैश के रूप में रखा जाता है, बॉट उनके यूज़र एजेंट से छाँटे जाते हैं, और आँकड़े कुछ मिनट पीछे चलते हैं। एक नेटवर्क के पीछे के कई लोग एक गिने जाते हैं।
आगे
पहला माइलस्टोन, कोर का ढाँचा, पूरा हो गया है। आगे M1 है, Godot का बिल्डिंग व्यू: दुनिया के मॉडल से दीवारें, दरवाज़े और कमरे, मंज़िल-कटाई, 90° के क़दमों में घूमने वाला कैमरा और आर्ट की जगह सादे क्यूब्स। इस पर काम अभी शुरू नहीं हुआ है। देखें रोडमैप।