Gra akcji w "Action!" by Kaz 2009-03-24 02:15:38

Paweł "Yosh" Różański atakuje nas drugim fragmentem rozważań o języku "Action!" (pierwszy był tu). Jest nadzieja, że przy większym zainteresowaniu ze strony czytelników zamieni się to w długi cykl, który pozwoli nam dokładnie poznać wady i zalety tego języka, a jednocześnie nauczy nas praktycznej umiejętności pisania w nim gier... Dalej już będzie peorował Yosh:



Siła ekspresji

Kolejnym aspektem języka, po dostępności kompilatora generującego zwięzły i szybki kod, jest jego zwięzłość - czyli możliwość wyrażania się w nim w sposób przejrzysty. W języku BASIC propagowany był kiedyś styl "samokomentujący", jakby to dziś powiedziano. Polegał na nazwaniu stałych po imieniu, co podnosiło czytelność listingu. Nie jakieś tam PEEK(20) czy GOTO 10, ale PEEK (RTCLOCK) i GOTO POCZATEK.

Oczywiście zmiennym RTCLOCK i POCZATEK należało wcześniej przypisać odpowiednie wartości. Dobry kompilator mógłby zoptymalizować kod podstawiając wartości zmiennych na swoje miejsca. Tak skompilowany program nie traciłby cykli na wyczytanie wartości ze zmiennej. Jednakże problem statycznej analizy zawartości zmiennych (czyli określenie podczas kompilacji zawartości zmiennych w danym miejscu wykonywanego kodu) jest złożony... Nic dziwnego, że nie robi tego ani kompilator "Action!", ani kompilator "Turbo Basic'a XL".

"Action!" próbuje obejść ten problem tak jak robi to język C - udostępniając makra. Dzięki nim możemy dowolny kawałek kodu nazwać inaczej, np.

DEFINE PLUS = "+"
DEFINE I = "+"
DEFINE TRZY = "3"
DEFINE DZIESCI = "* 10"
DEFINE PISZ = "PRINTF(""%U%E"", "
DEFINE NA = ")"
DEFINE EKRAN = ""

PROC MAIN()
  PISZ TRZY DZIESCI I TRZY PLUS TRZY NA EKRAN
  PISZ TRZY PLUS TRZY I TRZY NA EKRAN
RETURN

"Action!" posłusznie "wkleja" kolejne napisy podczas kompilacji - ważne jest, aby na koniec uzyskał kod zgodny z gramatyką "Action!". W tym szaleństwie jest metoda - kompilator jest zwolniony z jakichkolwiek weryfikacji przed podstawieniem tekstów (łatwe dla twórców kompilatora). Programista natomiast posiada możliwość nazywania części kodu bez troski o późniejszy przekład na kod maszynowy.



Do celu

Po tym powyższym przykładzie wyrwanym z kontekstu, kolejne będą nieuchronnie zbliżały nas do wykonania prostej gry.

Przed wykorzystaniem zmiennej należy ją wcześniej zadeklarować. Jeżeli zostanie zadeklarowana przed procedurami i funkcjami, jest zmienną globalną dostępną w dowolnym miejscu programu (podobnie jak w BASIC-u). "Action!" udostępnia trzy typy liczbowe: bajt - BYTE, liczbę 16-bitową ze znakiem - INT i liczbę 16-bitową bez znaku - CARD.

Deklaracja w programie wygląda następująco:

CARD SCORE

Nie interesuje nas pod jakim adresem "Action!" będzie trzymał tą wartość dla nas nazywa się ona SCORE. Jeżeli umiejscowienie zmiennej w pamięci jest istotne, możemy wymusić adres, na przykład:

BYTE RND = $D20A

Tym oto sposobem dostaliśmy generator liczb losowych (sponsorem jest układ POKEY), bez wykorzystywania biblioteki "Action!". Wywoływanie procedur i funkcji w "Action!" jest kosztowne w porównaniu do czystego kodu maszynowego. Więc nie STICK(0) ale BYTE STICK0 = $0278 i tak dalej. Jest to przy okazji spójne, bo prędzej czy później zamarzy nam się rejestr, który nie jest obsługiwany przez bibliotekę.

Napiszemy prostą grę silnie wykorzystującą pamięć ekranu. W BASIC-u wykorzystywana jest do tego procedura LOCATE, zwracająca kod ATASCII z podanej pozycji. W bibliotece standardowej "Action!" zawarta jest ona jako funkcja, co umożliwia proste użycie jej w wyrażeniu warunkowym. Rozpatrzmy prostą labiryntówkę, napisaną w stylu BASIC (tak aby ukazać, że nie tak ciężko uczy się "Action!"):

BYTE X,Y

