1 puan yazan GN⁺ 2025-07-02 | 1 yorum | WhatsApp'ta paylaş
  • Donkey Kong Country 2'nin bazı dönen varil bölümlerinde, eski SNES emülatörü ZSNES'te yön tuşu bırakıldığında bile dönmenin sürmesi şeklinde bir hata var; nedeni open bus davranışının uygulanmamış olması
  • Gerçek SNES'te eşlenmemiş bir adres okunduğunda veri yolundaki son değer yeniden okunur ve DKC2, bank $B3 içindeki $2000/$2001 okumalarında bu özelliğe dayanır
  • Sorunlu rutin, varilin önceki yönü ile yeni yönünü XOR'ladıktan sonra and $2000 uygular; gerçek donanımda bu 16 bitlik open bus okuması 0x2020 döndürür ve yön sınırı geçişi bununla belirlenir
  • ZSNES'teki gibi open bus okuması 0 olursa AND sonucu her zaman 0 olur, böylece dönüşü bitiren dallanma çalışmaz ve varil, ters yön tuşuna basılana kadar dönmeye devam eder
  • İlgili komutun büyük olasılıkla and #$2000 olması gerekiyordu; bir ROM revizyonunda $33EDAC adresindeki opcode 0x2D yerine 0x29 yapılırsa open bus olmadan da doğru çalışır

ZSNES'te dönen varilin durmaması sorunu

  • Donkey Kong Country 2'de bazı bölümlerdeki dönen varillerin ZSNES'te düzgün çalışmamasına yol açan eski bir hata bulunuyor
  • Normal çalışmada oyuncu, varilin içindeyken sadece sol veya sağ yön tuşunu basılı tuttuğu süre boyunca dönüşü kontrol edebilir
  • ZSNES'te sola ya da sağa bir kez basınca varil o yönde sürekli dönüyor, ters yöne basılınca da bu kez öteki yönde sürekli dönüyor
  • İleriki kısımlarda variller dikenlerin ya da tehlikelerin üzerine yerleştirildiği için bu hata, bölüm zorluğunu geliştiricilerin niyet ettiğinden çok daha fazla artırıyor
  • Aynı sorunun yaklaşık 20 yıl önce Anomie tarafından bulunup Snes9x'te düzeltildiği anlaşılıyor; o dönemdeki düzeltme, tam bir open bus emülasyonu değil, oyunun dayandığı belirli adres değerlerini sabit kodlama yoluyla yapıldı
  • ZSNES'te bu hata hiçbir zaman düzeltilmedi ve projenin son sürümü 2007 yılında çıktı

SNES'in open bus davranışı ve 65816 adresleme

  • SNES'te hatalı bir bellek adresi okumak genelde programın çökmesine yol açmaz
  • Eşlenmemiş bir adres okunduğunda CPU, veri yolunda en son bulunan değeri tekrar okur; buna open bus davranışı denir
  • SNES'in ana CPU'su 65C816 yani 65816'dır ve Ricoh 5A22 S-CPU paketinin içinde yer alır
  • 65816, 6502'nin 16 bitlik genişletilmiş bir CPU'sudur; SNES'te 24 bit adres yolu kullanılsa da adreslerin çoğu 8 bitlik bank ile 16 bitlik offset birleşiminden oluşur
  • Birçok komut dahili olarak 16 bit adres kullanır; instruction fetch sırasında program bank register, veri erişiminde ise data bank register devrededir
  • DKC2'nin dönen varil durumunda bank $B3 içindeki $2000 ve $2001 okunur; bu adresler bank $B3 içinde hiçbir yere eşlenmediğinden open bus okuması gerçekleşir

Sorunlu rutindeki bellek erişimi

  • Dönen varilin içindeyken sol ve sağ yön tuşları bırakıldığında her kare çalışan rutin bir open bus okuması yapar
  • Başlangıç durumunda data bank register $B3, direct page $0000'dır ve M/X bayraklarının ikisi de clear olduğu için register'lar ile bellek erişimleri 16 bittir
  • Bu rutin $0000-$2001 aralığındaki çeşitli adresleri kullanır
    • $0EE6: mevcut varil yönü
    • $0E0A: kare başına dönüş miktarı
    • $0032: geçici değişken gibi görünen konum
  • $00-$3F ve $80-$BF bank'larında $0000-$1FFF, konsolun 128KB WRAM'inin ilk 8KB'ına eşlenir
  • $2000-$20FF eşlenmemiş bir alan olduğundan and $2000 komutu bir open bus okuması olur

