1 puan yazan GN⁺ 2025-07-02 | 1 yorum | WhatsApp'ta paylaş
  • Donkey Kong Country 2'deki dönen varil hatası ZSNES emülatöründe ortaya çıkıyor
  • ZSNES, open bus davranışını doğru şekilde emüle etmediği için varillerin kalıcı olarak dönmeye devam etmesi sorunu oluşuyor
  • Gerçek donanımdan farklı olarak, ZSNES'te hatalı bellek erişiminde her zaman 0 döndürüldüğü için hata tetikleniyor
  • Doğru davranışta varil, tam doğru yönde (8 yön) dönmeyi durduran bir mantığa sahip
  • Bu sorunun, kodlamadaki küçük bir hatadan (immediate addressing yerine absolute addressing kullanılması) kaynaklandığı tahmin ediliyor

Donkey Kong Country 2 ve ZSNES emülatöründeki varil hatası

Donkey Kong Country 2'de, ZSNES adlı eski bir SNES emülatöründe bazı bölümlerdeki dönen varillerin düzgün çalışmamasına yol açan ünlü bir hata bulunuyor.

Varilin içine girildiğinde, normalde varil yalnızca yön tuşlarında sol/sağ basılı tutulduğu sürece dönmelidir; ancak ZSNES'te sol/sağa kısa süre basılsa bile varil o yönde sonsuz şekilde dönmeye devam ediyor.

Bu hata nedeniyle, özellikle ilerleyen bölümlerde dikenler veya engellerin üzerinde görülen dönen varil sekansları, geliştiricinin amaçladığından çok daha zor hale geliyor.

Bu sorun geçmişte ZSNES forumlarında bir ölçüde belgelenmişti, ancak forumlar artık kapandığı için ilgili materyalleri bulmak zor.

Hatanın nedeni - Open Bus Emulation

Bu hatanın temel nedeni, ZSNES'in open bus davranışını emüle etmemesidir.

  • open bus, SNES gibi eski platformlarda geçersiz bellek adresleri okunduğunda ortaya çıkan bir davranıştır
  • Gerçek donanımda, veri yoluna en son konan değer geri döndürülür
  • SNES'in ana CPU'su 65C816'dır (65816)
  • 65816, 6502'nin 16 bitlik sürümüdür; 24 bit adres veri yoluna sahiptir ve bellek bankalama yöntemi kullanır

DKC2'nin dönen varil kodunda, geçersiz adreslere (Bank $B3 içindeki $2000, $2001) erişildiğinde donanımda open bus yoluyla 0x2020 değeri döndürülür.

ZSNES'te bu özellik olmadığı için her zaman 0 döndürülür ve hata ortaya çıkar.

Oyun kodu nasıl çalışıyor

Dönen varille ilgili oyun rutini şu akışla çalışır:

  • Mevcut varil yönü ile dönüş miktarı (hız) toplanıp geçici bir değişkende saklanır
  • XOR işlemiyle yöndeki değişim ölçülür ve sonuç, open bus'tan okunan değerle AND işlemine sokulur
  • Bu AND sonucu 0 ise dönüş devam eder; 0 değilse durur ve yön, 8 yönden birine yuvarlanarak hizalanır

Gerçek donanımda open bus değeri 0x2020'dir; ancak 0 döndürülürse dönüş sonsuza kadar sürer.