PROC MAIN()
  X=3 Y=1

  GRAPHICS(0)
  POKE (752,1) ; wylacz kursor
  PRINTE("#####")
  PRINTE("# #")
  PRINTE("# ###")
  PRINTE("# #")
  PRINTE("#####")

  WHILE (1) ;z programu nie ma wyjscia - RESET przerywa
  DO
    POSITION(X,Y) PRINTE("@")
    WHILE (STICK(0)=15) DO OD
    POSITION(X,Y) PRINTE(" ")
    IF STICK(0)=7 AND LOCATE(X+1,Y)=32 THEN X=X+1 FI
    IF STICK(0)=11 AND LOCATE(X-1,Y)=32 THEN X=X-1 FI
    IF STICK(0)=13 AND LOCATE(X,Y+1)=32 THEN Y=Y+1 FI
    IF STICK(0)=14 AND LOCATE(X,Y-1)=32 THEN Y=Y-1 FI
  OD
RETURN


W zmiennych X i Y zawarta jest pozycja gracza. Główna pętla rysuje gracza, czeka na wychył joysticka i przesuwa gracza, jeżeli w danym kierunku jest znak o kodzie ATASCII 32 (czyli spacja). Taki program działa wystarczająco szybko nawet w BASIC-u. Jest jednak na tyle krótki, że pozwoli na zobrazowanie technik przyśpieszenia.

Wszystko sprowadza się do zrezygnowania z "ciężkich" poleceń POSITION i PRINTE i LOCATE. Polecenia POSITION i LOCATE pieczołowicie przeliczają pozycje X, Y na adres w pamięci ekranu. LOCATE i PRINTE dodatkowo operują na znakach kodowanych w ATASCII, co jest nam całkowicie zbędne.

Ekran trybu Graphics 0, jest rysowany od ciągłego obszaru pamięci którego adres zawarty jest w:

CARD SAVMSC = $58

Aby zadeklarować taki obszar konieczne jest wprowadzenie tablic w języku "Action!". Tablica to spójny obszar pamięci o określonym początku, deklarowanym poprzez:

BYTE ARRAY SCREEN

Teraz każde odwołanie postaci SCREEN(x)=y tłumaczone jest na "zapiszę na adres, który jest sumą zawartości pola SCREEN i x". Technicznie taka tablica to dwubajtowa zmienna zawierająca adres:

SCREEN = SAVMSC

spowoduje przestawienie początku tablicy na adres ekranu! Umożliwia to już rozsądnie wygodny dostęp do pamięci ekranu, na przykład:

SCREEN(4+40*10) = 128 ; narysuj murek w pozycji x:4 y:10
A=SCREEN(X+40*Y); to prawie A=LOCATE(X,Y)


Oczywiście nikt nie każe nam pamiętać, że spacja w negatywie to 128, a nasz ekran ma 40 bajtów szerokości, wystarczy to zdefiniować:

DEFINE WALL= "128"
DEFINE XY = "+40*"


I uzyskujemy w miarę przejrzyste konstrukcje:

SCREEN(4 XY 10) = WALL
A=SCREEN(X XY Y)


Oczywiście operacja mnożenia też kosztuje, zapiszmy więc pozycje naszego gracza poprzez offset w pamięci ekranu:

CARD PLAYERPOS
PLAYERPOS = 3 XY 1


Co pozwoli na wpisywanie go w plansze poprzez:

SCREEN(PLAYERPOS) = PLAYER

Znając rozkład pamięci ekranu wiemy, że pozycje wokół gracza znajdującego się na PLAYERPOS można wyrazić następująco:

PLAYERPOS - 41 PLAYERPOS - 40 PLAYERPOS - 39
PLAYERPOS - 1 PLAYERPOS PLAYERPOS + 1
PLAYERPOS + 39 PLAYERPOS + 40 PLAYERPOS + 41


Czyli wykrycie co mamy po prawej możemy zrealizować poprzez SCREEN(PLAYERPOS + 1). Podobnie jego ruch w górę: PLAYERPOS = PLAYERPOS - 40.

W tym momencie powinna już jak zmora pojawić się "pani od matematyki", na szczęście ze szkoły podstawowej :). Skoro:

SCREEN(PLAYERPOS + 1) -> adres SCREEN + PLAYERPOS + 1
SCREEN(PLAYERPOS) -> adres SCREEN + PLAYERPOS


to także przed ruchem :

SCREEN(PLAYERPOS) -> adres SCREEN + PLAYERPOS

po wykonaniu ruchu w górę:

SCREEN(PLAYERPOS - 40) -> adres SCREEN + PLAYERPOS - 40

wynika więc, że SCREEN + PLAYERPOS można wyciągnąć przed nawias. W połączeniu z wiedzą o tablicach, nie pozostaje nic innego jak wyrażać pozycje gracza... adresem tablicy. Niech:

BYTE ARRAY PLAYERPOS
PLAYERPOS = SCREEN + 3 XY 1


