W tym odcinku zajmiemy się łataniem gry, tak by można było w nią grać i jednocześnie mieć możliwość powrotu do systemu. Pierwszy test, czyli uruchomienie gry i przekonanie się, że nasz pierwszy slave jednak nie działa jak należy, mamy za sobą. Pokrótce też opisałem, co było powodem niewłaściwego działania. Skąd to wiedziałem? Zdeasemblowałem plik wykonywalny i przeanalizowałem go.
W skrócie jest to odtworzenie kodu z postaci binarnej - dla jasności chodzi o kod zapisany w asemblerze. Deasemblacja do kodu źródłowego, w którym napisano grę jest o wiele trudniejsza i nie wiem, czy we wszystkich przypadkach możliwa. W naszym przypadku postać binarna to plik wykonywalny, innymi słowy będzie to deasemblowanie pliku AppleHunt. Deasemblacja pochłania dużo czasu, wymaga wiedzy i doświadczenia. Dlatego dla wprawy warto przeglądać pod Resource choćby własne krótkie przykłady, tworzone w różnych językach programowania i skompilowane do postaci wykonywalnej. W ten sposób nauczymy się w jaki sposób generowany jest kod i jakiej jest on jakości, a to przełoży się na ilość czasu potrzebnego do stworzenia własnej gry. Wykorzystując taką wiedzę możemy podejrzeć kod dowolnej gry z Aminetu i pozgadywać w jakim języku został napisany. Nie należy się sugerować rozmiarem pliku wykonywalnego, ponieważ oprócz kodu zazwyczaj zawiera on także różne dane, a czysty kod może stanowić jedynie niewielki procent całości. Nie inaczej jest w naszej przykładowej grze. Jeśli potrafimy w procesie początkowym określić co jest daną, a co kodem, to będziemy mieli mniej pracy. Niestety odróżnienie tego nie należy do łatwych zadań i warto podpierać się różnymi narzędziami jak np. muzyczne rippery. Ostatecznie i tak będziemy musieli przysiąść i przejrzeć kod bardziej szczegółowo. Nie musimy analizować całości, bo po pierwsze zajęłoby to bardzo dużo czasu, a po drugie naszym celem nie jest w tym momencie poznanie w jaki sposób gra została napisana. Ważne, by odnaleźć kłopotliwe miejsca w kodzie, które powodują, że gra nie działa, bądź zachowuje się w inny sposób niż zakładał to autor.
W Resource'a za pomocą menu Open load file wczytujemy plik AppleHunt. Opcja ta rozpoznaje czy to jest plik zapisany wcześniej przez Resource, czy plik wykonywalny. W tym momencie ważne jest, byśmy nie używali Open binary file, bo wtedy plik zostanie potraktowany jako binarny. Po wczytaniu pliku, używając menu Project wybieramy Dissasemble. Chwilę to potrwa, bo program będzie analizował kod, nie należy zbytnio się ekscytować, zrobi to tak jak będzie umiał i na pewno sporo jeszcze zostanie dla nas. W końcu naszym oczom ukaże się taki oto widok:
Przystępujemy do przejrzenia tego co pokazał Resource, poruszamy się za pomocą strzałek w dół bądź w górę, w prawo przechodzimy do instrukcji skoku, (bcc, jsr, jmp) a w lewo wracamy do poprzedniego miejsca, z którego go wykonaliśmy. Przede wszystkim szukamy miejsc, które nie zostały oznaczone jako kod i są pokazane jako ciąg danych. Jak taki ciąg rozpoznać? Tu przychodzi z pomocą wiedza o mnemonikach i ich reprezentacji w postaci słów, na przykład rts to dc.w $4e75. Jeśli pamięć nas zawodzi, albo nie znamy jeszcze takiej reprezentacji (zapewniam, że po zdeasemblowaniu paru plików wykonywalnych idzie to zdecydowanie szybciej) to naciskamy Lewy Amiga i C, by zamienić dane na kod. I tu już sami musimy zdecydować, czy to co program przedstawił po zamianie jest kodem, czy też nie. Do zamiany kodu na ciąg danych typu słowa, używamy Lewy Amiga i W. Wspomniane dwa skróty klawiszowe odpowiadają menu Display -> set data type. Jeśli nie działają, to prawdopodobnie została zmieniona tablica klawiszy, wtedy należy zapoznać się bliżej z menu KEY BINDING. Po takiej analizie, stwierdziłem, że kod to około 2% deasemblowanego pliku. Innymi słowy, sytuacja jest bardzo dobra, bo nie musimy ślęczeć nad przeglądaniem kodu i tracić cenny czas.
W poprzednim odcinku pokrótce wyjaśniłem czym jest łatanie kodu - należy je rozumieć w kategorii zamiany kodu, czy też danych na inne. Przedstawię teraz absolutne minimum jeśli chodzi o makra używane w budowaniu patchlisty. Więcej informacji znajdziemy w whdload.i. Zacznijmy od bardzo prostego przykładu:
;przesunięcie względem początku $200 (offset $200) nop ;dc.w $4e71 nop ;dc.w $4e71 illegal ;dc.w $4afc nop ;dc.w $4e71 nop ;dc.w $4e71
W trzeciej linijce za sprawą rozkazu illegal powyższy kod spowoduje, że WHDLoad wyjdzie do OS i pokaże komunikat, w tym momencie nieistotne jaki. W każdym razie chcemy to naprawić. Najłatwiejsza możliwość to zamienić feralny kod na odpowiednią ilość rozkazów nop, a dokładnie wystarczy jeden. Zapisujemy to w patch liście:
PL_NOP $204,2
Przesunięcie wynosi $204, bo dwa początkowe nopy zajmują 4 bajty pamięci. Następny przykład:
;offset $300 lea $1001,a0 ;dc.w $41f8,$1001 moveq #10,d0 ;dc.w $700a move.w d0,d1 ;dc.w $3200 move.w d0,(a0) ;dc.w $3080 moveq #20,d2 ;dc.w $7414 ;dalsza część kodu
Z pozoru wszytko wygląda w porządku, ale problem będzie widoczny na maszynach z procesorem 68000. W czwartej linijce nastąpi próba zapisania słowa pod nieparzysty adres. W celu naprawy sytuacji należy przeanalizować kod i zrozumieć co autor miał na myśli. Czy chciał zapisać bajt pod ten adres, czy dwa bajty, czy też adres był zły? Możliwości, jak widać jest wiele i od tego też zależy w jaki sposób można powyższy kod załatać. Chcąc poprawić kod tak, by zapisywał bajt zamiast słowa, wystarczy zmienić reprezentację mnemoniku z move.w na move.b
PL_W $308,$1080 ;move.b
W przypadku zmiany adresu z nieparzystego na parzysty, podobnie jak poprzednio zmieniamy jedno słowo.
PL_W $302,$1000
Najgorszy jest przypadek, gdy autor, zamiast zapisywania słowa pod nieparzysty adres, chciał zapisać dwa bajty, czyli zamiast move.w, który zajmuje dwa bajty, chcielibyśmy mieć:
move.b #0,(a0)+ ;dc.w $10bc,$0000 move.b d0,(a0) ;dc.w $10c0
To jeden ze sposobów i jak widać zajmuje teraz 6 bajtów, można go skrócić do 4 bajtów, ale trzeba przeanalizować kod i poszukać czy jakiś rejestr nie zawiera zera w młodszym słowie, wtedy możemy użyć move.b d0,(a0), który zajmuje tylko 2 bajty. W tym momencie dochodzimy do wniosku, że nie wciśniemy 4 bądź 6 bajtów w 2 bajty. Nie pomoże prosta podmiana, bo zwyczajnie nadpiszemy inne instrukcje, chcąc dodać kolejny move.b. Do akcji wkroczy najczęściej używany sposób, zamiana kodu na jsr. Będziemy potrzebowali na ten cel 6 bajtów - dwa na mnemonik i cztery na adres. Jak napisał Codetapper w artykule o instalacji All Terrain Racing XMas Coverdisk za pomocą WHDLoad, te 6 bajtów to magiczna liczba w patchowaniu. Wiele razy będziemy liczyli bajty instrukcji, by wcisnąć się z łatą. Zobaczmy w jaki sposób to zapisujemy:
PL_PS $304,fixExample ;dalsza część patch listy PL_END fixExample: moveq #10,d0 ;original move.w d0,d1 ;original move.b #0,(a0) move.b d0,1(a0)
To teraz przykład po załataniu będzie wyglądał
lea $1001,a0 jsr fixExample moveq #20,d2
Konieczne było przeniesienie dwóch instrukcji, co zostało odnotowane jako komentarz, żeby zmieścić się z naszą łatą. Bardzo często na początku pisania slave'ów łapałem się na tym, że patchowany kod został źle umieszczony w oryginalnym i przeważnie WHDLoad wychodził do OS z komunikatem błędu, bądź gra się zawieszała. Weźmy przykład:
;offset $300 lea $1001,a0 ;dc.w $41f8,$1001 move.w #10,3(a0) ;dc.w $317c,$000a
To widać że łata PL_PS $302,fixExample spowoduje że gdy zdeasemblujemy kod przy założeniu że fixExample ma adres $0
lea $4eb9.w,a0 ;dc.w $41f8,$4eb9 or.b #$00,d0 ;dc.w $0000,$0000
Będzie to kompletnie inny kod niż oczekiwaliśmy, który może powodować niewłaściwe działanie gry, a z pozoru wydaje się, że wszystko jest w najlepszym porządku. Jak rozwiązać ten problem? - po pierwsze przesunięcie powinno być inne, tak by nie uszkadzało części rozkazu, a po drugie trzeba wypełnić dwa bajty tak, by kolejna instrukcja była poprawnie interpretowana przez procesor. Można użyć tutaj takiej kombinacji
PL_PS $300,fixExample PL_NOP $306,2
Bądź użyć do tego celu PL_PSS, która przyjmuje za pierwszy argument przesunięcie, dalej mamy adres, a na końcu ilość bajtów, które zostaną zastąpione przez nop.
PL_PSS $300,fixExample,2
Na przykładzie gry Apple Hunter poznamy kilka z wielu problemów powodujacych niewłaściwe działanie gier. Rozwiązanie ich wymaga większej wiedzy, zarówno dotyczącej procesora, jak i specyficznego hardware naszej maszyny. Poza tym nie ma się co oszukiwać, patchowanie jest czasochłonną i żmudną pracą, której efekty często odbiegają od oczekiwań użytkowników, np. gra na maszynach z 68000 działa wolniej, niż odpalona z dyskietki. Rozwiązanie takich problemów może być bardzo trudne. Z drugiej strony łatanie zakończone sukcesem dostarcza satysfakcji, że udało nam się "naprawić" grę. Uczymy się jak tworzyć gry, aby jak najszersze grono mogło bez problemu zagrywać się w nasze dzieło. Często podczas łatania będziemy mieć nieodparte wrażenie, że gra została napisana w nieprzemyślany sposób, bardzo szybko i nie wykorzystując połowy tego co oferuje Amiga, a najgorsze, że działa przez przypadek :)
Skoro wiemy już, że przed nami ciężkie wyzwanie, to zastanówmy się jaką strategię obrać w celu zminimalizowania ilości pracy. Podejście pierwsze jest łatwiejsze, ale nigdy nie wiemy czy wszystko załataliśmy. Bazuje ono tylko na empirycznym sprawdzaniu i próbach "wywalenia" gry. Odpalamy grę i testujemy ją, co wiąże się z tym, że musimy ją przejść. Najlepszą opcją będzie zabawa w rasowego testera i robienie wszystkiego, co jest możliwe w grze. W przypadku, gdy gra się "wywali" dostajemy komunikat ze strony WHDLoad, analizujemy log i łatamy. Proces powtarzamy, aż do skutku, czyli dochodzimy do wniosku, że według nas gra działa bezbłędnie. Bardzo dużym minusem jest tu niesamowita ilość testów, która nas czeka. Innym sposobem jest analiza kodu i szukanie podejrzanych miejsc, takich jak np. pętle czasowe bazujące na typie procesora. Obie strategie nie dają gwarancji, że naprawiona gra będzie zawsze poprawnie działać. Dlaczego? - składają się na to dwa czynniki. Po pierwsze trzeba sobie zdawać sprawę, że sztuka patchowania to nie tylko sprawienie, że gra w końcu zadziała z dysku twardego, to także naprawianie błędów programisty, które wychodzą na innych maszynach (gra w założeniu miała działać dobrze na nierozbudowanej Amidze 500). Na przykład, do tej pory nie mogę się nadziwić, dlaczego Elevator Mission działał. Podczas pracy nad nim, przynajmniej raz dziennie wysyłałem nowego slave'a do testera z Anglii, żeby wieczorem dostać informację, że gra nie działa właściwie na A500. W głowę zachodziłem, co może być nie tak i dlaczego "czasami" bohater widocznie spowalnia przy skoku do góry - myślałem, że tak ma po prostu być. Niezwykle ważny jest także czynnik ludzki. Z mojego doświadczenia wynika, że gdy łatam grę, to testuję slave w pewien określony sposób, idę utartą ścieżką w celu wygenerowania błędu lub sprawdzenia czy ostatnia zmiana działa. Czasami robię to po 40 razy (tak było w przypadku Flying Shark). Oznacza to, że nie widzę błędów, które mógłby zauważyć ktoś inny. Z tego powodu autorzy łatki często proszą użytkowników o testy, szczególnie gdy mogą przetestować grę na wielu różnych maszynach. Warto o takich rzeczach pamiętać i uzbroić się w cierpliwość, bo normalne jest, że nawet za 20 podejściem gra wciąż szwankuje.
Ponieważ nie korzystamy z OsEmu, ani z emulacji Kickstartu musimy odnaleźć wszystkie miejsca gdzie są odniesienia do OS. Najprościej jest wygenerować kod źródłowy, zapisać do pliku i zwyczajnie przeszukać w poszukiwaniu wywołań typu jsr -liczba(a6). Tu mała uwaga, zdarzyło mi się widzieć jsr -liczba(a5), które o dziwo działało na A500, w rejestrze A5 była baza biblioteki. Gdy już te wywołania odnajdziemy, należy się upewnić, że są to na pewno skoki związane z OS. Co robić, gdy już mamy listę takich skoków i jest ich zdecydowanie za dużo jak na nasze umiejętności? Podejście z patchowaniem funkcji OSu sprawdza się tylko wtedy, gdy wywołań jest niewiele i są one dosyć proste. Na przykład, jeśli gra mocno korzysta z graphics.library to używamy emulacji kickstartu, bo ilość pracy, która ewentualnie nas czeka, może być porównywalna z napisaniem gry od nowa. Dodatkowo emulacja kickstartu zwiększa zapotrzebowanie na pamięć - gra pod WHDload będzie potrzebowała jej więcej. Gry mogą używać OS w bardzo różny sposób, począwszy od całkowitego jego pominięcia, aż do produkcji, która używa go bardzo często. Część produkcji wykorzystuje tylko wywołania OS, by przejąć system. Wtedy taki kod wystarczy "zanopować", bądź przeskoczyć, używając PL_S w przypadku krótkiego skoku (podmiana kodu na bra przesunięcie). Łatanie OS nie sprowadza się oczywiście do skoków za pomocą jsr, jest to temat szeroki i wymaga większej znajomości systemu.
W przypadku AppleHunt jest tego niewiele i jedynie na początku (offset $c):
move.l (4).l,a6 ;baza exec lea (gfxName),a1 ;nazwa biblioteki jsr (_LVOOldOpenLibrary,a6) ; move.l d0,gfx_base ;zapamiętanie bazy move.l gfx_base,a6 ;baza do a6 move.l (gb_LOFList,a6),old_copper ;zapamiętanie starej copperlisty move.l #copper_list,(gb_LOFlist,a6) ;ustawienie nowej copperlisty
Na pierwszy rzut oka wygląda to dosyć dziwnie. Otwieramy bibliotekę graphics, żeby zapamiętać jedno pole (gb_LOFList) i wpisać w nie adres copperlisty. Z pewnością brakuje tu sprawdzenia, czy udało nam się otworzyć bibliotekę, poza tym bezpośrednie grzebanie w polach bazy biblioteki graphics to bardzo średni pomysł. Powyższy kod musimy zmodyfikować, a szczególnie jsr powodujący błąd WHDLoad. Najbezpieczniej będzie cały ten blok "zanopować", czyli wypełnić nopami. Obliczamy rozmiar bloku, jeśli jesteśmy na początku kodu to wciskamy dwa razy strzałkę w dół, a następnie wciskamy kombinację Prawy Amiga i F3, co odpowiada w menu Display opcji Set Counter. Następnie przechodzimy żądaną ilość razy w dół, tak aby zatrzymać się na pierwszej instrukcji za ostatnią z bloku. Zerkamy na górną belkę i odczytujemy wartość, która powinna wskazywać $2c. Uzbrojeni w tę wiedzę możemy użyć PL_NOP (zapamiętajmy to, bo jest coś, o czym nie wspominałem, a co wyjdzie w trakcie testowania). Wypełniając powyższy kod rozkazami nop, usuniemy ustawianie adresu copperlisty. Lepiej będzie użyć PL_PSS, która wstawi nam jsr fixCopperSet i zajmie 6 bajtów, a pozostałe $2c-6=$26 bajtów wypełniamy rozkazem nop.
;remove OldOpenLibrary call PL_PSS $c,fixCopperSet,$26 ;dalsza część patchlisty PL_END fixCopperSet: ; ; adres copperlisty z gry ; COPPER_ADR = GAME_STARTADR+$17ea ; ; ustawienie copperlisty ; move.l #COPPER_ADR,_custom+cop1lc move.w d0,_custom+copjmp1 rts
Dla ciekawskich: Po dodaniu powyższego kodu do przykładu z poprzedniego odcinka, skompilowaniu i uruchomieniu, naszym oczom ukaże się czarny ekran i niestety nie będzie możliwości wyjścia do OS.
W rejestrze dmacon jest możliwość ustawienia bitu 10 o nazwie BLTPRI, znanego też jako BLITHOG i tytułowy blitter-nasty bit. Może on powodować problemy na niektórych konfiguracjach i dlatego należy go wyłączyć. Ustawiany jest samotnie, tak jak w AppleHunt i wtedy bardzo łatwo go znaleźć szukając wzorca $8400. Pojawia się też często z innymi bitami, w takim przypadku szukamy rejestru dmacon:
;$7e (offset względem początku) move.w #$8400,$dff096
Wyrzucając BLTHOG i dodając brakujące bity dla dmacon upieczemy dwie pieczenie przy jednym ogniu. Dlaczego te bity musimy ustawić, przecież gra tego nie robiła? Ano dlatego, że gra nie zatrzymywała poprawnie systemu, zmieniała tylko adres copperlisty używany przez OS. Sytuacja teraz jest inna, WHDLoad zatrzymał system i wszystkie kanały dma są wyłączone, łącznie z wszystkimi przerwaniami. Wróćmy zatem do sedna, czyli do zmiany $8400. Robimy to w patchliscie i używamy do tego PL_W, chcąc jedynie zmienić wartość w powyższym kodzie, tak by obliczyć poprawny adres - dodajemy 2, bo tyle zajmuje mnemonik:
;usuniecie blthog i poprawienie dmacon PL_W $80,DMAF_SETCLR|DMAF_MASTER|DMAF_RASTER|DMAF_COPPER|DMAF_BLITTER
Napomknę tu, że WHDLoad posiada możliwość sprawdzania i weryfikowania custom rejestrów ($dffxxx), o ile jesteśmy posiadaczami MMU. To znakomicie ułatwia pracę, nawet jeśli zapomnimy o sprawdzeniu BLTHOG. Wystarczy wpisać odpowiednie parametry (whdload slave=nazwa preload mmu chkblthog snoopocs) i przetestować porządnie produkcję. WHDLoad powiadomi nas odpowiednim komunikatem.
Po dodaniu łaty i skompilowaniu, przyszedł czas na test. Wpisujemy WHDLoad slave=ram:AppleHunt.slave preload. Naszym oczom w końcu ukazuje się ekran tytułowy
Od razu zauważyć można, że muzyka gra zdecydowanie za szybko, a po wciśnięciu przycisku w joysticku widać barwną mozaikę.
Nadal nie możemy wyjść z gry do OS. Przeważnie staram się na początku uzyskać możliwość wyjścia z gry tak, by móc szybciej sprawdzać kolejne wersje slave'a. Tu wyjątkowo zrobimy to na końcu, bo będzie to fix tymczasowy. Chętni mogą przeskoczyć do końca, dodać łatę i wrócić do tego miejsca.
Bardzo często w starych produkcjach można spotkać pętle zależne od typu procesora, które mniej więcej wyglądają tak:
move.w #$ffff,d0 .wait dbf d0,.wait
Ich zadaniem jest odczekanie określonego czasu, niestety o ile na 68000 powyższy kod wykonany zostanie dokladnie przez ten czas, to na nowszych procesorach ulegnie on skróceniu i spowoduje niewłaściwe działanie gry. Do naprawy takiego błędu, używa się synchronizacji z promieniem wizji. Powyższy przykładowy kod zamienia się na:
move.w d1,-(sp) move.w #$ffff/34,d0 .wait move.b (_custom+vhposr),d1 .one cmp.b (_custom+vhposr),d1 beq .one dbf d0,.wait move.w (sp)+,d1
Jak widać jest to bardzo łatwe. Używamy rejestru D1, a w przykładzie powyżej był tylko D0, więc odkładamy go na stos. Po wykonaniu pustej pętli w D0 będzie wartość $xxxxffff, więc taką też musimy oddać - na szczęście używamy D0 jako licznika i tak zostanie on ustawiony. Kod pustej pętli zajmuje 8 bajtów, więc nie będzie problemów z podmianą na jsr adres i nop, gdyż rozmiar jest większy od 6 bajtów. Ta bardzo typowa pętla, może przyjmować różne formy w kodzie i nie zawsze jest tak różowo jak w naszym przykładzie. Czasem trzeba policzyć ilość cykli w takiej pętli i zamienić na odpowiednią pętlę używającą synchronizacji z promieniem wizji. Przykładowe puste pętle:
;pusta pętla z nopem (bądź z wieloma nopami) move.w #120,d0 .wait nop dbf d0,.wait ;pusta pętla oparta na instrukcji bcc move.l #$100000,d0 .wait subq.l #1,d0 bne.b .wait ;trudna pusta pętla do łatania move.w #100,d0 lea adres_gdzies,a0 move.w d0,(a0) .wait dbf d0,.wait ;pusta pętla działająca przez przypadek move.w #$2000,d0 move.w d0,adres move.b #200,d0 .wait dbf d0,.wait
Gdzie spotkamy wyżej wspomniane puste pętle? Jest parę miejsc, którym warto się przyjrzeć. Autorzy często dodają ją na samym początku gry, żeby zgasić motor stacji dyskietek, wtedy można taką pętlę zwyczajnie usunąć, poprzez zanopowanie, bądź skok do dalszej części programu (używając WHDLoad nie musimy się przejmować stacją dyskietek). W kodzie odpowiedzialnym za odgrywanie muzyki również często znajdziemy nieduże puste pętle. Między etapami gry, planszą tytułową, a grą, w game over, wszędzie, gdzie trzeba odczekać jakiś okres czasu, a autor był zbyt leniwy, by użyć synchronizacji z promieniem wizji i liczył, że gra będzie uruchamiana tylko na Amidze 500 ze standardowym procesorem 68000. Ułatwiając sobe życie warto przejrzeć kod gry pod kątem występowania instrukcji dbf, nop, bcc; być może gdzieś tam pałęta się przeoczona pusta pętla.
Przy okazji kilka słów o ciemnej stronie łatania takich pętli. Łata taka jest obarczone błędem niedokładności, bo nie zajmuje ściśle określonego czasu, jak to ma miejsce w przypadku pustej pętli, jest to zakres od do.Przejdźmy teraz do naszej gry i poszukajmy pustych pętli:
;$88 (offset) move.w #$ffff,d0 .empty0 dbf d0,.empty0 ;$222 moveq #0,d1 move.w #10,d1 .empty1 moveq #0,d0 move.w #$ffff,d0 .empty2 dbf d0,.empty2 dbf d1,.empty1 ;$bc4 moveq #0,d1 move.w #17,d1 .empty0 moveq #0,d0 move.w #$ffff,d0 .empty1 dbf d0,.empty1 dbf d1,.empty2 ;$1262 move.w #$12c,d0 .empty0 dbf d0,.empty0 ;$127a move.w #$12c,d0 .empty0 dbf d0,.empty0
Pierwsza i dwie ostatnie to bardzo proste puste pętle. Skupmy się tylko na drugiej, bo trzecia jest bardzo podobna. Wewnętrzną pętlę możemy załatać w prosty sposób, czyli $ffff/34, ale to nie wszystko, wykonywana jest ona 11 razy. Czyli musimy wykonać operację $ffff*11/34, która szczęśliwie mieści się w 16 bitach.
EMPTY_LOOP: MACRO move.w d1,-(sp) move.w #\1/34,d0 .wait move.b (_custom+vhposr),d1 .one cmp.b (_custom+vhposr),d1 beq .one dbf d0,.wait move.w (sp)+,d1 rts ENDM ;... dalsza część kodu PL_START PL_PSS $88,fixEmptyLoop0,2 PL_PSS $222,fixEmptyLoop1,$e PL_PSS $bc4,fixEmptyLoop2,$e PL_PSS $1262,fixEmptyLoop3,$2 PL_PSS $127a,fixEmptyLoop3,$2 PL_END ;.... dalsze część kodu fixEmptyLoop0: EMPTY_LOOP $ffff fixEmptyLoop1: EMPTY_LOOP $ffff*11 fixEmptyLoop2: EMPTY_LOOP $ffff*17 fixEmptyLoop3: EMPTY_LOOP $12c
Przechodzimy do sprawdzenia. Kompilujemy i uruchamiamy. Mimo załatania pustych pętli muzyka niestety w dalszym ciągu gra za szybko. Co ciekawe (sprawdziłem to) po odpaleniu slave'a na sprzęcie z procesorem 68000 wszystko jest w jak najlepszym porządku. Jest to błąd związany z tym, że autor sam miał tylko procesor 68000 i nie zdawał sobie sprawy, co się stanie jeśli uruchomimy grę na lepszym sprzęcie.
Przypatrzmy się bardzo klasycznej procedurze czekania na określoną linię rastra, która jest umieszczona wewnątrz pętli głównej ekranu tytułowego AppleHunt:
TitleLoop: bsr.w get_key cmp.b #$80,($DFF006).l ;_custom+vhposr - czekamy na poziomą linię $80 bne.b TitleLoop ;jeszcze nie osiągnięta to skocz do TitleLoop bsr.w mt_play ;odegraj moduł btst #7,($BFE001).l ;czekaj na przycisk joysticka bne.b TitleLoop ;nie wciśnięty to skocz do TitleLoop
Wszystko wygląda prawidłowo, czekamy sobie na $80 linię poziomą, gdy jej nie osiągnęliśmy to biegniemy do TitleLoop. Gdzie jest haczyk? Problemem jest w tym przypadku czas. Co będzie, gdy mt_play i get_key (reszta rozkazów jest pomijalna ze względu na niedużą ilość cykli) wykona się za szybko, bo mamy lepszy procesor i dalej będziemy w lini $80? Wtedy procedura odgrywania modułu będzie wykonywana za często i nie będzie tak naprawdę synchronizacji z pozycją promienia. Łata w tym przypadku jest prosta, musimy podmienić procedurę czekania na pozycję promienia:
PL_START ;... ;naprawa petli czekajacej na pozycje promienia PL_PSS $f6c,fixWaitBeam,$a-6 PL_END ;----------------------------------------------------------------------------- fixWaitBeam: movem.l a0/d0,-(sp) lea _custom+vposr,a0 .1 moveq #1,d0 and.w (a0),d0 beq.b .1 .2 and.w (a0),d0 bne.b .2 movem.l (sp)+,a0/d0 rts
Tym razem sprawdzenie przynosi pozytywny skutek, moduł jest odgrywany w poprawny sposób.
Wstawianie kodu sprawdzającego czy blitter skończył poprzednie zadanie, także należy do częstych błędów popełnianych przez autorów go wykorzystujących. Są produkcje, które nie używają blittera, a wszystko jest robione za pomocą procesora, najczęściej to porty gier z Atari ST. Braki kodu sprawdzającego dostępność blittera ujawniają się na lepszych maszynach. Szukanie takiego kodu jest w miarę łatwe, trzeba przeszukać go pod kątem $dff058, bądź $58(an), gdzie an = $dff000. Zdarzają się też inne kombinacje związane z optymalizacją kodu. Śmiało można powiedzieć, że większość kodu odwołująca się do blittera to albo bezpośrednie odwołanie do rejestru blittera (najwolniejsze, przykładowa instrukcja move.w #$0,$dff042 zajmuje 20 cykli), albo adresowanie pośrednie za pomocą rejestru An, gdzie przykładowo move.w #$0,$42(a5) zajmie 16 cykli, o ile w a5 mamy $dff000. Możliwe sią też inne optymalizacje. Kolejna kwestia to niepoprawne czekanie na blitter, czyli klasyczna procedura, którą można spotkać na przykład w książce Adama Doligalskiego:
.wait btst #14,$dff002 bne.b .wait
Procedura ta nie jest kompatybilna z wcześniejszymi wersjami układu Agnus. Mało tego, nie wiadomo, czy autor tak naprawdę zna działanie pierwszej instrukcji i czy chciał testować 14 bit. Dlaczego? Bo w tym przypadku testujemy bajt, a w nim jest tylko 8 bitów (od 0 do 7). Na szczęście w tej sytuacji działa modulo 8 i poprawnie testowany jest bit 6 w bajcie $dff002, który to jest bitem 14, ale w słowie $dff002. Co ciekawe, takie procedury można znaleźć w wielu miejscach, nawet we współczesnych produkcjach. Poprawnie powinniśmy użyć kodu:
tst.w $dff096 .wait btst #6,$dff002 bne .wait
bądź użyć makra BLITWAIT z whdmacros.i.
Jak sobie radzić w takich przypadkach? Po znalezieniu podejrzanego kodu musimy się upewnić, czy brakuje takiego kodu sprawdzającego. Jeśli go nie ma, to należy go dodać, czy to przed użyciem blittera, czy też zaraz po BLTSIZE, w zależności co łatwiej nam podmienić. Jeśli procedura czekania jest błędna, to jest łatwiej, bo sporo produkcji skacze do takiej procedury i wystarczy tylko ją naprawić.
Tutaj także są minusy, największy to ten, że gra działa za wolno na maszynie z procesorem 68000, bo gra wykonuje zbyt dużo blitów na ramkę. Naprawa takiego błędu może być bardzo czasochłonna.
W AppleHunt blitter jest używany do narzucenia 300 kostek 16x16x3 na ekran co ramkę. Procedura odpowiedzialna za skopiowanie kostki na ekran wygląda tak:
;$cb8 .bwait btst #14,$dff002 bne.b .bwait move.l a2,($DFF054).l move.l a1,($DFF050).l move.l #$9F00000,($DFF040).l move.l #$FFFFFFFF,($DFF044).l move.l #$700026,($DFF064).l move.w #$401,($DFF058).l add.l #$792,a1 add.l #$2800,a2 move.l a2,($DFF054).l move.l a1,($DFF050).l move.l #$9F00000,($DFF040).l move.l #$FFFFFFFF,($DFF044).l move.l #$700026,($DFF064).l move.w #$401,($DFF058).l add.l #$792,a1 add.l #$2800,a2 move.l a2,($DFF054).l move.l a1,($DFF050).l move.l #$9F00000,($DFF040).l move.l #$FFFFFFFF,($DFF044).l move.l #$700026,($DFF064).l move.w #$401,($DFF058).l
W tym odcinku skupimy się na szybkim fixie, a w następnym pokażę, co jeszcze można zrobić z tą procedurą. Łatanie będzie polegało na wstawieniu w trzech miejsach poprawnego czekania na blitter:
PL_START ;blitter PL_PSS $cb8,fixWaitBlit_1,4 PL_PS $cf4,fixWaitBlit_2 PL_PS $d32,fixWaitBlit_2 PL_END fixWaitBlit_1 BLITWAIT rts fixWaitBlit_2 BLITWAIT adda.l #$792,a1 ;original rts
Sprawdzamy i widzimy, że teraz jest możliwe granie w grę.
Niektóre rejestry specjalizowane są typu tylko do odczytu, bądź tylko do zapisu, a w grze ewidentnie robiony jest zarówno odczyt jak i zapis. Zazwyczaj używany jest rozkaz clr, który oprócz zapisu powoduje odczyt na procesorze 68000. Szukamy w kodzie wystąpienia clr i analizujemy, czy dotyczy to takiego rejestru, jeśli tak, to łatamy. Najczęściej można spotkać konstrukcję typu clr w kodzie odpowiedzialnym za zatrzymanie muzyki. Także uskutecznianie clr.w $dff088 (bądź co gorsza clr.l $dff088), czyli zmuszenie coppera do natychmiastowego restartu pierwszej lokacji jest błędne. Szczególnie to można odczuć, gdy sterujemy blitterem za pomocą coppera.
W AppleHunt bez problemu odnajdujemy:
;fix clr.w aud clr.w ($DFF0A8).l ;wycisz głośność kanału audio 0 clr.w ($DFF0B8).l ;wycisz głośność kanału audio 1 clr.w ($DFF0C8).l ;wycisz głośność kanału audio 2 clr.w ($DFF0D8).l ;wycisz głośność kanału audio 3
A łata jest banalna:
;fix clr.w aud PL_PSS $1034,fixClrAudX,$12 ;... dalsza część patchlisty PL_END fixClrAudX move.w #0,_custom+aud0+ac_vol move.w #0,_custom+aud1+ac_vol move.w #0,_custom+aud2+ac_vol move.w #0,_custom+aud3+ac_vol rts
Na zakończenie tego odcinka dodamy na szybko możliwość wyjścia z gry. Niestety, nie będzie to "ładne" wyjście, bo będzie pokazywany komunikat WHDLoad z błędem. Szerzej zajmiemy się tym w ostatnim odcinku. W celu dodania "wyjścia do OS" musimy w jakiś sposób sprawdzać, czy odpowiedni klawisz został wciśnięty. Do tego celu znakomicie przyda się przerwanie PORTS, bo tam mamy możliwość między innymi, odebrania klawiszy. Oto łata, którą dodajemy w patchliście:
;dodanie PORTS int PL_W $ef6,$c008 ;wyrzucenie ustawiania int PL_NOP $fa0,8 PL_NOP $94,8
Zmieniliśmy ustawienie przerwań, pozwalając na przerwanie PORTS i "zanopowaliśmy" inne ustawienia przerwań. Warto sprawdzić, co za kod przykryłem nopami - pozostawiam to jako łatwe ćwiczenie. Od tej pory wciskając jakikolwiek klawisz, mamy możliwość "wyjścia" z gry. Nie jest to piękne, ale nad tym jeszcze popracujemy.
Do następnego odcinka!