- Beberapa bagian barel berputar di Donkey Kong Country 2 memiliki bug di emulator SNES lama ZSNES: barel tetap berputar meski tombol arah dilepas. Penyebabnya adalah perilaku open bus yang tidak diimplementasikan
- Pada SNES asli, saat membaca alamat yang tidak dipetakan, nilai terakhir pada data bus dibaca kembali, dan DKC2 bergantung pada karakteristik ini saat membaca $2000/$2001 di bank $B3
- Rutinitas bermasalah melakukan XOR antara arah barel sebelumnya dan arah baru, lalu menjalankan
and $2000; pada hardware asli, pembacaan open bus 16-bit ini mengembalikan 0x2020 untuk menentukan apakah batas arah telah dilewati - Jika pembacaan open bus bernilai 0 seperti di ZSNES, hasil AND selalu 0 sehingga cabang penghentian rotasi tidak dijalankan, dan barel terus berputar sampai tombol arah berlawanan ditekan
- Instruksi tersebut kemungkinan besar seharusnya
and #$2000; pada salah satu revisi ROM, mengubah opcode di $33EDAC dari0x2Dmenjadi0x29membuatnya bekerja normal tanpa open bus
Fenomena barel berputar yang tidak berhenti di ZSNES
- Donkey Kong Country 2 memiliki bug lama: barel berputar di beberapa stage tidak bekerja dengan benar di ZSNES
- Dalam perilaku normal, pemain hanya dapat mengendalikan rotasi selama menahan tombol arah kiri atau kanan saat berada di dalam barel
- Di ZSNES, jika tombol kiri atau kanan ditekan sekali, barel terus berputar ke arah tersebut; jika tombol arah berlawanan ditekan, barel terus berputar ke arah sebaliknya
- Pada bagian-bagian selanjutnya, barel ditempatkan di atas duri atau elemen berbahaya, sehingga bug ini membuat tingkat kesulitan stage jauh lebih tinggi daripada yang dimaksudkan developer
- Masalah yang sama tampaknya ditemukan oleh Anomie sekitar 20 tahun lalu dan diperbaiki di Snes9x; perbaikannya saat itu bukan emulasi open bus secara menyeluruh, melainkan hardcoding nilai alamat tertentu yang diandalkan game
- Di ZSNES, bug ini tidak pernah diperbaiki, dan rilis terakhir proyek tersebut adalah pada 2007
Open bus SNES dan pengalamatan 65816
- Di SNES, membaca alamat memori yang salah biasanya tidak membuat program crash
- Saat membaca alamat yang tidak dipetakan, terjadi perilaku open bus, yaitu CPU membaca kembali nilai yang terakhir berada di data bus
- CPU utama SNES adalah 65C816, yaitu 65816, yang disertakan dalam paket Ricoh 5A22 S-CPU
- 65816 adalah CPU perluasan 16-bit dari 6502, dan di SNES menggunakan bus alamat 24-bit, tetapi sebagian besar alamat dibentuk dari kombinasi bank 8-bit dan offset 16-bit
- Banyak instruksi secara internal menggunakan alamat 16-bit; instruction fetch melibatkan program bank register, sedangkan akses data melibatkan data bank register
- Dalam status barel berputar DKC2, game membaca $2000 dan $2001 di bank $B3, tetapi alamat-alamat ini tidak dipetakan ke mana pun di bank $B3 sehingga menjadi pembacaan open bus
Akses memori pada rutinitas bermasalah
- Saat tombol arah kiri/kanan dilepas di dalam barel berputar, rutinitas yang berjalan setiap frame melakukan pembacaan open bus
- Status awalnya adalah data bank register bernilai
$B3, direct page bernilai$0000, dan flag M/X sama-sama clear, sehingga register dan akses memori bersifat 16-bit - Rutinitas ini menggunakan beberapa alamat dalam rentang
$0000-$2001$0EE6: arah barel saat ini$0E0A: jumlah rotasi per frame$0032: lokasi yang tampaknya merupakan variabel sementara
- Pada bank $00-$3F dan $80-$BF,
$0000-$1FFFdipetakan ke 8KB pertama dari WRAM 128KB konsol $2000-$20FFadalah area yang tidak dipetakan, sehingga instruksiand $2000menjadi pembacaan open bus
Bagaimana 0x2020 digunakan untuk menentukan penghentian rotasi
- Rutinitas menambahkan jumlah rotasi ke arah saat ini untuk membuat arah baru, lalu menyimpannya ke variabel sementara
- Setelah itu, rutinitas melakukan XOR antara arah sebelumnya dan arah baru, menerapkan
and $2000pada hasilnya, lalu memeriksa apakah hasilnya 0 - Pada hardware SNES asli,
and $2000sebagai pembacaan open bus 16-bit selalu mengembalikan 0x2020- Kode mesin untuk
and $2000adalah2D 00 20 - 65816 menggunakan data bus 8-bit, sehingga pembacaan 16-bit dilakukan sebagai dua pembacaan 8-bit
- Dalam kasus ini, byte terakhir yang dibaca adalah high byte alamat, yaitu
0x20, sehingga kedua pembacaan mengembalikan0x20
- Kode mesin untuk
- Karena itu, perilaku nyatanya secara fungsional sama dengan
and #$2020 - Jika hasil AND adalah 0, arah baru disimpan apa adanya dan rotasi berlanjut pada frame berikutnya
- Jika tidak 0, jumlah rotasi diatur menjadi 0, lalu
0x1000ditambahkan ke arah baru dan dimasking dengan0xE000agar selaras ke arah kelipatan0x2000terdekat
Mengapa di ZSNES barel terus berputar
- Nilai arah barel tampaknya memakai skala seperti
0x0000menunjuk ke bawah dan0x4000menunjuk ke kiri - Untuk barel yang dianalisis, jumlah rotasi searah jarum jam adalah
0x0300, sedangkan jumlah rotasi berlawanan arah jarum jam adalah0xFD00, dan satu putaran 360 derajat memakan sedikit lebih dari 85 frame - Nilai ini sedikit lebih pendek dari 1,5 detik pada 60fps
- Dalam AND dengan
0x2020, perubahan yang benar-benar bermakna adalah bit 13, yaitu 0x2000 - Perubahan
0x2000pada nilai arah setara dengan satu langkah perubahan arah dasar atau arah diagonal - Pada hardware normal, saat tombol arah dilepas, barel berputar sampai arah dasar/diagonal berikutnya, lalu berhenti tepat menghadap arah tersebut
- Jika pembacaan open bus selalu mengembalikan 0, hasil AND juga selalu 0 sehingga kode penghentian rotasi tidak pernah dijalankan
Kemungkinan typo dan hasil patch ROM
and $2000adalah instruksi absolute addressing, dan secara logis kemungkinan besar seharusnya immediate addressingand #$2000- Pada hardware asli, karena open bus mengembalikan 0x2020,
and $2000kebetulan bekerja sesuai maksud - Perilaku kebetulan ini bergantung pada kondisi bahwa 6 bit terbawah dari jumlah rotasi per frame selalu 0, sehingga
0x2020secara fungsional menjadi sama dengan0x2000 - Opcode yang salah dijalankan pada bank
$B3offset$EDAC, dan pada revisi yang dianalisis dipetakan ke$33EDACdi dalam ROM game berukuran 4MB - Jika byte ini diubah dari
0x2Dmenjadi0x29, barel berputar bekerja normal meskipun pembacaan open bus selalu mengembalikan 0 - Lokasi ROM yang tepat dapat berbeda pada revisi game lain
- Karena game berjalan normal di hampir semua emulator SNES selain ZSNES lama, kasus ini lebih merupakan analisis yang menelusuri perilaku yang bergantung pada hardware daripada patch praktis
1 komentar
Opini Hacker News
Saya menghabiskan banyak waktu dalam hidup karena kesalahan seperti lupa menaruh
#di depan nilai immediate saat menulis assembly 6502, sehingga malah mengakses memoriSeperti kasus ini, sering kali secara kebetulan tetap berjalan dalam sebagian kondisi. Yang lebih buruk adalah ketika bergantung pada RAM yang belum diinisialisasi alih-alih floating bus; karena sifat DRAM, program bisa selalu berjalan baik di mesin atau emulator saya, lalu rusak di mesin orang lain yang memakai chip DRAM berbeda. Biasanya ini baru ketahuan di mesin party 15 menit sebelum presentasi di demoparty, ketika program tidak jalan
LDA #2berarti “muat angka 2 ke A”, sedangkanLDA 2berarti “muat nilai yang ada di lokasi memori 2 ke A”Karena Open Bus pada judul ditulis dengan huruf kapital, awalnya saya mulai membaca dengan mengira itu nama diri dari protokol atau standar bus lama yang belum pernah saya dengar
Setelah membaca, ternyata maksudnya adalah decoder jalur alamat tidak mengaktifkan perangkat memori apa pun pada alamat tertentu
$2000, sehingga bus berada dalam keadaan “terbuka” dan tidak tersambung ke apa pun. Cukup lucu bahwa masalah lupa#pada pengalamatan immediate baru terungkap karena emulator lama tidak menangani pembacaan memori seperti perangkat keras sungguhan. Dengan perbaikannya, memakai pengalamatan immediate alih-alih pengalamatan absolut tidak akan melakukan pembacaan memori, jadi waktu eksekusinya juga akan lebih cepat. Pada blok kode tersebut mungkin bisa lebih cepat sekitar 2µs, tetapi mungkin hanya bermakna di bare metal, dan emulator kemungkinan besar toh tidak memiliki akurasi timing yang sepenuhnya sempurnaSetahu saya Donkey Kong 64 memiliki memory leak sehingga game mati setelah 8–9 jam permainan terus-menerus, durasi yang tidak realistis untuk standar saat itu. Itu tidak tertangkap selama pengembangan, tetapi jika memakai save state emulator dan terus bermain alih-alih memakai fitur save dalam game, waktu itu cepat terakumulasi. Namun riwayatnya agak tidak jelas. Beberapa sumber mengklaim bundling Memory Pak adalah langkah menit terakhir untuk menyembunyikan bug dengan menggeser waktu crash dari 8–9 jam menjadi 13–20 jam, tetapi riset terbaru tampaknya menunjukkan itu kebetulan, dan Rare maupun Nintendo tidak merilisnya dalam keadaan mengetahui bug tersebut
[0] https://arstechnica.com/gaming/2021/06/how-snes-emulators-go...
Saat mengerjakan fitur RunAhead RetroArch dan memeriksa titik ketika save state tidak cocok, saya pernah melihat SNES Puyo Puyo memakai open bus PPU
Setelah memuat state, nilai yang dibaca dari PPU Open Bus berubah, sehingga log trace eksekusi CPU tidak cocok
Saya tidak selalu membuat kesalahan keluarga 6502 seperti ini, tetapi kalau terjadi, biasanya kesalahannya adalah memakai alamat memori alih-alih nilai immediate
Ini kesalahan yang sangat umum dan mudah dilakukan, dan saya percaya Chuck Peddle sendiri sangat menyesali sintaks yang memberi
#pada nilai immediate seperti#$1234. Saya membuat#tampil merah terang di IDE, dan itu sedikit membantu. Para dewa assembly di Rare pun terkena masalah yang samaintel_syntax noprefixmilik GNU assemblerPada instruksi yang bisa menerima nilai immediate maupun alamat memori, ada ambiguitas sintaks yang memungkinkan konstanta bernama yang direferensikan ke depan sebagai nilai immediate ditafsirkan sebagai referensi simbol yang belum diketahui. Akibatnya, alih-alih nilai immediate yang diharapkan, kode di-assemble menjadi instruksi dengan alamat memori placeholder yang akan diisi dengan alamat relokasi simbol saat linking. Debugging-nya menyiksa
Saya suka tulisan seperti ini. Saya selalu merasa hanya bisa mengikuti sekitar 60% kode assembly, jadi penjelasan prosa di sampingnya benar-benar membantu
Menarik juga mendengar cerita tentang bug di perangkat lunak klasik yang tidak dipahami siapa pun, atau mungkin bahkan belum pernah disadari sampai sekarang
Maksudnya perangkat yang wajib ada pada sistem yang bisa terhubung ke jaringan, dan sudah cukup murah untuk dipasang bahkan pada arsitektur embedded yang sepenuhnya terisolasi. Pada NES asli, banyak operasi baca dan tulis pada dasarnya hanya men-toggle tegangan pada suatu jalur, lalu apa pun yang terjadi setelahnya dibiarkan terjadi. Efek yang diinginkan diperoleh dengan men-toggle tegangan secara sangat terkendali dan tepat selaras dengan sinyal yang menandai interval blanking CRT. Sebagian animasi di Super Mario Bros 3 men-toggle multiplexer RAM untuk memilih salah satu dari beberapa bank data sprite, sehingga saat perangkat keras grafis mengambil sprite, ia membaca dari chip yang sepenuhnya berbeda dengan tampilan yang sedikit berbeda. Karena timing TV penting, perangkat lunak untuk wilayah TV NTSC dan PAL juga harus dirilis terpisah; keduanya memiliki refresh rate berbeda, dan refresh rate itulah clock yang menggerakkan logika rendering. Benar-benar era yang keras
Setahu saya open bus hanya terlihat pada sistem awal yang memakai bus sinkron sederhana
Pada kebanyakan sistem lain, akses ke alamat yang tidak ada akan mengembalikan nilai konstan yang penuh 0 atau penuh 1. Itu karena protokol bus memiliki handshake yang memungkinkan master mengetahui bahwa tidak ada respons, dan dalam istilah PCI ini disebut “master abort”
Open bus secara harfiah berarti jalur bus data berada dalam rangkaian terbuka
CPU menaruh alamat yang tidak dipetakan atau hanya untuk tulis ke address bus, dan karena tidak ada hardware di bus yang merespons, jalur bus tidak digerakkan dan berada dalam keadaan mengambang. Secara nominal, ini adalah perilaku yang tidak terdefinisi pada level hardware
Untuk memahami apa yang sebenarnya terjadi, kita perlu melihat sedikit lebih jauh struktur fisik data bus. Ada konduktor panjang yang membawa sinyal di sekitar motherboard dan cartridge, terpisah dari ground plane oleh lapisan tipis substrat isolator. Ini tampak seperti kapasitor, dan memang para engineer menjelaskan serta memodelkannya sebagai kapasitansi parasitik. Efek ini membatasi laju transfer data maksimum bus, jadi biasanya berusaha diminimalkan. Namun karena efek ini, saat bus tidak digerakkan, ia cenderung tetap pada tegangan yang terakhir menggerakkannya. Ia bertindak seperti sel DRAM kecil, sehingga muncul efek yang disebut dalam tulisan: “pembacaan open bus mengembalikan nilai terakhir yang melewati bus”
Tidak jarang game seperti DKC2 tanpa sengaja bergantung pada efek open bus. Di NES, register port serial untuk koneksi controller hanya menggerakkan bit rendah, sementara bit tinggi adalah open bus. Ada beberapa game yang membaca input controller dengan instruksi
LDA $4016dan mengharapkan nilai$40atau$41. Di sini angka 4 adalah nilai yang tersisa karena open busAda juga strategi speedrun yang bergantung pada perilaku open bus sebagai bagian dari eksploitasi korupsi memori atau eksekusi kode arbitrer. Misalnya, credits warp di Super Mario World mengirim program counter ke memori yang tidak dipetakan dan membiarkannya berjalan cukup lama hingga akhirnya mencapai RAM, lalu mengeksekusi payload yang dibuat lewat manipulasi posisi musuh secara presisi [1]
Namun, perilaku open bus yang umumnya bisa diprediksi juga punya pengecualian. Cartridge nonstandar dapat mengembalikan nilai default untuk memori yang tidak dipetakan, atau menyertakan resistor pull-up/pull-down yang memengaruhi perilaku open bus. Ada juga interaksi menarik dengan DMA. SNES mendukung fitur bernama HDMA, yang memungkinkan aplikasi menjadwalkan transfer data dari CPU ke hardware grafis pada timing yang tepat untuk mengunggah data atau mengubah pengaturan di tengah frame [2]. Transfer DMA ini menghentikan CPU sejenak agar dapat memakai bus untuk transfer, dan jika transfer DMA menyela di tengah eksekusi instruksi—yakni setelah alamat target dibaca tetapi sebelum pembacaan open bus sebenarnya—maka ia dapat mengubah perilaku pembacaan open bus
Kasus tepi yang sangat spesifik ini berdampak besar pada eksploitasi speedrun Super Metroid [3]. Eksploitasi ini memicu
memcpydi luar batas dan mencoba memindahkan blok data besar dari open bus ke RAM. Pembacaan open bus hampir selalu mengembalikan 0, karena byte terakhir dari instruksi load terkait adalah 0. Namun di ruangan tertentu yang memiliki banyak efek grafis HDMA, cukup besar kemungkinan transfer DMA memengaruhi salah satu pembacaan itu, sehingga byte nonnol terselip di posisi penting dan eksploitasi tidak berjalan semestinya lalu crash. Hal ini memicu perdebatan ringan di komunitas. Sebagian route dan strategi hanya stabil di emulator dan firmware nonstandar. Pemain yang memakai hardware asli atau emulator yang sangat akurat lebih mungkin mengalami crash, tetapi sebagian besar emulator, termasuk semua rilis ulang resmi dari Nintendo, tidak mengemulasikan kasus tepi khusus ketika transfer HDMA di tengah instruksi mengubah nilai pembacaan open bus iniSelain itu, penyelesaian TAS Super Metroid tercepat saat ini [4] bergantung pada interaksi HDMA ini. Ditemukan situasi ketika mencoba mengeksekusi open bus menyebabkan crash, tetapi biasanya tidak bisa dikendalikan secara berguna. Dengan memanipulasi musuh di dalam ruangan untuk memengaruhi timing CPU, HDMA bisa menaruh instruksi yang berguna ke bus pada timing yang tepat. Pada akhirnya, konsol dibuat mengeksekusi input controller sebagai kode, sehingga mencapai eksekusi kode arbitrer sepenuhnya
[1]: https://youtu.be/vAHXK2wut_I
[2]: https://youtu.be/K7gWmdgXPgk
[3]: https://youtu.be/CnThmKhtfOs
[4]: https://tasvideos.org/8214S
Berkat seri videonya tentang membuat komputer breadboard dengan 6502, isi tulisan dan penjelasan masalah hardware ini benar-benar bisa dipahami. Tentu saja, ini dengan membayangkan contoh bus dasarnya diperluas ke mesin komersial. Kalau tidak, saya mungkin hampir tidak akan memahaminya
https://eater.net
Saat memprogram chip Parallax Propeller, ada masalah yang agak mirip
Seharusnya memakai
JMP #addressuntuk melompat ke lokasi memori yang ditentukan, tetapi saya selalu memakaiJMP address, yang melompat ke alamat yang dibaca dari lokasi memori yang ditentukan. Sepertinya assembler 6502 masih tertanam dalam muscle memoryPropeller:
JMP #address6502:
JMP addressPropeller:
JMP address6502:
JMP (address)Yang paling buruk, seperti dalam tulisan ini, kode Propeller yang bug kadang-kadang tetap berjalan. Lalu pada suatu saat berhenti, dan kita menghabiskan berjam-jam mencari tahu sebabnya
Grafis 3D pra-render SGI di DKC 1 adalah teknologi mutakhir pada masanya. Vector Man di Genesis juga melakukan hal serupa, tetapi kurang mendapat sorotan
Saya tidak percaya dengan apa yang saya lihat. Menjelang rilisnya, ada videotape yang mempratinjau game itu dan juga memperlihatkan cerita di balik pengembangannya; seingat saya itu materi promosi yang didapat dengan mengajukannya lewat sesuatu seperti kotak sereal. Saya menonton kaset itu berkali-kali. Saya tidak pernah punya DKC sendiri, tetapi bisa memainkannya di rumah teman
Majalah-majalah waktu itu membicarakan topik ini dengan cukup samar, sering mengisyaratkan seolah-olah kemampuan SNES merender karakter dan sebagainya secara real-time. Padahal sebenarnya itu pada dasarnya lebih mirip animasi flipbook
Saat bermain game lewat emulasi lalu mentok, pada akhirnya kita sering jadi bertanya-tanya apakah itu bug emulator
Kalau untuk masalah spesifik ini, saya mungkin akan mengira gamenya memang dirancang seperti itu dan memang sulit. Tidak sepenuhnya terkait, tetapi saat sebuah game terasa benar-benar sulit, saya juga berpikir serupa, “Apakah ini karena latensi emulasi?” Setelah menyelami masalah ini cukup dalam, akhirnya saya membuat sendiri MiSTer FPGA
Seingat saya, ada bagian setelah menangkap tikus di mana kita harus menekan empat tombol sekaligus. Namun input USB hanya meneruskan 3 tombol dalam satu waktu, jadi agar bisa lewat, kita harus menekan keempat tombol itu secara membabi buta supaya akhirnya semuanya terdaftar dalam waktu yang sangat singkat. Harus mencoba berkali-kali dan itu sangat menjengkelkan
Seperti yang disebutkan, saya hanya mengira mencocokkan waktu peluncuran dari barrel untuk mendapatkan sudut yang tepat adalah desain game yang memang disengaja. Saya benar-benar terkejut setelah tahu itu ternyata bug
Ketika menjalankannya dengan emulator pada awal 2000-an, rasanya jauh lebih sulit daripada yang saya ingat. Belakangan saya tahu ada bug emulasi yang membuat musuh tidak menghilang meskipun markas diledakkan, dan Ladd tetap membeku. Jadi untuk menyelesaikan level, kira-kira perlu 2 bar nyawa tambahan. Saya pernah menamatkannya sekali seperti itu untuk melihat apakah mungkin, tetapi tidak pernah melakukannya lagi