Zakładając, że adres tablicy zawarty w zmiennej PLAYERPOS to adres komórki ekranu, w której jest gracz, mamy:

PLAYERPOS(0) - zapis/odczyt w pozycje gracza
PLAYERPOS(-40) - pozycja nad graczem
PLAYERPOS=PLAYERPOS-1 - przestaw pozycje gracza w lewo zmieniając adres tablicy, itd.



Przerobiona gierka wyglądać będzie teraz tak:

DEFINE LEFT = "-1"
DEFINE RIGHT = "1"
DEFINE UP = "-40"
DEFINE DOWN = "40"
DEFINE XY = "+40*"
DEFINE NOTHING = "0"
DEFINE PLAYER = "32"

CARD SAVMSC = $58
BYTE STICK0 = $0278

BYTE ARRAY SCREEN
BYTE ARRAY PLAYERPOS

PROC MAIN()
  GRAPHICS(0)
  PRINTE("#####")
  PRINTE("# #")
  PRINTE("# ###")
  PRINTE("# #")
  PRINTE("#####")

  SCREEN = SAVMSC
  PLAYERPOS = SCREEN + 3 XY 1

  WHILE (1) ;z programu nie ma wyjscia - RESET przerywa
  DO
    PLAYERPOS(0) = PLAYER
    WHILE (STICK0=15) DO OD
    PLAYERPOS(0) = NOTHING
    IF STICK0=7 AND PLAYERPOS(RIGHT)=NOTHING
      THEN PLAYERPOS ==+ RIGHT FI
    IF STICK0=11 AND PLAYERPOS(LEFT)=NOTHING
      THEN PLAYERPOS ==+ LEFT FI
    IF STICK0=13 AND PLAYERPOS(DOWN)=NOTHING
      THEN PLAYERPOS ==+ DOWN FI
    IF STICK0=14 AND PLAYERPOS(UP)=NOTHING
      THEN PLAYERPOS ==+ UP FI
  OD
RETURN

gdzie PLAYERPOS ==+ RIGHT to skrót składniowy dla PLAYERPOS = PLAYERPOS + RIGHT.

Mając dość szybki sposób na określanie co się wokół dzieje, możemy porwać się na zdefiniowanie kamieni z "BoulderDash-a" i bomb z "Robbo", co da nam grę "Robbo Dash" ;). Poniższy program przegląda każde pole planszy i wykonuje operacje na znajdujących się tam obiektach:

BYTE RND = $D20A
CARD SAVMSC = $58

BYTE ARRAY SCREEN
BYTE ARRAY OBJ
CARD OBJFROM
CARD OBJTO

DEFINE STONE = "84"
DEFINE BOMB = "64"
DEFINE WALL = "128"
DEFINE NULL = "0"

DEFINE RIGHT = "1"
DEFINE LEFT = "-1"
DEFINE UP = "-40"
DEFINE DOWN = "40"
DEFINE XY="+40*"

BYTE B

PROC MAIN()
GRAPHICS (0)
SCREEN = SAVMSC
OBJTO = SCREEN + 41 ; murek jest przy poczatku
OBJFROM = SCREEN + 919 ; i przy koncu

FOR B = 0 TO 23 DO
SCREEN(0 XY B) = WALL
SCREEN(39 XY B) = WALL
OD
FOR B = 1 TO 38 DO
SCREEN(B XY 0) = WALL
SCREEN(B XY 23) = WALL
OD

OBJ = OBJFROM
WHILE (1)
DO
IF OBJ=OBJTO THEN ; jak caly ekran obsluzony
OBJ=OBJFROM ; to od nowa
IF (RND AND 7) THEN ; dostaw kamien/bombe
SCREEN(45 + (RND AND 31)) = STONE
ELSE
SCREEN(45 + (RND AND 31)) = BOMB
FI
ELSE
OBJ = OBJ - 1 ; iteruj po obszarze gry
FI

