1 पॉइंट द्वारा GN⁺ 2025-07-02 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Donkey Kong Country 2 का घूमने वाला barrel bug ZSNES emulator में होता है
  • ZSNES open bus behavior को सही तरह से emulate नहीं करता, जिससे barrel हमेशा के लिए घूमता रहता है
  • असली hardware से अलग, ZSNES में गलत memory access पर हमेशा 0 return होता है, जिससे bug ट्रिगर होता है
  • सही behavior में barrel ठीक दिशा (8 दिशाओं) पर घूमना बंद करने वाली logic का पालन करता है
  • माना जाता है कि यह समस्या coding की एक छोटी गलती (यानी immediate addressing की जगह absolute addressing का उपयोग) से पैदा हुई

Donkey Kong Country 2 और ZSNES emulator का barrel bug

Donkey Kong Country 2 में ZSNES नाम के पुराने SNES emulator पर कुछ stages में घूमने वाले barrel ठीक से काम नहीं करते — यह एक मशहूर bug है

जब खिलाड़ी barrel में प्रवेश करता है, तो सामान्यतः barrel को केवल उतनी देर घूमना चाहिए जितनी देर तक left/right direction key दबाई जाए। लेकिन ZSNES में left/right को हल्का-सा दबाने पर भी barrel उसी दिशा में हमेशा घूमता रहता है

इस bug की वजह से, खासकर बाद के stages में कांटों या obstacles के ऊपर आने वाले rotating barrel sections, developers की मंशा से कहीं अधिक कठिन हो जाते हैं

यह समस्या पहले ZSNES forum में कुछ हद तक documented थी, लेकिन अब forum गायब हो चुका है, इसलिए उससे जुड़ी सामग्री ढूँढना मुश्किल है

bug का कारण - Open Bus Emulation

इस bug की जड़ में ZSNES द्वारा open bus behavior को emulate न कर पाना है

  • open bus पुराने platforms जैसे SNES पर invalid memory address read के समय होने वाला behavior है
  • असली hardware में bus पर आख़िरी बार रखा गया value वापस मिलता है
  • SNES का मुख्य CPU 65C816 (65816) है
  • 65816, 6502 का 16-bit version है, जिसमें 24-bit address bus और memory banking method होता है

DKC2 के rotating barrel code में, invalid addresses (Bank $B3 के $2000, $2001) को access करने पर hardware में open bus के कारण 0x2020 value लौटती है

ZSNES में यह feature नहीं है, इसलिए हमेशा 0 return होता है और bug पैदा होता है

game code कैसे काम करता है

rotating barrel से जुड़ा game routine इस तरह काम करता है

  • मौजूदा barrel direction और rotation amount (speed) को जोड़कर एक temporary variable में store किया जाता है
  • XOR operation से direction change को मापा जाता है, और उसके result पर open bus से पढ़े गए value के साथ AND operation किया जाता है
  • अगर AND result 0 हो तो rotation जारी रहती है, और अगर 0 न हो तो rotation रुक जाती है और direction को 8 दिशाओं में से किसी एक पर round करके align किया जाता है

असली hardware में open bus value 0x2020 होती है, लेकिन अगर 0 return हो तो rotation अनंत समय तक चलती रहती है

