1 poin oleh GN⁺ 2025-07-02 | 1 komentar | Bagikan ke WhatsApp
  • 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 dari 0x2D menjadi 0x29 membuatnya 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-$1FFF dipetakan ke 8KB pertama dari WRAM 128KB konsol
  • $2000-$20FF adalah area yang tidak dipetakan, sehingga instruksi and $2000 menjadi 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 $2000 pada hasilnya, lalu memeriksa apakah hasilnya 0
  • Pada hardware SNES asli, and $2000 sebagai pembacaan open bus 16-bit selalu mengembalikan 0x2020
    • Kode mesin untuk and $2000 adalah 2D 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 mengembalikan 0x20
  • 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 0x1000 ditambahkan ke arah baru dan dimasking dengan 0xE000 agar selaras ke arah kelipatan 0x2000 terdekat

Mengapa di ZSNES barel terus berputar

  • Nilai arah barel tampaknya memakai skala seperti 0x0000 menunjuk ke bawah dan 0x4000 menunjuk ke kiri
  • Untuk barel yang dianalisis, jumlah rotasi searah jarum jam adalah 0x0300, sedangkan jumlah rotasi berlawanan arah jarum jam adalah 0xFD00, 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 0x2000 pada 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 $2000 adalah instruksi absolute addressing, dan secara logis kemungkinan besar seharusnya immediate addressing and #$2000
  • Pada hardware asli, karena open bus mengembalikan 0x2020, and $2000 kebetulan bekerja sesuai maksud
  • Perilaku kebetulan ini bergantung pada kondisi bahwa 6 bit terbawah dari jumlah rotasi per frame selalu 0, sehingga 0x2020 secara fungsional menjadi sama dengan 0x2000
  • Opcode yang salah dijalankan pada bank $B3 offset $EDAC, dan pada revisi yang dianalisis dipetakan ke $33EDAC di dalam ROM game berukuran 4MB
  • Jika byte ini diubah dari 0x2D menjadi 0x29, 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

 
GN⁺ 2025-07-02
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 memori
    Seperti 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

    • Saya penasaran apakah pernah ada arsitektur yang memakai CPU 6502 bersama memori dinamis. Dari pengalaman saya yang terbatas, rasanya platform itu selalu memakai RAM statis
    • 6502 adalah bahasa assembly pertama saya, dan saya selalu membedakannya seperti LDA #2 berarti “muat angka 2 ke A”, sedangkan LDA 2 berarti “muat nilai yang ada di lokasi memori 2 ke A”
    • Dalam situasi seperti ini, memasukkan kode ke LLM sebenarnya bisa membantu. Salah ketik atau kesalahan seperti ini berdampak besar tetapi terlalu mudah terlewat oleh mata manusia, sementara LLM cukup pandai menemukannya
  • 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 sempurna

    • Rare punya beberapa contoh game yang berjalan baik saat pengujian, tetapi bug tersembunyinya baru muncul bertahun-tahun kemudian pada arsitektur baru. Bukan berarti perusahaan lain tidak begitu; hanya saja Rare adalah nama yang mudah dirujuk untuk topik ini
      Setahu 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
    • Sebagian emulator SNES pada titik ini pada dasarnya hampir sempurna bahkan sampai timing [0]. Meski begitu, 2µs tidak akan membuat perbedaan yang terasa kecuali dalam kasus yang sangat khusus
      [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 sama

    • Dulu sekali saya mengalami masalah serupa dalam mode intel_syntax noprefix milik GNU assembler
      Pada 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
    • Set instruksi seperti ARM pada dasarnya membuat kesalahan seperti itu mustahil. Untuk menangani memori, harus memakai instruksi yang berbeda
  • 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

    • Salah satu alasan sistem pada era ini menarik adalah tidak adanya mekanisme pemeriksaan modern yang kini hampir di mana-mana kita anggap sebagai bawaan
      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 $4016 dan mengharapkan nilai $40 atau $41. Di sini angka 4 adalah nilai yang tersisa karena open bus
    Ada 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 memcpy di 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 ini
    Selain 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

    • Pada bagian yang mengatakan kita perlu melihat struktur fisik data bus, saya ingin sekali lagi memuji Ben Eater
      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 #address untuk melompat ke lokasi memori yang ditentukan, tetapi saya selalu memakai JMP address, yang melompat ke alamat yang dibaca dari lokasi memori yang ditentukan. Sepertinya assembler 6502 masih tertanam dalam muscle memory
    Propeller: JMP #address
    6502: JMP address
    Propeller: JMP address
    6502: 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

    • Sekitar tahun 1995, saya adalah anak berusia 11 tahun yang tepat menjadi target audiens DKC, dan game itu benar-benar mengejutkan
      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
    • Saat kecil, grafis itu terasa agak palsu. Karena pada tingkat tertentu, kita bisa merasakan bahwa itu bukan “sungguhan”
      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

    • Di Chrono Trigger juga ada hal seperti ini
      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
    • Saya hanya pernah memainkan DKC di ZSNES, dan sebelum membaca tulisan ini sama sekali tidak tahu bahwa ini adalah bug emulator
      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
    • Waktu kecil saya banyak memainkan Bionic Commando
      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