B = OBJ(0)
IF NULL = B THEN
;nic nie rob dla pustego
;ale sprawdz go pierwszego
ELSEIF STONE = B THEN
IF OBJ(DOWN) = NULL THEN
OBJ(DOWN) = STONE
OBJ(0) = NULL
ELSEIF OBJ(DOWN+RIGHT) = NULL AND OBJ(RIGHT)
= NULL THEN
OBJ(RIGHT) = STONE
OBJ(0) = NULL
ELSEIF OBJ(DOWN+LEFT) = NULL AND OBJ(LEFT)
= NULL THEN
OBJ(LEFT) = STONE
OBJ(0) = NULL
;jak kamyk polecial w lewo,
;zapobiegnij ponownemu rozpatrzeniu
OBJ = OBJ - 1
FI
ELSEIF BOMB = B THEN
IF OBJ(DOWN) = NULL THEN
OBJ(DOWN) = BOMB
OBJ(0) = NULL
ELSE
OBJ(0) = NULL
IF OBJ(DOWN+LEFT) <> WALL THEN OBJ(DOWN+LEFT)
= NULL FI
IF OBJ(DOWN) <> WALL THEN OBJ(DOWN)
= NULL FI
IF OBJ(DOWN+RIGHT) <> WALL THEN OBJ(DOWN+RIGHT)
= NULL FI
IF OBJ(LEFT) <> WALL THEN OBJ(LEFT)
= NULL FI
IF OBJ(RIGHT) <> WALL THEN OBJ(RIGHT)
= NULL FI
IF OBJ(UP+RIGHT) <> WALL THEN OBJ(UP+RIGHT)
= NULL FI
IF OBJ(UP) <> WALL THEN OBJ(UP)
= NULL FI
IF OBJ(UP+LEFT) <> WALL THEN OBJ(UP+LEFT)
= NULL FI
FI
FI
OD
RETURN

Efektem programu jest deszcz kamieni, podobny do tego z "Boulder Dash". Mam nadzieję, że przykłady są czytelne. Pozdrawiam i czekam na uwagi.



Jedyną procedurą, która pozostała jest GRAPHICS. Pozbędziemy się jej w kolejnym artykule, o Display List i generatorze znaków. W nim (jeżeli ktoś to będzie czytał) przybliżę kolejne niuanse tablic, na przykład dlaczego w powyższych przykładach brak jest rozmiaru tablicy.
sikor 2009-03-24 07:03:37

I o to chodzi. Dobrym przykładem byłby cykl artykułów o zrobieniu gierki od A do Z w Action, dodakjąc w każdej części kolejny "klocek" (procedurę) z szeroko opisanym kodem. Na początek z funkcjami bibliotecznymi, a później można pokazać metody optymalizacji (tak jak to jest tutaj zrobione).
Pozdrawiam i liczę na więcej ;)

pps 2009-03-24 08:58:04

Genialnie proste :)
Wielkie dzięki za artykulik i również proszę o więcej!
Pozdrawiam.

xxl 2009-03-24 09:13:50

pysznie, moze powiazac cykl artkow o pisaniu gier w action! z tym: http://atarionline.pl/forum/comments.php?DiscussionID=105

immolator 2009-03-24 09:58:26

Super! Dzięki za trud włożony w arta.

George 2009-03-24 10:56:23

Właśnie zaczynam zabawę z Action!, więc ten artykuł jest Just In Time. Dzięki wielkie.

anonymus 2009-03-24 11:46:42

Nie wiem gdzie napisać, więc proszę tu o pomoc, jaki emulator wybrać, żeby mi saturday demo ładnie grało, a nie jak w a800win czy w alttirze?

Kaz 2009-03-24 12:26:17

W sprawie gdzie pisac - proponuje forum - zostales juz zarejestrowany :).

zilq 2009-03-24 15:24:14

Bardzo fajna sprawa dla ludzików, chcący poznać "tajniki" pisania gier (nie tylko w ACTION!)
Oczekuję dalszych artykułów. Sam zapewne skorzystam, gdyż ACTION! to język który najbardziej jest zbliżony do stosowanego (i znanego) przeze mnie Pascala (tudzież PHP) Więc przesiadka mało boli :]
Oby tak dalej...

irwin 2009-03-24 16:13:50

Brawo Yosh! - bije pokłony, i czekamy na dalsze odcinki. Tak trzymaj!
@Kaz - może by tak z boku strony zrobić jakiś link typu kurs Action!, lub programowanie kursy - aby wszystkie odcinki były w jednym miejscu, bo za jakiś czas jak ktoś ich będzie potrzebował to się sporo naszuka a i tak nie będzie pewny czy wszystkie odnalazł w gąszczu nowinek, które niejaki Kaz wypuszcza z prędkością nadświetlną ;-)

stjack 2009-03-24 17:50:50

Super! Dzieki wielkie!

Yosh 2009-03-24 18:46:39

@sikor: ku temu to zmierza, aczkolwiek cały kod raczej będzie do ściągnięcia - już teraz ciężko się 'cytuje'
@xll: raczej nie ma takiej flaszowej gry... starałem się wybrać prosty - a za razem 'procesorożerny' przykład.

Chciałbym także podziękować Kazowi za miejsce na stronie i za redakcje - to On dostawił obrazki jak i przeredagował kilka zdań aby mniej kluły po oczach.

Dzięki wszystkim za dobre słowa - powiedzmy, że już jest tego tyle że dalej to przyjemność :P:P (w pisaniu kodu, bo tekściarzem to nigdy nie byłem...)

George 2009-03-25 08:32:17

Ja sobie dodaję do ulubionych i już nie muszę szukać :)

Ilmenit 2009-03-25 15:42:32