0x2020'nin dönüşün bitmesini belirlemede kullanılış biçimi

  • Rutin, mevcut yöne dönüş miktarını ekleyerek yeni yönü oluşturur ve bunu geçici değişkene kaydeder
  • Ardından önceki yön ile yeni yönü XOR'lar, sonuca and $2000 uygular ve sonucun 0 olup olmadığını kontrol eder
  • Gerçek SNES donanımında 16 bitlik bir open bus okuması olan and $2000, her zaman 0x2020 döndürür
    • and $2000 komutunun makine kodu 2D 00 20'dir
    • 65816, 8 bit veri yolu kullandığı için 16 bit okumayı iki adet 8 bit okuma olarak gerçekleştirir
    • Bu durumda en son okunan bayt adresin high byte'ı olan 0x20 olduğundan iki okumada da 0x20 döner
  • Bu nedenle gerçek davranış işlevsel olarak and #$2020 ile aynıdır
  • AND sonucu 0 ise yeni yön olduğu gibi kaydedilir ve dönüş sonraki karede de sürer
  • Sonuç 0 değilse dönüş miktarı 0 yapılır, ardından yeni yöne 0x1000 eklenir ve 0xE000 ile maskelenerek en yakın 0x2000 katı yöne hizalanır

ZSNES'te neden sürekli dönüyor

  • Varil yönü değerleri, 0x0000 aşağıyı ve 0x4000 solu gösterecek türde bir ölçek kullanıyor gibi görünüyor
  • İncelenen varilde saat yönündeki dönüş miktarı 0x0300, saat yönünün tersindeki dönüş miktarı ise 0xFD00; tam 360 derecelik dönüş biraz 85 karenin üzerinde sürüyor
  • Bu da 60fps esas alındığında 1,5 saniyeden biraz kısa bir süreye denk geliyor
  • 0x2020 ile yapılan AND işleminde gerçekte anlamlı olan değişim, bit 13 yani 0x2000
  • Yön değerinde 0x2000 değişimi, temel yön ya da çapraz yönlerde bir adımlık değişime karşılık gelir
  • Normal donanımda yön tuşu bırakıldığında varil bir sonraki temel/çapraz yöne kadar döner ve tam o yöne bakacak şekilde durur
  • Open bus okuması her zaman 0 döndürürse AND sonucu da her zaman 0 olur ve dönüşü sonlandıran kod hiçbir zaman çalışmaz

Yazım hatası olasılığı ve ROM yaması sonucu

  • and $2000 absolute addressing komutudur; mantıken bunun immediate addressing biçimi olan and #$2000 olması daha olasıdır
  • Gerçek donanımda open bus 0x2020 döndürdüğü için and $2000 tesadüfen amaçlandığı gibi çalışır
  • Bu tesadüfi davranış, kare başına dönüş miktarının alt 6 bitinin her zaman 0 olması durumunda 0x2020'nin işlevsel olarak 0x2000 ile aynı hale gelmesine dayanır
  • Hatalı opcode, bank $B3 içindeki $EDAC offset'inde çalıştırılır ve incelenen revizyonda oyunun 4MB ROM'u içinde $33EDAC adresine eşlenir
  • Bu bayt 0x2D yerine 0x29 yapılırsa, open bus okuması her zaman 0 dönse bile dönen variller doğru çalışır
  • Kesin ROM konumu oyunun diğer revizyonlarında farklı olabilir
  • Eski ZSNES dışında neredeyse tüm SNES emülatörlerinde oyun zaten doğru çalıştığından, bu örnek pratik bir yamadan çok donanıma bağımlı davranışın izini süren bir analiz niteliği taşıyor

