Commodore 64: renowacja i grafika 3D w BASIC-u, C i asemblerze
Przetłumaczone automatycznie z angielskiego. Przeczytaj oryginał

Zastrzeżenie: opisane tu projekty są wynikiem mojej własnej, samodzielnej pracy, wykonanej bez użycia AI. Sam artykuł powstał z pomocą AI.
W czerwcu 2018 roku dostałem Commodore 64 w oryginalnym pudełku. Katalog c64 w moim repozytorium retro-lab jest tego efektem: fotograficzny dziennik renowacji i trzy implementacje tego samego celu – obracającego się modelu 3D wireframe – w trzech językach, w których realnie da się programować tę maszynę.
Renowacja #
Komputer był kompletny, ale brudny i pożółkły. Rozebrałem go, wyczyściłem obudowę i każdy klawisz osobno, po czym złożyłem z powrotem.



W moim domu nie ma już żadnego monitora z wejściem RF. Najwygodniejszym rozwiązaniem okazało się wyjście S-Video podłączone do taniego grabbera USB, z VirtualDubem wyświetlającym obraz na laptopie:

Na co dzień pracowałem na emulatorze VICE, a na prawdziwy sprzęt przechodziłem tylko po to, żeby sprawdzić wynik.
Jeden renderer, trzy języki #
Punktem odniesienia był generic, mój silnik 3D w JavaScripcie z 2013 roku. Jego matematyka (macierze obrotu, kamera, dzielenie perspektywiczne) jest na tyle prosta, że da się ją przenieść niemal na wszystko. Pytanie brzmiało, jak daleko da się z nią zajść na 6510 taktowanym 1 MHz z 64 KB RAM-u.
BASIC 2.0 #
Pierwsza wersja to zwykły Commodore BASIC (main.bas): macierze obrotu w tablicach DIM i rzut perspektywiczny każdego wierzchołka. Na razie nic nie rysuje. Zamiast tego wypisuje rzutowaną współrzędną i liczbę tyknięć zegara, jaką zajęły obliczenia, oraz FRE(1)+65536, żeby zobaczyć, ile pamięci zostało. O to właśnie chodziło: zmierzyć, jak kosztowna jest sama matematyka, zanim napiszę jakikolwiek kod graficzny.


Osobny program, bitmap_mode.bas, włącza tryb bitmapowy dwoma POKE-ami do VIC-II i wypełnia ekran kolorem, w nieskończoność przechodząc przez kolejne kolory:
5 base=2*4096
9 poke 53272,peek(53272)or8
10 poke 53265,peek(53265)or32
15 co = 3
20 for i = base to base+7999: poke i,0: next
30 for i = 1024 to 2023: poke i, co: next
35 co = co + 1
40 goto 20
Najprościej uruchomić którykolwiek z tych listingów, wklejając go do VICE po NEW i wpisując RUN.
C z cc65 #
cc65 to kompilator C, asembler i linker dla rodziny 6502. Jedno polecenie tworzy program z nagłówkiem BASIC-a, więc startuje on przez zwykły autostart w VICE:
cl65 -r -O main.c -t c64
-r włącza zmienne register (cc65 pozwala na maksymalnie 6 na funkcję, trzymanych na stronie zerowej). Model to sześcian zdefiniowany jako wierzchołki i ściany, a renderer przerysowuje obraz tylko wtedy, gdy naciśnięcie klawisza zmienia położenie kamery.

Zastąpienie TGI
cc65 jest dostarczany z TGI, przenośną biblioteką graficzną. Jest wygodna, ale duża i wolna, więc zastąpiłem ją bezpośrednim dostępem do rejestrów VIC-II i CIA. Poniższy kod przełącza w tryb bitmapowy i wybiera bank VIC-II:
void video_init() {
// DATA DIRECTION
TMP1 = *(byte*) 0xDD02;
TMP1 = TMP1 | 3;
*(byte*) 0xDD02 = TMP1;
// VIC-II BANK
TMP1 = *(byte*) 0xDD00;
TMP1 = TMP1 & 252;
*(byte*) 0xDD00 = TMP1;
// BITMAP LOCATION, RELATIVE TO VIC-II BANK
*(byte*)0xD018 = 0x48;
// BITMAP MODE (POKE53265,PEEK(53265)OR32)
TMP1 = *(byte*) 0xD011;
TMP1 = TMP1 | 32;
*(byte*) 0xD011 = TMP1;
video_clear();
}
Drugą optymalizacją było zrezygnowanie z czyszczenia całej bitmapy w każdej klatce. Program zapamiętuje rzutowane wierzchołki z poprzedniej klatki i zamalowuje kolorem tła tylko te linie.
Mapa pamięci
Wiedza o tym, co gdzie się znajduje, to na tej maszynie połowa pracy. Układ, na którym ostatecznie stanąłem w programie cc65:
| Adres | Co |
|---|---|
$0400–$07FF |
ekran tekstowy |
$0800– |
kod programu (nagłówek BASIC-a + skompilowany C) |
do $CFFF |
wolny RAM; sterta rośnie w górę, stos C rośnie w dół od $CFFF |
$D000–$D3FF |
rejestry VIC-II |
$D400–$D7FF |
SID |
$DC00–$DDFF |
CIA 1 i CIA 2 |
$E000–$FFFF |
KERNAL ROM |
Stan klawiatury odczytywany jest ze strony zerowej pod $00CB zamiast przez KERNAL.
Modele z Blendera
Ręczne wpisywanie list wierzchołków szybko się nudzi. Mały skrypt w Rubym czyta plik Wavefront OBJ wyeksportowany z Blendera i wypisuje tablice C gotowe do wklejenia do main.c:
cd models
ruby 0_parser.rb cube.obj
Liczba wierzchołków ma ogromne znaczenie: na C64 obliczenia macierzowe zajmują znacznie więcej czasu niż rysowanie linii.
Asembler 6502 z DASM #
Część w asemblerze to początek trzeciego portu. Używa makr DASM opakowujących procedury z ROM-u KERNAL-a i BASIC-a (wypisanie łańcucha, ustawienie kursora, czyszczenie ekranu, wypisanie liczby) i jest uruchamiana z BASIC-a przez SYS 4096, ponieważ kod jest umieszczony pod $1000:
processor 6502
org $1000
R_PRINT_STRING = $AB1E
R_SET_CURSOR = $E50C
R_CLEAR_SCREEN = $E544
mac puts
lda #<{1}
ldy #>{1}
jsr R_PRINT_STRING
endm

Czego się z tego nauczyłem #
- Ten sam algorytm w BASIC-u, C i asemblerze to najczytelniejsza znana mi lekcja „kosztu abstrakcji”. Kod z cc65 jest dużo większy niż ręcznie pisany asembler, a interpretowany BASIC jest dużo wolniejszy od obu.
- Na sprzęcie 8-bitowym rysowanie jest tanie, a matematyka droga. Arytmetyka wyłącznie całkowitoliczbowa, wstępnie obliczone wartości i przerysowywanie tylko tego, co się zmieniło, liczą się bardziej niż cokolwiek innego.
- Rzut 3D z tego projektu trafił potem do sektora rozruchowego x86, na ESP8266, a później do OpenCL w Simple High Performance Computing.
Źródło: retro-lab/c64. Zobacz też przegląd moich praktycznych projektów.