Pomysł - Cross-kompilator kodu Action!, ale kompilujący oryginalnym kompilatorem. Dałoby radę zrobić na różne sposoby np. generując a8s, interpretując oryginalny kod kompilatora...

Yosh 2009-03-25 23:11:33

@Ilmenit: dość łatwo w Atariwin plus podmontować katalog dla Action! działa jak złoto - może napisze o tym ciut...

Z Action! jest problem, że jego przekład jest 'wprost' bez najmniejszego namysłu, c65 jeszcze dokładnie nie testowałem - ale widzę, że tam będzie ciut lepiej. (Po skończeniu z Action! planuje przekład kodu na c65 - aby pokazać, że praktycznie jest to to samo)

Generalnie są programy które w Action! 'się wyrobią' i te które trzeba ostro w assemblerze (wiem brzmi to jak truizm - tak jest z każdym językiem programowania). Czasami ... bardziej mnie boli, że kod mógłby być krótszy ;)

Kaz 2009-03-26 12:59:48

Irwin - linki nie powinny zaginac, bo w kazdym nastepnym artykule z cyklu beda odnosniki do poprzednich czesci. A na koniec cos sie z tym zrobi - umiesci w dziale "Poradniki".

Tdc 2009-03-28 03:24:07

Bardzo fajny tekst - Yosh ma nieco odmienne podejście do tematu ode mnie, nad niektórymi rzeczami się nigdy nie zastanawiałem !:)
To dobrze bo takie różne punkty widzenia są bardzo cenne dla czytelników !

Sikor: Dobrym przykładem byłby cykl artykułów o zrobieniu gierki od A do Z w Action, dodakjąc w każdej części kolejny "klocek" (procedurę) z szeroko opisanym kodem.

Mówisz - masz ;)
Z tego co zgrywaliśmy u Mikera jest jedno źródło bardzo dobre do nauki np. dla wielbicieli Basica (choć chyba wtedy nie widzieliście tego programu). Muszę je teraz tylko opisać ;):) Może nie dziś ale macie je jak w banku.

Myślę że fajnie by było aby Yosh również zastanowił się nad swoim cyklem (o ile w ogóle chce) - wtedy oba nasze przykłady gier byłby pewną całością. Zapraszam na priva;)

Ilmenit: ja kiedyś (wczesne lata 90-te) na pececie napisałem coś podobnego co powodowało możliwość kompilowania kodu z Action! pod (np. pecetowym) językiem C. Nie działało to 100% bo pewne problemy były nierozwiązywalne lub trudne (czego mi się już robić nie chciało). Ale ciekawostka wyszła fajna;)

Kaz 2009-03-28 07:18:13

No i fajnie! To czekam na opisanie i oczywiscie udostepniam łamy AO.

Yosh 2009-03-28 13:15:39

Znalazłem przykład (prosty!) w którym Action! rozkłada cc65 (albo ja nie umiem go używać i ktoś mnie wyprostuje :P)

Szykuje całkiem nowe smakowitości

@Tdc: fajnie, że na priv-a - tylko jak? :) (zapobiegawczo wklepałem swój email w tym komentarzu - daj znać jakie masz pomysły)

Aktualnie mam plan, żeby okrasić RD lepszą grafiką (przy okazji pokrótce o display liście) czyli generalnie go 'skończyć' w kilku kolejnych 'odcinkach' - starając się żeby nie urósł.

Ilmenit 2009-03-29 22:11:42

@Yosh: daj ten przykład w Action! i CC65.

Kaz 2009-03-29 23:53:46

Yosh - zaraz Ci wysle maila do TDC.

Tdc 2009-03-30 22:21:38

Sorry z tym e-mailem, ale nie miałem za bardzo czasu w ostatnich dniach aby pisać, a jak napisałem posta to zdałem sobie sprawę, że nie wspisałem swojego emaila... W każdym razie wiele osób ma do mnie e-mail i zbytniego problemu z tym nie ma (można też na forum AA słać listy do mnie - choć szkoda że nie ma takiej możliwści na Atarum - czyli adres jest niewidoczny ale list wysłać można).
No i dzięki Kaz za przesłanie emaila do Yoshowi.

Jestem bardzo zainteresowany tym przykładem Action! vs cc65.

I wszyscy czekamy na Twoje smakowitości ;)

Co do opisu DL to jestem jaknajbardziej za. Szczególnie, że w Action! można wiele fajnych rzeczy zrobić.

Yosh 2009-03-31 18:47:37

Jako, że mam sprawę niecierpiącą zwłoki i chwilowo odpoczywam (no powiedzmy :)) od Atari. to będę się streszczał (Ilmenit widzę, że niecierpliwy)

BYTE ARRAY a(100) = 40040

PROC MAIN()
while(1) do
a(1) = 2
od
RETURN

