#2 Bir dünya, kayıtlar, tekrarlar ve kayıt biçiminde saklanan bir hata
· Nick
İlk yazıdan bu yana çekirdek, içine bir şeyler koyacağı bir dünya, onu saklayacağı dosyalar ve iki çalıştırmanın birbiriyle uyuştuğunu kanıtlama yolu kazandı. Hâlâ grafik ve karakter yok. Bu yazının büyük kısmı tesisat işi; bir kısmı ise hikâyesini anlatmaya değer çıkan bir hata.
Karolardan oluşan bir dünya
Dünya, sıfırdan yukarı doğru numaralanan ayrık katlardaki 1×1 m’lik karolardan oluşan bir ızgaradır. Duvar ya da kapı bir karodur; merdiven iki kat arasındaki özel bir bağlantı olacak, ama merdivenler henüz inşa edilemiyor. İçeride dünya, tüm harita için düz katmanların (zemin, yapı, yapı bayrakları) ve yürünebilirlik için türetilmiş bir katmanın birleşimidir. Harita, sürüm sayaçlı 16×16’lık parçalarda (chunk) izlenir; böylece bir çizici ya da yol arama, karoları karşılaştırmadan “bu alan değişti mi?” diye sorabilir.
Dünyanın boyutu oluşturulurken seçilir. İnşa, bir dikdörtgen alan komutlarla yapılır: bir alana zemin, duvar ya da kapı koymak veya bunları kaldırmak. Dikdörtgenin bir kısmı engelliyse kalanı yine de inşa edilir ve komut neleri atladığını bildirir. Şimdilik üç yerleşik karo türü var: zemin, duvar, kapı. Bu noktadan sonra harita, dünya özetinin ve kayıtların bir parçası. Binlerce rastgele komutta, yavaş ama açıkça doğru bir başvuru modeliyle karşılaştırılarak denetleniyor.
Kayıtlar ve tekrarlar
Bir kayıt ve bir tekrar aynı dosya kapsayıcısını paylaşır. Kapsayıcı, her biri bir tür ve sürüm taşıyan etiketli bölümlere ayrılır; böylece dünyanın yeni parçaları (karakterler, ihtiyaçlar, ilişkiler) ileride eski dosyaları bozmadan yeni bölümler olabilir. Başlık ve gövde için sağlama toplamları var, gövde sıkıştırılıyor ve her boyut, bellek ayrılmadan önce bir sınırla denetleniyor; böylece bozuk ya da kötü niyetli bir dosya oyunun tüm RAM’inizi yemesine yol açamaz. Karo türleri numarayla değil adla saklanır, dolayısıyla tür listesi değişse bile bir kayıt çalışmaya devam eder.
Tekrar, en başından beri istediğim şey: bir tohum artı komut kaydı. Kayıt sırasında çekirdek her 100 tick’te bir de kontrol özeti yazar. Bir tekrarı oynatırken yol boyunca her komutun sonucunu ve her kontrol özetini karşılaştırır ve ikisinin ayrıştığı ilk tick’te durur. Bir tekrar ayrıca onu çekirdeğin hangi derlemesinin kaydettiğini de hatırlar; farklı bir derlemeden gelen tekrar, sessizce sapmak yerine reddedilir.
Çekirdeğin kendisi dosyalara asla dokunmaz: akışları okur ve yazar, kayıtların nerede duracağına oyun karar verir. Bu sınırı, daha önceki yasaklı API düzeneği zorunlu kılıyor.
Henüz yapılmadı: bir tekrar dosyasını oynatan bir komut satırı aracı ve kaydetmenin oyun tarafı (bir kayıt klasörü, otomatik kayıtlar, güvenli yazma).
İki çalıştırmanın uyuştuğunu kanıtlamak
Determinizm testi artık bir senaryoyu iki ayrı süreçte çalıştırıyor: bir tohum, inşa komutlarından oluşan bir senaryo (bir kısmı kasten geçersiz) ve her rastgele akıştan değer çeken bir test sistemi. Her süreç bunu üç yolla çalıştırıyor — baştan sona, bir kayıt ve geri yükleme üzerinden, kaydedilen baytların tekrarı üzerinden — ve her tick’te özetleri karşılaştırıyor. Sonra iki süreç, her 10. tick’te özetlerini birbiriyle karşılaştırıyor.
Başarısız olabildiğinden emin olmak için testi bilerek bozdum. Tick’e gizlice sokulan tohumsuz bir Random testi kırmızıya çevirdi. .NET’in her süreçte farklı tohumladığı rastgeleleştirilmiş bir dize özeti ise tek bir süreç içinde fark edilmedi; ancak iki süreç karşılaştırılınca yakalandı. İki süreç kullanmanın tüm nedeni bu.
Yazıya döktüğüm kural şu: çekirdeğin tek bir derlemesi x64 ve arm64 işlemcilerde aynı özetleri vermeli; yani Apple çipli bir Mac’te kaydedilen tekrar bir Windows PC’de oynatılabilmeli. Sabitlenmiş başvuru özetleri ile örnek kayıt ve tekrar dosyaları tam olarak bu kontrol için var. Kayan noktalı trigonometri ve üstel işlevler platformlar arasında farklılık gösterir, bu yüzden dünyayı etkileyen hiçbir şeyde kullanılamazlar; bir sistemin bunlara ilk ihtiyaç duyduğu anda deterministik bir yedek yazmam gerekecek. Bu başvuru dosyaları yalnızca bilerek değişir: bir özet kayarsa, değişiklik kabul edilmeden önce açıklanmalıdır.
Hayır diyebilen bir kıyaslama
Artık bir senaryo, bir tohum ve bir tick sayısı alıp JSON raporu basan başsız bir çalıştırıcı var: tick başına ortanca ve 99. yüzdelik süre, ajan sayısı, dünya özeti ve tick başına kaç bayt ayrıldığı. İlk senaryo, 16 katta 512×512’lik bir haritayı yaklaşık 52.000 inşa komutuyla kuruyor.
Başlangıç değeri makine başına tutulur, çünkü bir dizüstü bilgisayarın süreleri masaüstünde bir şey ifade etmez. Gerileme; ortancanın başlangıç değerinin %10’dan fazla üstünde ya da 99. yüzdeliğin %35’ten fazla üstünde olması ve yalnızca artışın 0,05 ms’den büyük olması durumunda sayılır, çünkü neredeyse boş bir tick zamanlayıcının çözünürlüğü düzeyindedir. Bir eşik her makinede geçerli: tick başına sıfır bayt ayırma. Bunun bugün neyi ölçtüğü konusunda dürüst olmalıyım. Sistem ve karakter olmadan bir tick yaklaşık 35 nanosaniye sürüyor; yani süre eşiği, hareket kilometre taşında ilk gerçek sistemler gelene kadar büyük ölçüde uyuyor. Şu an işi yapan, bellek ayırma eşiği.
Kararsız olmayan kararsız test
Kıyaslamayı kurarken, çekirdek değişmediği hâlde bir kayıt biçimi testi bazı derlemelerde başarısız olmaya başladı. Test, sıkıştırılmış bir tekrarın her baytını sırayla çevirir ve yükleyicinin hepsini reddetmesini ister. Bir derlemede, dosyanın sonuna yakın bir bayt kabul edildi. Bir önceki derlemede aynı test geçiyordu.
Gelip gitmesinin nedeni şuydu: bir tekrar, onu yazan derlemenin kimliğini taşır; dolayısıyla sıkıştırılmış baytlar derlemeden derlemeye değişir ve çevrilen bayt da değişir. Açık hep oradaydı; test onu yalnızca zarlar öyle düştüğünde fark etti.
Açığın kendisi: sağlama toplamı verileri açıldıktan sonra kapsıyordu ve .NET’in açıcısı hoşgörülü çıktı. Bittiğini hiç söylemeyen bir akışı kabul ediyor ve sondan sonra gelen her şeyi görmezden geliyor. Son baytta bir dolgu bitini çevirmek, aynı verilere açılan farklı ama yine geçerli bir akış üretiyor; sağlama toplamı yine tutuyor ve hasarlı bir dosya sağlam gibi yükleniyordu.
Düzeltme, kapsayıcının yeni bir sürümü. Sağlama toplamı artık baytları dosyada saklandıkları hâliyle kapsıyor ve açmadan önce denetleniyor; açma işlemi de tam olarak başlığın söylediği yerde bitmek zorunda. Boş bir gövde sıkıştırılmadan saklanıyor, çünkü sıkıştırıcı boş girdi için hiçbir şey yazmıyor. Denetimler artık derlemeye bağlı olmayan sabit sıkıştırılmış akışlar ve hasarlı dosyalarla fuzzing kullanıyor. Örnek dosyalar yeni sürüm için yeniden üretildi; dünya özetleri değişmedi.
Başka yerlerde
Web sitesi, şu an okuduğunuz site, ana sayfada artık iki ziyaretçi sayacı gösteriyor: tüm zamanların ve son 24 saatin tekil ziyaretçileri. Bunlar web sunucusunun sayfa kaydından sayılıyor; JavaScript, çerez ya da dış hizmet yok. Bir adres yalnızca tuzlanmış bir özet olarak saklanıyor, botlar kullanıcı aracısına göre ayıklanıyor ve sayılar birkaç dakika geriden geliyor. Aynı ağın arkasındaki birden çok kişi tek sayılıyor.
Sırada ne var
İlk kilometre taşı olan çekirdek iskeleti bitti. Sırada M1, yani Godot inşa görünümü var: dünya modelinden duvarlar, kapılar ve odalar, kat kesiti, 90°’lik adımlarla dönen bir kamera ve grafik yerine sade küpler. Üzerinde çalışmaya henüz başlanmadı. Yol haritasına bakın.