माना जाता है कि इस logic में मूल रूप से AND operation को immediate value (address #$2000) के साथ होना चाहिए था, लेकिन गलती से absolute address (address $2000) का उपयोग किया गया

लेकिन hardware के open bus गुण के कारण, असल में दोनों तरीके सामान्य रूप से काम करते हैं

समाधान और निष्कर्ष

Snes9x जैसे दूसरे SNES emulators ने इस bug को hardcoded तरीके से fix किया, जबकि ZSNES का development बंद हो चुका है, इसलिए इसे patch नहीं किया गया

अगर उस routine में AND instruction का opcode 0x2D से 0x29 (AND #$2000) में बदल दिया जाए, तो open bus behavior के बिना भी rotating barrel सामान्य रूप से काम करता है

यह समस्या असली hardware या modern emulators में नहीं होती

आख़िरकार, यह bug open bus emulation की कमी और coding mistake के साथ मिलकर पैदा हुई समस्या का एक उदाहरण है


अतिरिक्त पृष्ठभूमि: 65816 संरचना और SNES memory map

65816 CPU में 24-bit address bus होती है, लेकिन आम तौर पर 8-bit bank और 16-bit offset के संयोजन का उपयोग होता है

  • program counter (PC) 16-bit होता है, और program bank register (PBR, K) के साथ मिलकर पूरा address बनाता है
  • data bank (DBR, B) का उपयोग data operations के लिए bank चुनने में होता है
  • hardware stack और direct page हमेशा $00 bank में मौजूद रहते हैं

SNES memory map भी 65816 पर आधारित है, इसलिए addresses को 8-bit bank + 16-bit offset के रूप में समझना अधिक प्रभावी है

समापन

यह उदाहरण दिखाता है कि legacy hardware की विशेषताएँ (जैसे open bus) emulation में अप्रत्याशित bugs पैदा कर सकती हैं

developer को immediate addressing का उपयोग करना चाहिए था, लेकिन संयोग से absolute address भी सामान्य रूप से काम करता रहा

आज के समय में open bus behavior तक को emulate करना पुराने software के सटीक पुनर्निर्माण के लिए बहुत महत्वपूर्ण है

1 टिप्पणियां

 
GN⁺ 2025-07-02
Hacker News राय
  • 6502 assembly programmer के रूप में मैंने # चिन्ह छोड़ देने और immediate value की जगह memory access कर बैठने वाली गलती में बेहिसाब समय गंवाया है, और यह समस्या इसलिए और भी सिरदर्द बनती है क्योंकि कभी-कभी ऐसी गलती किस्मत से सही भी चल जाती है। लेकिन दिए गए उदाहरण में floating bus समस्या से भी बदतर मामला वह होता है जब code uninitialized RAM पर निर्भर करता है; हर DRAM का शुरुआती मान अलग हो सकता है, इसलिए अपने कंप्यूटर या emulator पर सब ठीक चलता है, लेकिन किसी दूसरे DRAM वाले सिस्टम पर फेल हो जाता है। अक्सर demoparty में, जब किसी और के hardware पर code चलाना होता है और 15 मिनट से भी कम समय बचा हो, तभी ऐसी समस्या पकड़ में आती है

    • जिज्ञासा है कि क्या 6502 CPU के साथ dynamic memory इस्तेमाल करने वाली कोई architecture वास्तव में थी। मेरे अनुभव में उस platform पर हमेशा static RAM ही इस्तेमाल हुई थी

    • 6502 मेरी पहली assembly language थी, और LDA #2 मुझे हमेशा “A register में संख्या 2 load करो” जैसा लगता था। दूसरी ओर LDA 2 का अर्थ “memory location 2 का मान load करो” जैसा महसूस होता था, इसलिए मैं शुरुआत से ही इस फर्क को ध्यान में रखकर गलती से बचने की कोशिश करता था

    • ऐसे मामलों में code को LLM से गुजारकर देखना उल्टा उपयोगी हो सकता है। बड़ी impact वाली typo या गलती की जगह पकड़ने में LLM अक्सर अच्छे होते हैं

  • मैंने Open Bus शब्द को capital letters में देखकर पहले सोचा कि यह कोई पुराना bus protocol या standard होगा, और उसी समझ के साथ लेख पढ़ना शुरू किया। बाद में समझ आया कि इसका मतलब बस इतना है कि bus किसी चीज़ से जुड़ी नहीं थी, क्योंकि address decoder द्वारा चुने गए address ($2000) पर कोई memory device सक्रिय नहीं हुआ था। # छूट जाने से immediate mode की बजाय memory read हो रही थी, लेकिन पुराने emulator ने असली hardware से अलग व्यवहार किया और इसी से यह सामने आया। समाधान के रूप में directive को immediate addressing mode में बदल देने पर अब memory read होती ही नहीं, इसलिए code लगभग 2us तेज भी हो जाता है। लेकिन इतना performance फर्क असली hardware के बाहर — खासकर उन emulators में जो timing को पूरी तरह match नहीं करते — शायद बहुत मायने नहीं रखता

    • बताया गया कि (कुछ) SNES emulators अब लगभग time-based perfection तक पहुँच चुके हैं। फिर भी 2us का फर्क वास्तव में बहुत ही अपवादजनक स्थितियों को छोड़ दें तो लगभग नज़र नहीं आता। संबंधित लेख: How SNES emulators got a few pixels from complete perfection

    • Rare जैसी कंपनियों के कई ऐसे games रहे हैं जिनमें release के बहुत बाद, नई architectures की वजह से दबी हुई bugs सामने आईं। Donkey Kong 64 में 8–9 घंटे लगातार खेलने पर एक fatal memory leak होता है, लेकिन emulator की save सुविधा के कारण वह समय एकदम से accumulate हो जाता है और bug आसानी से दिख जाता है। यह भी एक theory रही है कि release के समय साथ दिया गया Memory Pak इसी bug को छिपाने के लिए था, लेकिन हालिया शोध के मुताबिक Rare और Nintendo दोनों को उस समय इस bug की जानकारी नहीं थी

  • SNES Puyo Puyo में मैंने PPU open bus behavior का सामना किया था। यह RetroArch में RunAhead feature पर काम करते समय save state mismatch का कारण खोजने के दौरान मिला, जहाँ PPU open bus से पढ़ी गई value state load के बाद बदल जाती थी, इसलिए CPU execution trace log मेल नहीं खाता था

  • 6502 या उससे मिलते-जुलते code में मैं भी अक्सर memory address और immediate value को गड़बड़ा देता हूँ। #$1234 जैसी notation मुझे गलती करवाने वाली लगती है, और मैंने यह भी सुना है कि Chuck Peddle तक ने इस syntax पर गहरा पछतावा जताया था। IDE में # को लाल रंग से highlight करने पर इससे कुछ बचाव हो जाता था। Rare के developers भी ऐसी गलती से नहीं बच सके

    • बहुत पहले GNU assembler में intel_syntax noprefix mode के साथ मुझे इसी तरह की समस्या हुई थी, जहाँ immediate named constant को आगे से refer करने पर syntax इतनी ambiguous थी कि उसे memory address या symbol दोनों तरह से समझा जा सकता था। नतीजतन उम्मीद के उलट एक अस्थायी memory address बन गया जो symbol के link होने तक इंतज़ार करता रहा, और bug ढूँढना बेहद दर्दनाक अनुभव था

    • ARM जैसे instruction set, जहाँ memory को अलग निर्देशों से संभालना पड़ता है, ऐसी उलझाने वाली गलतियों को मूल रूप से रोक देते हैं

  • मेरी जानकारी में open bus behavior केवल शुरुआती सरल synchronous bus systems में दिखाई देता है। ज़्यादातर दूसरे systems में non-existent address पर access करने पर all-0 या all-1 जैसा कोई स्थिर मान लौटाया जाता है, या bus protocol में handshake के ज़रिए — जैसे PCI का master abort — यह पता चल जाता है कि कोई response नहीं आया

  • Parallax Propeller chip को program करते समय मैंने बार-बार ऐसी ही गलती की है। JMP #address और JMP address का फर्क मैं अक्सर गड़बड़ा देता हूँ, शायद 6502 assembler की muscle memory की वजह से। Propeller में JMP #address का मतलब दिए गए address पर jump करना है, जबकि JMP address का मतलब उस address से पढ़े गए मान पर jump करना है। समस्या यह है कि ऐसा bug कभी-कभी सही भी चल जाता है, इसलिए जब तक behavior टूट न जाए, कारण खोजने में घंटों निकल जाते हैं

  • open bus का मतलब है कि data bus lines सचमुच open रह जाती हैं, यानी circuit electrically खुला रह जाता है। जब CPU किसी unmapped या write-only address को bus पर रखता है, तो कोई hardware response नहीं देता और bus lines floating स्थिति में रह जाती हैं — यानी hardware level पर undefined behavior। वास्तव में क्या होगा, यह समझने के लिए data bus की physical structure देखनी पड़ती है। Bus motherboard और cartridge के बीच signals ले जाने वाला लंबा conductor होता है, जो एक पतले insulating substrate के ऊपर ground plane से अलग रहता है। यह संरचना capacitor की तरह काम करती है, इसलिए थोड़ी देर तक पिछला signal voltage “पकड़े” रखती है। इसी वजह से open bus में आखिरी भेजा गया मान फिर से पढ़ा हुआ दिखाई देता है। DKC2 जैसे games अनजाने में इसी open bus property पर निर्भर कर लेते हैं, और NES का controller serial port भी केवल lower bits पर signal देता है जबकि upper bits open bus होते हैं, इसलिए कुछ games LDA $4016 के साथ $40 या $41 की अपेक्षा करते हैं। open bus behavior का इस्तेमाल Super Mario World credits warp जैसी speedrun strategies में भी किया जाता है, चाहे वह memory corruption हो या arbitrary code execution। हालांकि non-standard cartridges, pull-up/pull-down resistors, या DMA के साथ असामान्य interactions — जैसे Horizontal DMA — अलग नतीजे पैदा कर सकते हैं। उदाहरण के लिए, अगर SNES का HDMA transfer instruction के बीच में हो जाए तो open bus read timing बदल जाती है, और Super Metroid speedrun exploit में जिन memory blocks को copy करना होता है उनके बीच असामान्य values आ सकती हैं, जिससे exploit टूट जाता है। इसलिए original hardware या बहुत सटीक emulator पर crash हो सकता है, जबकि ज़्यादातर emulators या official re-releases में ऐसा niche behavior पूरी तरह implement नहीं होता और strategy सामान्य रूप से काम करती है। Super Metroid TAS world record completion भी इसी HDMA behavior पर निर्भर करती है। Enemy positions को manipulate करके CPU timing बदली जाती है ताकि HDMA open bus पर वांछित value रखे, और अंततः controller input को code की तरह execute करवा कर arbitrary code execution तक पहुँचा जा सके Super Mario World credits warp वीडियो, HDMA उपयोग वीडियो, Super Metroid DMA exploit वीडियो, Super Metroid TAS रिकॉर्ड

    • Ben Eater की 6502 breadboard computer वीडियो श्रृंखला ने यह समझने में बहुत मदद की कि ऐसा hardware behavior कैसे काम करता है। इससे यह भी महसूस होता है कि commercial devices में यही bus behavior किस तरह फैलता है Ben Eater साइट
  • मुझे इस तरह की दिलचस्प bug analysis सामग्री बहुत पसंद है। मैं assembly code का शायद सिर्फ 60% ही मुश्किल से समझ पाता हूँ, लेकिन साथ में दी गई लिखित व्याख्या समझ को काफी बढ़ा देती है। और इतने लंबे समय तक किसी को न पता चली bug का किसी महान software में बाद में सामने आना, ऐसी कहानियाँ खास तौर पर मज़ेदार लगती हैं

    • उस दौर के systems में आज के embedded systems में ज़रूरी समझे जाने वाले अधिकांश checks थे ही नहीं, चाहे वह network connectivity की संभावना की वजह से हों या नहीं, और यही बात उन्हें और रोचक बनाती है। NES के ज़माने में countless read/write बस line voltages toggle करने जैसा काम थे, और वास्तव में क्या होगा यह उसी क्षण पता चलता था। CRT blanking signal के साथ बेहद सटीक synchronized timing पर voltages toggle करके मनचाहा effect निकाला जाता था, और Super Mario Bros. 3 में RAM multiplexer toggle करके हर screen refresh timing पर sprite bank बदलने जैसी तरकीबें इस्तेमाल की जाती थीं। अलग-अलग क्षेत्रों के TV standard NTSC/PAL की scan rate rendering logic के clock की तरह काम करती थी, इसलिए हर TV standard के लिए software अलग जारी करना पड़ता था — सचमुच बेहद जंगली दौर था
  • जब मैं emulator पर game खेलते हुए कहीं अटक जाता हूँ, तो हमेशा यह शक रहता है कि “कहीं यह emulator bug तो नहीं?” इस issue में भी मुझे शायद यही लगता कि game design ने जानबूझकर इसे इतना कठिन बनाया है। और जब game बहुत मुश्किल लगता था तो यह भी सोचता था कि “क्या यह emulator latency की वजह से है?”, और इसी कारण मैंने खुद mister FPGA बनाकर इस्तेमाल करना शुरू किया

    • Chrono Trigger में एक जगह चार keys एक साथ दबानी होती हैं, लेकिन USB input एक बार में केवल तीन ही keys भेज पाता था, इसलिए चार में से सिर्फ एक बार सही register होता था; वह हिस्सा बेहद कठिन और निराशाजनक लगा था

    • मैंने DKC को केवल ZSNES पर खेला था, इसलिए यह लेख पढ़ने तक मुझे बिल्कुल नहीं पता था कि यह emulator bug थी। मैं तो यही समझता रहा कि game design ने जानबूझकर कठिनाई दी है, और बाद में bug का पता चलना सचमुच चौंकाने वाला था

    • बचपन में मैंने Bionic Commando बहुत खेला था, लेकिन emulator पर दोबारा खेलते समय वह कहीं ज़्यादा कठिन लगा। बाद में पता चला कि emulator bug की वजह से enemies गायब ही नहीं होते थे, इसलिए ज़रूरी lives लगभग दोगुनी हो जाती थीं। फिर भी एक बार मैंने उसी हालत में game पूरा किया, लेकिन दोबारा वैसा नहीं करना चाहूँगा

  • DKC 1 की SGI-आधारित pre-rendered 3D graphics उस समय की cutting-edge technology थी। Mega Drive का Vector Man भी इसी तरह की technique इस्तेमाल करता था, लेकिन DKC जितना ध्यान नहीं खींच सका

    • 1995 में मैं DKC के मुख्य target age group (11 साल) में था, और इस game की graphics सचमुच चौंकाने वाली थीं। Release के आसपास मुझे इसका एक promotional video भी मिला था, और उसके behind-the-scenes footage वाली tape मैंने कई बार चलाई थी। मेरे पास खुद game नहीं था, लेकिन दोस्तों के घर पर इसे खेलने का मौका मिल जाता था

    • बचपन में मुझे DKC की graphics में किसी तरह का “नकलीपन” महसूस होता था। उस समय की magazines अक्सर बनावटी अंदाज़ में लिखती थीं कि SNES real-time में 3D characters render कर रहा है, लेकिन मुझे धुँधला-सा अंदाज़ा था कि असल में यह flipbook animation जैसा कुछ है