i jego przekład (tak Action! be, nie domyślił się że to martwa pętla i niepotrzebnie robi LDA #1 BNE) (liczba cykli po ; )

311 73 0E32 LDA #$01 ;2
311 75 0E34 BNE $0E39 ;2
311 78 0E39 LDA #$02 ;2
311 80 0E3B STA $9C69 ;4
311 84 0E3E JMP $0E32 ;3

Ale domyślił się, że a(1) = 2 przy stałym adresie a = $9C68, to może sobie dodać tą 1ke i zrobić lda #2,sta $9c68 bez wyliczeń i ma z głowy.

natomiast w cc65

#define a ((unsigned char*)40040)

void main (void)
{
while(1)
a[1] = 2;
}

odpalonym cc65 -Oi -Or -Os -Cl -t atari ldasta.c

daje:

L0003: ldx #$9C ;2
lda #$68 ;2
sta sreg ;4
stx sreg+1 ;4
lda #$02 ;2
ldy #$01 ;2
sta (sreg),y ; 6
jmp L0003 ; 3

Ładnie załatwił martwą pętlę (jest tylko jmp).. ale dalej.. to jakas masakra.. już nie mówię - nie domyślił się, że to adres jest stały, i zrobił sobie tymczasowy pointer sreg... no ale jak go zrobil to mógł się chociaż domyśleć, że

ldx #$9C
lda #$68
sta sreg
stx sreg+1
lda #$02
ldy #$01
L0003: sta (sreg),y
jmp L0003


:) czyli ponawiać tylko sta

Jak wywołać cc65 ew, jak zmienić kod aby był ciut ładniejszy niż ten co jest teraz... ?

Ten przykład jest może naiwny - ale obrazuje, że nie potrafię zmusić cc65 do elementarnych optymalizacji - typu "stały adres kodu"

Ilmenit 2009-03-31 19:14:23

#define a 0x9C68

void main(void)
{
while(1)
*(unsigned char*)(a+1) = 0x01;
}

kompilacja: cl65 -t atari -l -Osir test.c

wynik:
L0003: lda #$01
sta $9C69
jmp L0003

Ilmenit 2009-03-31 19:22:02

Btw, dokładnie taki kod wynikowy daje napisanie tego "w stylu CC65":

#include

#define a 0x9C68

void main(void)
{
while(1)
POKE(a+1,1);
}

Yosh 2009-03-31 19:47:42

Dzięki, faktycznie ten konkretny przykład da się tak obejść - cc65 wraca do rozważenia ...

natomiast jest to drobne oszustwo ponieważ adres jest obliczany już na etapie sklejania stałych. Należy więc stworzyć przykład który nie pozwoli na ten haczyk :)

dla pętli wewnętrznej
i=1
while(i) do
a(i) = 2
i=i+1
od

0E32 LDY #$01
0E34 STY $0E27
0E37 LDA $0E27
0E3A BNE $0E3F
0E3C JMP $0E4D
0E3F LDA #$02
0E41 LDX $0E27
0E44 STA $9C68,X
0E47 INC $0E27
0E4A JMP $0E37
0E4D RTS
i porownywalny kod w C

unsigned char i=1;
while(i)
{
POKE(a+i,1);
i++;
}

lda #$01
L000E: sta L0003
lda L0003
beq L0006
lda L0003
clc
adc #$68
pha
lda #$00
adc #$9C
tax
pla
sta sreg
stx sreg+1
lda #$01
ldy #$00
sta (sreg)
lda L0003
clc
adc #$01
jmp L000D
L0006: rts

Bo to jest kod na tablice.. więc tym razem lepiej jest użyć kodu

#define a 0x9C68

void main(void)
{
unsigned char i=1;
while(i)
{
((unsigned char*)a)[i] = 0x01;
i++;
}
}

co daje lepszy kod:
lda #$01
sta L0003
ldx #$9C
lda #$68
sta L0005
stx L0005+1
L0007: lda L0003
beq L0008
lda L0005
sta sreg
lda L0005+1
sta sreg+1
lda L0003
pha
clc
adc #$01
sta L0003
pla
clc
adc sreg
ldx sreg+1
bcc L000D
inx
L000D: sta sreg
stx sreg+1
lda #$01
ldy #$00
sta (sreg),y
jmp L0007