Bu mantıkta aslında AND işleminin immediate value (address #$2000) ile yapılması gerekirken, yanlışlıkla absolute address (address $2000) kullanıldığı tahmin ediliyor.

Ancak donanımın open bus özelliği sayesinde, pratikte her iki yöntem de normal çalışıyor.

Çözüm ve sonuç

Snes9x gibi diğer SNES emülatörleri bu hatayı hardcode bir düzeltmeyle giderdi, ZSNES ise geliştirilmesi durduğu için yama almadı.

İlgili rutinde AND komutunun opcode'unu 0x2D'den 0x29'a (AND #$2000) çevirmek, open bus davranışı olmadan da dönen varilin doğru çalışmasını sağlıyor.

Bu sorun gerçek donanımda veya modern emülatörlerde ortaya çıkmıyor.

Sonuç olarak bu hata, open bus emülasyonu desteğinin olmaması ile kodlama hatasının birleşmesiyle ortaya çıkan bir örnek.


Ek arka plan: 65816 yapısı ve SNES bellek haritası

65816 CPU, 24 bit adres veri yoluna sahip olsa da çoğunlukla 8 bit banka + 16 bit ofset kombinasyonunu kullanır.

  • Program counter (PC) 16 bittir; program bank register (PBR, K) ile birlikte tam adresi oluşturur
  • Data bank (DBR, B), veri işlemleri için kullanılacak bankayı seçmekte kullanılır
  • Donanım stack'i ve direct page her zaman $00 bankasında bulunur

SNES bellek haritası da 65816 temelinde tasarlandığından, adresleri 8 bit banka + 16 bit ofset olarak düşünmek daha verimlidir.

Kapanış

Bu örnek, legacy donanım özelliklerinin (open bus vb.) emülasyonda beklenmeyen hatalara yol açabileceğini gösteriyor.

Geliştirici aslında immediate addressing kullanmalıydı, ancak absolute addressing'in de tesadüfen düzgün çalıştığı bir durum ortaya çıkmış.

Günümüzde, open bus davranışına kadar emülasyon yapmak, eski yazılımların doğru biçimde yeniden üretilmesi açısından çok önemli olduğunu gösteriyor.

1 yorum

 
GN⁺ 2025-07-02
Hacker News görüşü
  • Ben bir 6502 assembly programcısı olarak # işaretini unutup immediate değer yerine bellek erişimi yapma hatası yüzünden sayısız saat kaybettim; bazen bu tür hataların şans eseri çalışması ise meseleyi daha da can sıkıcı hale getiriyor. Ama örnekteki floating bus probleminden bile daha kötü olanı, ilklendirilmemiş RAM'e güvenen kodlar; çünkü her DRAM'in başlangıç değeri farklı olabiliyor, bu yüzden kendi makinenizde ya da emülatörünüzde hep çalışan kod, farklı DRAM kullanan başka bir makinede başarısız olabiliyor. Genelde bu tür sorunlar, demoparty'de kodu başkasının donanımında çalıştırmanız gerekirken ve teslim süresine 15 dakikadan az kalmışken ortaya çıkıyor

    • 6502 CPU kullanan sistemlerde gerçekten dinamik bellek kullanılan bir mimari olup olmadığını merak ediyorum. Benim deneyimimde bu platformlar hep statik RAM kullanıyordu

    • 6502 benim ilk assembly dilimdi ve LDA #2 ifadesini “A register'ına 2 sayısını yükle” diye düşünüyordum. Buna karşılık LDA 2 bana “2 numaralı bellek konumundaki değeri yükle” gibi geliyordu; bu fark sayesinde en baştan bu hatadan kaçınmaya çalışıyordum

    • Böyle durumlarda kodu bir LLM'den geçirmek gerçekten faydalı olabilir. LLM'lerin bu tür etkisi büyük yazım hatalarını ya da hata noktalarını yakalama konusunda güçlü olması nedeniyle

  • Open Bus ifadesi büyük harfle yazılınca, yazıyı okurken bunun eski bir veri yolu protokolü ya da standardı olduğunu sandım. Meğerse sadece veri yolunun hiçbir yere bağlı olmadığı durumu anlatıyormuş; yani adres çözücünün seçtiği adreste ($2000) hiçbir bellek aygıtı etkinleşmiyormuş. # ile immediate modu belirtmeyi unutunca bellekte aslında hiçbir şey okunamaması durumu, eski emülatörlerin gerçek donanımdan farklı davranması sayesinde fark edilmiş. Çözüm olarak yönerge immediate adresleme moduna çevrilince artık bellek okuması yapılmıyor ve kod yaklaşık 2us kadar hızlanıyor. Ama bu kadar küçük bir performans farkı, gerçek donanım dışında — özellikle zamanlaması tam örtüşmeyen emülatörlerde — pek anlamlı görünmüyor

    • (Bazı) SNES emülatörlerinin bugün neredeyse zamanlama açısından kusursuzluğa ulaştığı açıklanıyor. Yine de 2us'lik fark, gerçekten istisnai durumlar dışında pratikte fark edilebilir bir etki yaratmıyor. İlgili yazı: How SNES emulators got a few pixels from complete perfection

    • Rare gibi şirketlerin, ancak piyasaya çıkıştan uzun süre sonra yeni mimariler sayesinde fark edilen hatalar içeren oyunlar yayımladığı örnekler var. Donkey Kong 64'te 8-9 saat kesintisiz oynadıktan sonra ölümcül bir bellek sızıntısı ortaya çıkıyor; ama emülatör save özelliği sayesinde bu süre bir anda birikince hata kolayca görünür hale geliyor. Bu arada oyuna kutudan çıkan Memory Pak'in bu hatayı gizlemek için eklendiği söylenmişti, ama yakın tarihli araştırmalara göre ne Rare ne de Nintendo o sırada bu hatanın farkındaydı

  • SNES Puyo Puyo'da PPU open bus davranışıyla karşılaştım. RetroArch'ta RunAhead özelliği üzerinde çalışırken save state'lerin neden eşleşmediğini araştırıyordum; PPU open bus'tan okunan değer, durum yüklendikten sonra değiştiği için CPU execution trace log'ları uyuşmayan özel bir vakaydı

  • 6502 ya da benzer kodlarda ben de sık sık bellek adresiyle immediate değeri karıştırıyorum. #$1234 gibi gösterimin hataya açık olduğunu düşünüyorum; hatta Chuck Peddle'ın bile bu sözdiziminden derin pişmanlık duyduğunu duymuştum. IDE'de # işaretini kırmızıyla vurgulamak bunu biraz önlemeye yardımcı olmuştu. Rare'in geliştiricileri bile bu tür bir hatadan kaçamamış

    • Oldukça uzun zaman önce GNU assembler'da intel_syntax noprefix modunda benzer bir sorun yaşamıştım; burada immediate adlı sabitlere önden referans verirken sözdizimsel bir belirsizlik oluşuyor ve bu, bunların bellek adresi ya da sembol olarak yorumlanmasına yol açabiliyor. Sonuçta beklediğimin aksine, sembolün link zamanında çözülmesini bekleyen geçici bir bellek adresi oluşuyordu ve bu hatayı bulmak gerçekten işkenceydi

    • ARM gibi, belleği işlemek için ayrı talimatlar gerektiren instruction set'ler bu tür kafa karıştırıcı hataları kökten engelliyor

  • Bildiğim kadarıyla open bus olgusu yalnızca erken dönem basit senkron veri yolu sistemlerinde görülüyor. Çoğu diğer sistem, var olmayan bir adrese erişildiğinde hep 0 ya da hep 1 gibi sabit bir değer döndürüyor; bu da veri yolu protokolündeki bir handshake mekanizmasıyla, yani yanıt yoksa master'ın bunu algılamasıyla çözülüyor (PCI'daki master abort gibi)

  • Parallax Propeller çipini programlarken benzer bir hatayı defalarca yaşadım. JMP #address ile JMP address arasındaki farkı sık sık karıştırıyorum; bunun sebebi 6502 assembler kas hafızası. Propeller'da JMP #address, belirtilen adrese atlar; JMP address ise verilen adresten okunan değere atlar. Sorun şu ki bu tür hatalar bazen çalışıyor da, bu yüzden sistem durana kadar sebebini bulmak için saatler harcıyorsunuz

  • Open bus, veri yolu hatlarının gerçekten açıkta kalması anlamına gelir. CPU eşlenmemiş ya da yalnızca yazılabilir bir adresi veri yoluna koyduğunda hiçbir donanım yanıt vermez ve veri yolu hatları floating durumda kalır — yani donanım seviyesinde undefined behavior. Gerçekte ne olduğunu anlamak için veri yolunun fiziksel yapısına bakmak gerekir. Veri yolu, anakart ile kartuş arasında sinyal taşıyan uzun iletkenlerden oluşur ve ince bir yalıtkan katmanla toprak düzleminden ayrılmıştır. Bu yapı bir tür kapasitör gibi davrandığından, son sinyal voltajını bir süre “tutma” eğilimindedir. Bu yüzden open bus durumunda son taşınan değerin yeniden okunması etkisi görülür. DKC2 gibi oyunlar bazen farkında olmadan bu open bus özelliğine dayanır; NES'in kontrolcü seri portunda da yalnızca düşük bitler sinyal taşır ve yüksek bitler open bus olarak kalır, bu yüzden bazı oyunlar LDA $4016 komutundan $40 ya da $41 bekler. Open bus davranışı, Super Mario World credits warp gibi speedrun stratejilerinde de kullanılır (bellek kirletme ya da arbitrary code execution yoluyla). Ancak standart dışı kartuşlar, pull-up/pull-down dirençleri ya da DMA ile alışılmadık etkileşimler (Horizontal DMA gibi) istisnai sonuçlar doğurabilir. Örneğin SNES'te bir HDMA aktarımı komutun ortasında gerçekleşirse, open bus okuma zamanlamasını etkileyebilir; bu da Super Metroid speedrun exploit'inde kopyalanmak istenen bellek blokları arasına anormal değerlerin girmesine ve exploit'in bozulmasına neden olabilir. Bu yüzden orijinal donanımda ya da çok hassas emülatörlerde çökme yaşanırken, çoğu emülatör ya da resmî yeniden sürüm bu niş davranışı tam uygulamadığından strateji sorunsuz çalışır. Super Metroid TAS dünya rekoru tamamlaması da bu HDMA davranışına dayanır. Düşman konumlarını değiştirip CPU zamanlamasını kaydırarak HDMA'nın open bus üzerine istenen değeri koyması sağlanır; böylece sonunda kontrolcü girdileri kod olarak çalıştırılır ve arbitrary code execution mümkün hale gelir Super Mario World credits warp videosu, HDMA kullanımı videosu, Super Metroid DMA exploit videosu, Super Metroid TAS kaydı

    • Ben Eater'ın 6502 breadboard bilgisayarı video serisi, bu tür donanım davranışlarının nasıl çalıştığını anlamamda çok yardımcı olmuştu. Ticari cihazlarda bu veri yolu davranışlarının nasıl ölçeklendiğini hissettiriyor Ben Eater sitesi
  • Bu tür ilginç bug analizi içeriklerini seviyorum; assembly kodunun belki ancak %60'ını takip edebiliyorum ama eşlik eden yazılı açıklamalar sayesinde anlamam çok artıyor. Ayrıca uzun yıllar kimsenin fark etmediği bug'ların başyapıt yazılımlarda sonradan ortaya çıkması türünden hikâyeler özellikle eğlenceli geliyor

    • O dönemin sistemlerinde, bugün gömülü sistemlerde vazgeçilmez olan — ister ağ bağlantısı ihtimali yüzünden olsun ister olmasın — denetim mekanizmalarının çoğu yoktu; bu da onları daha da ilginç kılıyor. NES döneminde sayısız read/write işlemi aslında sadece hatlardaki voltajı açıp kapatmaktan ibaretti ve ne olacağı ancak tam o anda belli oluyordu. CRT blanking sinyaliyle tam senkron zamanlamada voltaj değiştirerek istenen efektler elde ediliyordu; örneğin Super Mario Bros. 3'te ekran yenileme zamanında sprite bank'larını değiştirmek için RAM multiplexer'ını açıp kapatma gibi numaralar yapılıyordu. Farklı bölgelerdeki NTSC/PAL TV'lerin tarama hızı, rendering mantığının saat sinyali gibi iş gördüğü için her televizyon standardına uygun ayrı yazılımlar yayımlamak gerekiyordu; gerçekten çılgın bir dönemdi
  • Bir oyunda emülatörde takılıp kalırsam, aklıma her zaman “acaba emülatör bug'ı mı?” sorusu geliyor. Bu olayda da ben herhalde sadece oyunun tasarımının böyle zor olduğunu sanırdım. Oyun çok zor olduğunda da bazen “acaba emülatör latency'si yüzünden mi?” diye düşünürdüm; bu yüzden sonunda kendi mister FPGA sistemimi kurdum

    • Chrono Trigger'da dört tuşa aynı anda basmanız gereken bir bölüm var; ama USB girişleri aynı anda yalnızca üç tanesini iletebildiği için dört denemenin sadece biri kaydoluyordu ve bu çok zorlayıcı, sinir bozucu bir deneyimdi

    • DKC'yi yalnızca ZSNES ile oynamıştım; bu yüzden yazıyı okuyana kadar bunun bir emülatör bug'ı olduğunu hiç fark etmemiştim. Oyunun tasarım gereği böyle zor olduğunu sanıyordum ve bunun aslında bir bug olduğunu öğrenince gerçekten şok oldum

    • Çocukken Bionic Commando'yu çok oynamıştım ama emülatörde tekrar oynayınca çok daha zor gelmişti. Sonradan öğrendim ki emülatör bug'ı yüzünden düşmanlar kaybolmuyor ve gereken can sayısı iki katına çıkıyormuş. Yine de bir kez o şekilde bitirdim ama bir daha yapmak istemem

  • DKC 1'in SGI tabanlı pre-render 3D grafikleri döneminin en ileri teknolojilerindendi. Mega Drive'daki Vector Man de benzer bir teknik kullanmıştı ama DKC kadar ilgi görmemişti

    • 1995'te DKC'nin asıl hedef yaş grubundaydım (11 yaş) ve bu oyunun grafikleri gerçekten sarsıcıydı. Çıkış döneminde tanıtım videosunu da edinmiştim; kamera arkası görüntüler içeren o kaseti defalarca izlemiştim. Oyunun kendisine hiç sahip olmadım ama arkadaşımın evinde oynama fırsatım olmuştu

    • Çocukken DKC grafiklerinde bir şekilde “sahte” bir his olduğunu sezmiştim. O dönem dergiler, SNES'in gerçek zamanlı 3D karakter render ettiği gibi yapay açıklamalar yapıyordu; ama aslında bunun daha çok bir flipbook animasyonu mantığında olduğunu belirsiz de olsa fark etmiştim