1 yorum

 
GN⁺ 2025-07-02
Hacker News yorumları
  • 6502 assembly yazarken immediate değerin önüne # koymayı unutup bellek erişimi yapma gibi hatalar yüzünden hayatımdan çok zaman kaybettim.
    Bu örnekte olduğu gibi, tesadüfen bazı durumlarda çalışan şeyler de sık görülür. Daha kötüsü, kayan veri yolundan ziyade başlatılmamış RAM'e bağımlı olmaktır; DRAM'in özellikleri nedeniyle benim makinemde ya da emülatörde her zaman düzgün çalışırken, farklı bir DRAM çipi kullanan başkasının makinesinde bozulabilir. Bunu genellikle bir demoparty'de sunumdan 15 dakika önce parti makinesinde çalışmayınca fark edersiniz.

    • 6502 CPU ile dinamik bellek kullanan bir mimari olup olmadığını merak ediyorum. Sınırlı deneyimime göre o platformlar hep statik RAM kullanıyordu gibi geliyor.
    • 6502 ilk assembly dilimdi ve LDA #2 için “A'ya 2 sayısını yükle”, LDA 2 için de “bellek konumu 2'deki değeri A'ya yükle” diye hep ayrı düşünürdüm.
    • Böyle durumlarda kodu bir LLM'e vermek gerçekten yardımcı olabilir. Bu tür yazım hataları ve yanlışlıklar büyük etki yaratır ama insan gözünden çok kolay kaçar; LLM'ler bunları bulmada oldukça iyi.
  • Başlıktaki Open Bus büyük harfle yazıldığı için, önce hiç duymadığım eski bir veri yolu protokolünün ya da standardının özel adı sandım ve öyle okumaya başladım.
    Okuyunca, adres hattı kod çözücüsünün belirtilen $2000 adresinde hiçbir bellek aygıtını etkinleştirmediği, bu yüzden veri yolunun hiçbir şeye bağlı olmayan “açık” durumda olduğu anlamına geldiğini gördüm. Immediate addressing için gereken # işaretinin unutulması sorununun, eski bir emülatörün bellek okumayı gerçek donanımdaki gibi işlememesiyle ortaya çıkması oldukça komik. Düzeltme olarak absolute addressing yerine immediate addressing kullanılırsa bellek okuması yapılmayacağı için çalışma süresi de hızlanır. O kod parçasında kabaca 2µs kadar hızlanma olabilir; ama muhtemelen sadece bare-metal'de anlamlıdır, emülatörün zaten tam zamanlama doğruluğuna sahip olmama ihtimali büyük.

    • Rare'in, testlerde sorunsuz çalışıp yıllar sonra yeni bir mimaride gizli hatası ortaya çıkan birkaç oyun örneği var. Başka şirketlerde yok demek değil; bu konuda Rare yalnızca referans vermesi kolay bir isim.
      Donkey Kong 64'te bir bellek sızıntısı vardı; bildiğim kadarıyla o dönem için gerçekçi olmayan kadar uzun, 8-9 saatlik kesintisiz oynanıştan sonra oyun çöküyordu. Geliştirme sırasında yakalanmamıştı, ama emülatör save state kullanıp oyun içi kayıt sistemi yerine sürekli devam ederseniz bu süre hızla birikiyor. Yine de geçmişi biraz belirsiz. Bazı kaynaklar, Memory Pak'in kutuya dahil edilmesinin çökme süresini 8-9 saatten 13-20 saate iterek hatayı gizlemeye yönelik son dakika hamlesi olduğunu iddia ediyor; ancak son araştırmalar bunun tesadüf olduğu ve Rare ya da Nintendo'nun bu hatadan haberdar olarak oyunu yayımlamadığı yönüne işaret ediyor.
    • Bazı SNES emülatörleri bu noktada zamanlama açısından bile fiilen neredeyse kusursuz [0]. Yine de 2µs, istisnai durumlar dışında hissedilebilir bir fark yaratmayacaktır.
      [0] https://arstechnica.com/gaming/2021/06/how-snes-emulators-go...
  • RetroArch'ın RunAhead özelliği üzerinde çalışırken save state'lerin eşleşmediği noktayı kontrol ettiğim sırada, SNES Puyo Puyo'nun PPU open bus kullandığını görmüştüm.
    Durumu yükledikten sonra PPU Open Bus'tan okunan değer değiştiği için CPU yürütme izleme günlükleri eşleşmiyordu.

  • 6502 ailesine özgü hataları sürekli yapmam; ama yaptığımda genelde immediate değer yerine bellek adresi yazma hatası olur.
    Çok yaygın ve yapması kolay bir hata; Chuck Peddle'ın da immediate değerlere #$1234 gibi # ekleten sözdiziminden derin pişmanlık duyduğuna inanıyorum. IDE'de # karakterini parlak kırmızı gösterilecek şekilde ayarlamak biraz yardımcı oldu. Rare'in assembly tanrıları bile aynı probleme yakalanmış.

    • Uzun zaman önce GNU assembler'ın intel_syntax noprefix modunda benzer bir sorun yaşamıştım.
      Hem immediate değer hem de bellek adresi alabilen komutlarda, ileride tanımlanacak adlı bir sabit immediate değeri bilinmeyen sembol başvurusu olarak yorumlayabilen bir sözdizimsel belirsizlik vardı. Sonuçta beklenen immediate değer yerine, bağlama sırasında sembolün relocation adresiyle doldurulacak yer tutucu bir bellek adresine sahip komut olarak assemble ediliyordu. Hata ayıklaması eziyetti.
    • ARM gibi komut kümeleri böyle hataları fiilen imkânsız kıldı. Bellekle uğraşırken başka bir komut kullanmanız gerekiyor.
  • Böyle yazıları seviyorum. Assembly kodunu her zaman ancak %60 kadar takip edebiliyormuşum gibi hissediyorum; yanındaki düz yazı açıklama gerçekten yardımcı oluyor.
    Klasik yazılımların içinde kimsenin anlamadığı ya da belki bugüne kadar fark bile etmediği hata hikâyelerini duymak da eğlenceli.

    • Bu dönemin sistemlerini çekici kılan şeylerden biri, bugün neredeyse her yerde varsayılan saydığımız modern denetim mekanizmalarının olmamasıydı.
      Ağa bağlanabilen sistemler için zorunlu olan, tamamen izole gömülü mimarilere bile eklenebilecek kadar ucuzlamış mekanizmalardan söz ediyorum. Orijinal NES'te birçok okuma ve yazma, bir yerlerdeki hatlarda voltajı toggle etmekten ibaretti; sonrasında ne olacaksa oluyordu. İstenen etki, CRT blanking aralığını temsil eden sinyalle tam denk gelecek şekilde voltajı çok kontrollü biçimde toggle ederek elde ediliyordu. Super Mario Bros 3'ün bazı animasyonları, RAM çoklayıcısını toggle ederek birkaç sprite veri bankasından birinin seçilmesini sağlıyor ve grafik donanımı sprite'ı alırken görünümü biraz farklı olan tamamen başka bir çipten okumasına yol açıyordu. TV zamanlaması önemli olduğu için NTSC ve PAL TV bölgeleri için yazılımı ayrı çıkarmak gerekiyordu; iki standardın tarama hızları farklıydı ve bu tarama hızı, render mantığını çalıştıran clock'tu. Gerçekten sert zamanlardı.
  • Bildiğim kadarıyla open bus yalnızca basit senkron veri yolu kullanan erken dönem sistemlerde görülür.
    Diğer çoğu sistemde, var olmayan bir adrese erişildiğinde tamamı 0 ya da tamamı 1 olan sabit bir değer döndüğünü biliyorum. Çünkü veri yolu protokolünde master'ın yanıt olmadığını anlayabileceği bir el sıkışma mekanizması vardır; PCI terimleriyle bu “master abort”a karşılık gelir.

  • Open bus, kelimenin tam anlamıyla veri yolu hatlarının açık devre olması demektir.
    CPU eşlenmemiş ya da yalnızca yazılabilir bir adresi adres yoluna koymuştur ve veri yolundaki hiçbir donanım yanıt vermediği için bus hatları sürülmeden havada kalır. Nominal olarak donanım düzeyinde tanımsız davranıştır.
    Gerçekte ne olduğunu anlamak için veri yolunun fiziksel yapısına biraz daha bakmak gerekir. Anakart ve kartuş çevresinde sinyalleri taşıyan uzun iletkenler vardır; bunlar ince bir yalıtkan PCB katmanı üzerinden toprak düzleminden ayrılır. Bu bir kondansatöre benzer ve mühendisler de bunu gerçekten parazitik kapasitans olarak açıklar ve modeller. Bu etki bus’ın azami veri aktarım hızını sınırladığı için en aza indirilmeye çalışılır. Ama aynı etki yüzünden bus sürülmediğinde, en son sürüldüğü gerilimde kalma eğilimi gösterir. Küçük bir DRAM hücresi gibi davranır ve yazıda sözü edilen “open bus okuması, bus’tan en son geçen değeri döndürür” etkisi ortaya çıkar.
    DKC2’de olduğu gibi oyunların yanlışlıkla open bus etkisine bağımlı olması nadir değildir. NES’te kontrolcü bağlantısı için kullanılan seri port register’ı yalnızca alt bitleri sürer; üst bitler ise open bus’tır. Kontrolcü girişini LDA $4016 komutuyla okuyup $40 ya da $41 değerini bekleyen birkaç oyun da vardır. Buradaki 4, open bus yüzünden kalan değerdir.
    Bellek bozulması veya keyfi kod çalıştırma exploit’lerinin parçası olarak open bus davranışına dayanan speedrun stratejileri de vardır. Örneğin Super Mario World credits warp, program counter’ı eşlenmemiş belleğe gönderip uzun süre ilerlemesini sağlar; sonunda RAM’e ulaşır ve düşman konumlarını hassas biçimde manipüle ederek oluşturulmuş payload’u çalıştırır [1].
    Ancak genel olarak öngörülebilir open bus davranışının da istisnaları vardır. Standart dışı kartuşlar eşlenmemiş bellekte varsayılan değer döndürebilir ya da pull-up/pull-down dirençleri içererek open bus davranışını etkileyebilir. DMA ile ilginç etkileşimleri de vardır. SNES, HDMA adlı bir özelliği destekler; uygulama, kare ortasında veri yüklemek veya ayar değiştirmek için CPU’dan grafik donanımına tam zamanlamalı veri aktarımı planlayabilir [2]. Bu DMA aktarımı, aktarımı yapmak için bus’ı kullanacağından CPU’yu kısa süre durdurur; DMA aktarımı bir komutun yürütülmesinin ortasına, yani hedef adres okunduktan sonra ama asıl open bus okumasından önce girerse, open bus okuma davranışını değiştirebilir.
    Bu son derece özel uç durum, Super Metroid speedrun exploit’i [3] üzerinde büyük etki yapar. Bu exploit, sınır dışı bir memcpy oluşturarak open bus’tan RAM’e büyük bir veri bloğu taşımaya çalışır. Open bus okuması neredeyse her zaman 0 döndürür, çünkü ilgili load komutunun son baytı 0’dır. Ama HDMA grafik efektlerinin yoğun olduğu belirli odalarda DMA aktarımının bu okumalardan birini etkileme olasılığı epey yüksektir; bu durumda kritik bir konuma 0 olmayan bir bayt girer, exploit düzgün çalışmaz ve crash olur. Bu da topluluk içinde hafif bir tartışma yarattı. Bazı route’lar ve stratejiler yalnızca emülatörlerde ve standart dışı firmware’lerde kararlı çalışır. Orijinal donanım ya da çok doğru emülatör kullanan oyuncuların crash yaşama olasılığı yüksektir; buna karşılık Nintendo’nun tüm resmî yeniden yayımları da dahil çoğu emülatör, komut ortasındaki HDMA aktarımının open bus okuma değerini değiştirdiği bu özel uç durumu emüle etmez.
    Ayrıca şu anki en hızlı Super Metroid TAS bitirişi [4] bu HDMA etkileşimine dayanır. Open bus’ı çalıştırmaya çalışırken crash olan bir durum bulundu, ama normalde yararlı şekilde kontrol edilemiyordu. Odadaki düşmanlar manipüle edilerek CPU zamanlaması etkilendi ve HDMA ile doğru anda bus’a yararlı bir komut konabildi. Sonunda konsolun kontrolcü girişini kod olarak çalıştırması sağlandı ve tam keyfi kod çalıştırma elde edildi.
    [1]: https://youtu.be/vAHXK2wut_I
    [2]: https://youtu.be/K7gWmdgXPgk
    [3]: https://youtu.be/CnThmKhtfOs
    [4]: https://tasvideos.org/8214S

    • Veri yolunun fiziksel yapısına bakmak gerektiği kısmında bir kez daha Ben Eater’ı övmek istiyorum.
      6502 ile breadboard bilgisayar yaptığı video serisi sayesinde yazıdaki içeriği ve donanım sorunlarının açıklamasını gerçekten anlayabiliyorum. Elbette bu, onun temel bus örneğini ticari makinelere doğru genişleterek düşünmek demek. Yoksa neredeyse hiçbir şey bilmezdim.
      https://eater.net
  • Parallax Propeller çipini programlarken de bir ölçüde benzer bir sorun var.
    Belirtilen bellek konumuna atlayan JMP #address kullanmak gerekiyor, ama ben hep belirtilen bellek konumundan okunan adrese atlayan JMP address yazıveriyorum. Sanırım 6502 assembler hâlâ kas hafızamda kaldığı için.
    Propeller: JMP #address
    6502: JMP address
    Propeller: JMP address
    6502: JMP (address)
    En kötüsü de, bu yazıdaki gibi, bug’lı Propeller kodunun bazen çalışması. Sonra bir noktada duruyor ve nedenini bulmak için saatler harcıyorsunuz.

  • DKC 1’in SGI’da önceden render edilmiş 3D grafikleri o dönem için son teknolojiydi. Genesis’teki Vector Man de benzer bir şey yapmıştı ama daha az dikkat çekti.

    • 1995 civarında DKC’nin tam hedef kitlesi olan 11 yaşında bir çocuktum ve o oyun gerçekten sarsıcıydı.
      Gördüğüme inanamıyordum. Çıkışı civarında oyunu tanıtan ve geliştirme arkası hikâyeleri de gösteren bir video kaset vardı; sanırım mısır gevreği kutusu gibi bir yerden başvurup alınan bir promosyon materyaliydi. O kaseti gerçekten çok izledim. DKC’ye kendim sahip olamadım ama bir arkadaşımın evinde oynayabiliyordum.
    • Çocukken o grafikler bir şekilde sahteymiş gibi gelirdi. Çünkü bir düzeyde “gerçek” olmadığını sezebiliyordum.
      O dönemde dergiler bu konuda epey muğlak konuşur, SNES’in karakterleri falan gerçek zamanlı render ediyormuş gibi ima ederdi. Oysa gerçekte özünde flipbook animasyonuna daha yakındı.
  • Emülasyonla oyun oynarken bir yerde takılınca, sonunda bunun bir emülatör hatası olup olmadığını merak ediyorsunuz.
    Bu özel sorun için oyunun zaten böyle tasarlandığını ve sadece zor olduğunu düşünürdüm. Tam olarak bağlantılı değil ama oyun gerçekten zor hissettirdiğinde benzer şekilde “bu emülasyon gecikmesinden mi kaynaklanıyor?” diye düşünüyorum. Bu konuyu derinlemesine kurcalaya kurcalaya sonunda kendi MiSTer FPGA’mi yaptım.

    • Chrono Trigger’da da buna benzer bir şey vardı.
      Fareyi yakaladıktan sonra dört tuşa aynı anda basmanız gereken bir bölüm olduğunu hatırlıyorum. Ama USB girişi tek seferde yalnızca 3 tuşu ilettiği için, geçebilmek adına dört tuşa rastgele abanıp çok kısa bir süre içinde hepsinin eninde sonunda kaydedilmesini sağlamak gerekiyordu. Birçok kez denemek zorunda kalmıştım ve çok sinir bozucuydu.
    • DKC’yi yalnızca ZSNES’te oynamıştım ve yazıyı okuyana kadar bunun bir emülatör hatası olduğunu hiç bilmiyordum.
      Söylendiği gibi, fıçıdan fırlatılma zamanlamasını tutturup doğru açıyı yakalamanın kasıtlı bir oyun tasarımı olduğunu düşünmüştüm. Bunun bir hata olduğunu öğrenince gerçekten çok şaşırdım.
    • Çocukken Bionic Commando’yu çok oynardım.
      2000’lerin başında emülatörde çalıştırınca hatırladığımdan çok daha zor gelmişti. Sonradan anladım ki üssü havaya uçursanız bile düşmanların kaybolmamasına neden olan bir emülasyon hatası varmış ve Ladd hâlâ donup kalıyormuş. Bu yüzden bölümü bitirmek için yaklaşık 2 can dilimi daha gerekiyordu. Mümkün mü diye görmek için bir kez bu şekilde bitirdim, ama bir daha yapmadım.