(chyba)... kurka... to jak mam napisać ten kod w cc65 aby był lepszy od kodu z action (przypominam rok 85', język kompilowany przez Atari)

kompilowane -Oi -Or -Os -Cl oczywiście

Ilmenit 2009-03-31 21:01:17

Wiesz, Action! został napisany do tego, aby był szybki i tworzył ładny kod na 6502, przez co język jest ubogi w stosunku do C.
Co najważniejesze nie ma rekurencji, a bez tego możesz robić znacznie wiecej optymalizacji.

Najprościej ten kod zapisać w C jako:

#include
#define a 0x9C68
void main(void)
{
register unsigned char i;
for (i=1;i;++i)
POKE(i + a,1);
}

ale według specyfikacji języka C tworzymy w ten sposób zmienną na stosie (dla rekurencji).
1. Jeżeli nie używamy rekurencji, to można opcją kompilatora takie zmienne traktować jako statyczne i kod będzie szybszy (choć już niezgodny z C).
2. Można też przenieść "unsigned char i" do zasięgu globalnego, ale odpada wtedy słowo register, które umieszcza zmienne na stronie zerowej.

W celu optymalizacji najprościej potraktować zmienną jako stały adres na stronie zerowej np. 0xFB,
ale wtedy traci się wygodę automatycznego przydziału adresów...

Jeden z dawnych branchy CC65 miał słowo zstatic do definiowania zmiennych zero-page. Niestety to nie zostało użyte i w CC65 trzeba takie zmienne definiować w ASMie i użyć #pragma zpsym(), jak poniżej:

//// test.c
#include
#define a 0x9C68
extern unsigned char i;
#pragma zpsym ("i");
void main(void)
{
for (i=1;i;++i)
((unsigned char*)a)[i] = 0x01;
}
//// zero.asm
.export _i
.segment "ZEROPAGE"
_i: .byte 0
////

Daje to kod wynikowy (_i jest na zero-page, więc zaoszczędzamy cykl):

lda #$01
85 sta _i
A5 L0003: lda _i
F0 beq L0004
A2 ldx #$9C
A9 lda #$68
18 clc
65 adc _i
90 bcc L000E
E8 inx
85 L000E: sta sreg
86 stx sreg+1
A9 lda #$01
A0 ldy #$00
91 sta (sreg),y
E6 inc _i
4C jmp L0003
60 L0004: rts

Ilmenit 2009-03-31 21:09:46

Tu opisany inny sposób na wrzucenie zmiennej na stronę zerową:
http://www.cc65.org/mailarchive/2002-05/1373.html

Yosh 2009-03-31 22:04:21

Dzięki, fakt jest ciut lepiej

Co do rekurencji, po to jest kompilator który wie, która funkcja nie jest rekurencyjna.. aby ją zmielił idealnie np

int dodaj(int a, int b)
{
int suma, suma_do_zwrotu ;

suma = 0
suma+=a;
suma+=b;
suma_do_zwrotu = suma
return suma_do_zwrotu;
}

Co każdy rozsądny kompilator (gcc) zwinie do 3 rozkazow (x86)

$ gcc -Os -fomit-frame-pointer test.c -c -S

y@YOSH ~
$ cat test.s
.file "test.c"
.text
.globl _suma
.def _suma; .scl 2; .ty
_suma:
movl 8(%esp), %eax
addl 4(%esp), %eax
ret

język C ma zapewniać rekurencje - ale nie zasłaniać się nią - skoro on wie, że to nie rekurencyjne to niech agresywnie optymalizuje

Język to co innego niż jego kompilator np niektóre języki rozwijają rekurencje, najbardziej znana to ogonowa - wspomniana przy LOGO (przy poprzednim arcie)

implementujesz rekurencyjną pętelkę - a jako wynik masz iteracyjną pętelkę.

robi to oczywiście gcc...

int suma(int b, int a)
{
if (a>0) return suma(b+a, a-1);
return b;
}

rekurencja ? nie sądzę.... (co najwyżej ogonowa)

_suma:
movl 4(%esp), %eax
movl 8(%esp), %edx
L3:
testl %edx, %edx
jle L2
addl %edx, %eax
decl %edx
jmp L3
L2:
ret

W Action! wręcz się cieszę że nie ma rekurencji - jak zauważyłeś da się prostszy kod generować, a w grach rekurencja i tak mało potrzebna.....

Co nie zmienia faktu, że dobry kompilator C wie kiedy co powycinać - bo Język a jego kompilator to dwie różne sprawy. Językiem się wyrażamy, a kompilator ma to zrobić dobrze(tm).

Żeby nie było, że jestem stronniczy... w Action! zapomnieli że jest strona zerowa :)

Ilmenit 2009-03-31 22:18:59

Dobry kompilator wie. CC65 nie jest dobry :-) Nie ma co się dziwić, skoro rozwija go właściwie tylko jedna osoba. A i tak jest lepszy niż "konkurencyjny" z88dk, który nawet nie wspiera podstawowych cech języka C, nie mówiąc już o nowszych standardach.
Z rekurencją jest inny problem, mianowicie rekurencyjnie możesz wołać funkcje A->B->A->B, też w przypadku, gdy znajdują się w innych jednostkach kompilacji. Bez globalnego optymalizatora nic nie poradzisz. Rekurencja ogonowa to szczególny przypadek rekurencji, podobnie jak szczególnym przypadkiem są funkcje, które nie wołają innych funkcji.

Fajnie spojrzeć jak różny kod generuje Action i CC65. Chyba podeślę Ulrichowi powyższe kody, bo nie widziałem, żeby CC65 wygenerował STA Absolute,X, co w praktyce jest bardzo przydatne.

Yosh 2009-03-31 22:27:08

Żeby nie było - ja cc65 lubię - bo .. jest :) tzn nie jest tak źle a napewno kilka osób scena 8bit przez niego zyskała... Action! mimo że jest hackiem ('a co to strona zerowa' 'a rekurencja? nie wiem nie widzialem') zdobył moje serce rokiem wydania

gcc ma też ultra duper przełącznik:
"GCC 4.1 provides the -fwhole-program and --combine options to do this, but you have to pass all translation units you want optimized together to GCC at once. This is very unfriendly to existing makefiles and counter to existing practice."

Podaje się mu wszystko w jednej lini - wszystkie pliki .c... a on to mieli jak może - nic nie widać. ale kod jest krótszy nawet o 25% względem kompilowania każdego pliku osobno -Os i linkowania ich.

Jak trochę o tym czytałem to on to robi baaaaaardzo siłowo - wkleja wszystko razem, każdej zmiennej globalnej daje static - a co! przecież nie będzie widziana z innego modułu skoro wszystko w jednym na raz kompiluje :)

Jak widać, wszyscy orzą jak mogą (fwhole-program fajne do embedded)

Ilmenit 2009-03-31 22:41:06

CC65 przyciągnął na przykład mnie :-) Jak tylko się o nim dowiedziałem, to postanowiłem napisać jakąś grę, bo sentyment odżył, a nie wyobrażam sobie rzeźbienia w czystym ASMie. A tak robię jedną z najbardziej złożonych gier, jakie powstały na małe Atari, mając wygodne IDE Visuala :-)

Yosh 2009-03-31 22:50:24

Jak nostalgia to nostalgia :) mnie też cc65 przyciągnął - też rzeźba w asmie gry do której aż takiej wydajności nie trzeba. Action! użyłem dlatego, że gdy powstał miałem lat 5 :) gdybym miał go od początku atari....... ale w Action! mam wgrany tylko kod include "h9:gra.act" - co powoduje ciągniecie źródeł z katalogu w którym edytuje w notepad++

1) jeden include - mało tekstu - więcej ramu na kod skompilowany
2) często pisze tak, że wszystko się wysypuje - szczerze mówiąc gdybym z Action! zaczynał na real atari to może bym się zniechecił, a może pisał mniejsze przekmniy...

Ciekawe jak w tym cc65 jest drzewo trzymane (i czy wogole) a może to kompilator sterowany składnią ? wiesz coś może (ja wiem, mogę źródła - ale i tak za dużo czasu dla Atari w tym tygodniu :P) może mógłbym dopisać trochę optymalizatora :P

Yosh 2009-03-31 22:55:39

o kurka ale jestem zmęczony "mnie też cc65 przyciągnął - też rzeźba w asmie gry do której aż takiej wydajności nie trzeba." - miało być że nie chciało mi się rzeźbić w asm czegoś co można w cc65/action no i dwa razy "Żeby nie było," .. dobranoc :)

Kaz 2009-04-01 02:32:40

Niezle sie to czyta takie rozwazania za i przeciw. To przypomina mi "Sonde" i Kurka i Kaminskiego.

Tdc - dopisuje postulat mozliwosci wyslania maila na forum do listy rzeczy do zrobienia.

Tdc 2009-04-02 03:47:07

Kaz: dzięki, bo jak widać to może się sprawdzić w praktyce ;)

Ilmenit 2009-04-14 12:45:23

Im więcej bawię się CC65 tym mniej jestem nim zachwycony... Jest oparty na Small C, przez co generuje paskudnie rozwlekły kod, którego nie da się optymalizować bez kompletnej przebudowy kompilatora. Jednocześnie jest - niestety - najbardziej rozbudowanym kompilatorem C dla 6502... :/

Ilmenit 2009-04-14 17:06:29

@Yosh: Jak widzę CC65 nie potrafi optymalizować kodu, gdy wskaźnik zdefiniujesz na stałe miejsce w pamięci. Spójrz jednak na kod wygenerowany w poniższym przykładzie (cl65 -Osi -Cl -l test.c). Kod nawet ładniejszy niż z Actiona :-)
Jak się jeszcze pobawię to napiszę chyba artka o tym jak pisać w CC65, żeby było szybko.

unsigned char a[255];

void main(void)
{
unsigned char i;
for (i=1;i;++i)
a[i] = 0x02;
}

lda #$01
sta L0004
L0005: lda L0004
beq L0006
ldy L0004
lda #$02
sta _a,y
inc L0004
jmp L0005
L0006: rts