8 Bit Cihazlarda Hızlı Çizgi Çizdirme
Uzun bir aradan sonra yeni bir yazı ile karşınızdayım. Bu yazı biraz talep üzerine gerçekleşti diyebilirim. Birden fazla arkadaşım 8-bit cihazlarda hızlı çizgi çizdirme konusunda çalışmaya başladılar. Ben de bu konuda bir makale yazmaya karar verdim.
Commodore 64 için hazırladığım örneği aşağıdaki linkten indirebilirsiniz:
WASD ile (x1, y1) posizyonunu, UHJK ile de (x2, y2) pozisyonunu değiştirebilirsiniz. SPACE tuşu bağlangıç ve bitiş noktalarında parıldayan spriteları gizler. Çerçevede yer alan renk bloğu rutinin ne kadar CPU yediğini, bloğun rengi ise rutinin tipini göstermektedir. Mavi: az eğimli açılar, Mor: çok eğimli, her Y’ye tek piksel denk gelen açılar anlamına gelmektedir. Bu programda dik açılar için fazla optimizasyon yapılmamış, az eğimli açılara odaklanılmıştır. Nedenini yazının ilerleyen kısımlarında göreceksiniz.
Bu kodu derlemek için ihtiyacınız olan Assembly Editörü’nü aşağıdaki linkten indirebilirsiniz:
Sonucu PC üzerinde görmek isterseniz de;
işinizi görecektir. Kodu her platformda derleyebilirsiniz. Gerekli parametreleri “compile.bat” dosyasında göreceksiniz. Windows kullanıyorsanız doğrudan “compile.bat” dosyasının içerisinde yer alan;
- KICKASM_PATH
- VICE_PATH
değişkenlerini kendi bilgisayarınızda bulunan dosya yolları ile değiştirecek olursanız, “compile.bat” dosyasını çalıştırdığınızda program otomatik olarak derlenip, Vice’ın içerisinde çalışmaya başlayacaktır.
Şimdi bu kadar giriş yaptıktan sonra biraz açıklamalara geçelim.
Bilgisayarda Çizgi Nasıl Çizdirilir?
Bu konuya eski makalelerimde (Ekran Kartlarının Geleceği) değinmiştim ama teknik detayına girmemiştim.
Hemen bir grafik üzerinden hızlı bir giriş yapalım;
Bu örnekte (1,3) – (12,10) koordinatları arasına bir çizgi çekmek istiyoruz. Başlangıç noktamız (1,3)’den itibaren ne kadar sağa, ne kadar aşağı ilerlememiz gerekiyor? Bunu deltaX ve deltaY değerleri olarak (kısaca dx ve dy) hızlıca son ve baş noktalar arasındaki farkı alarak hesaplıyoruz. Sonra da dx/dy ile eğimi hesaplayabiliyoruz. Artık elimide bir eğim değeri var. Bunu nasıl kullanabiliriz?
Elimizde eğim değeri olmasaydı da oran orantı ile bulunduğumuz x’e karşılık gelen y’yi bulabilirdik.
Mesela x=8 için y kaçtır?
y = y1+(x-x1)/dx*dy
y = 3+(8-1)/11*7 = 7.45
olarak bulduk. 7.45’i yuvaladığımızda 7 elde ediyoruz. Demek ki x: 8 için y: 7 olması gerekiyormuş.
Şimdi buradaki “/dx*dy”yi “*m” olarak değiştirebiliriz. Böylece bölmeden kurtulur ve tek bir çarpma ile sonucu bulabiliriz.
y = y1+(x-x1)*m
y = 3+(8-1)*0.6363 = 7.45, aynı sonucu elde ettik.
Bu şekilde ilerleyerek çizgiyi çizebiliriz. Yani;
dx = x2 - x1
dy = y2 - y1
m = dy / dx
for(x = x1; x <= x2; x++) {
y = y1 + (x - x1) * m
plot(x, y)
}
Bu kod yukarıdaki çizgiyi çizmemizi sağlayacaktır.
Bu kadar, bir sonraki makalede görüşmek üzere… Keşke 8 Bit cihazlar için iş bu kadar kolay olsaydı. İlk olarak çarpma / bölme işlemlerinden kurtulmamız gerekiyor. Bu da Bresenham ve türevleri olan algoritmalar ile mümkün. Bu algoritmaları bir çok yerde benzer formüllerle bulabilirsiniz. Kişisel tercihim şu formüldür (sadece yukarıdaki dx > dy ihtimaline özel olarak çalışacak rutin).
dx = x2 - x1
dy = y2 - y1
stepCounter = dx / 2
x = x1
y = y1
do {
plot(x, y)
stepCounter += dy
if(stepCounter >= dx) {
stepCounter -= dx
y++
}
x++
} while(x <= x2)
Burada dikkat ederseniz çarpma / bölme kullanmıyoruz. Baştaki “/ 2” 8 bir bilgisayarlarda “sağa bir bit kaydırma” işlemi çeklinde gerçekleştirilebiliyor, diğer bir ifade ile “dx >> 1” olarak düşünebilirsiniz. “stepCounter” ismini verdiğimiz sayaç 0’dan değil “dx / 2″den başlıyor. Bunun nedeni çizginin her y satırındaki parçasının başına ve sonuna eşit dağıtılması, simetrik ve düzgün bir çizgi oluşturmamızı sağlaması. Yani ilk parçanın ortasından çizmeye başlıyoruz.
Bresenham’ın bulduğu ilginç fikir şu. Elimizde dx = 11 ve dy = 7 varsa, biz 7/11 ya da 11/7 şeklinde küsüratlı sayılar bulup, bunları kullanmak yerine;
- Sayaca 7 ekle, 11’i geçti mi?
- Geçmediyse pixeli koy ama aynı satırda kal, geçtiyse alt satıra geç ve sayaçtan 11 çıkar, küsürat sayaçta kalsın.
fikrini ortaya atmış olmasıdır. Bu sayacın dy değerlerini ekleye ekleye gidip, dx sınırını geçip geçmediği kontrol etmesi, geçmediği sürece aynı satırda kalması ve geçtiği noktada alt satıra inip sayacı sıfırlamak yerine, sayaçtan sınır değerini (dx) çıkarmak bizi küsüratlı sayılarla çalışmaktan kurtarıyor.
Evet, ve işte makalenin sonuna… yine gelemedik. Belki PC’de eski DOS günlerinde olsaydık ve 13h mode’unda 320×200 ekran açmış olsaydık makaleyi burada (en azından yukarıdaki çizgiyi çektirme ihtimali için) sonlandırabilirdik. Çünkü ekrandaki her bir pixel bir byte’a denk geliyor 13h modunda. Ama Commodore 64, ZX Spectrum, Amstrad CPC, Atari 800 XL ve onlarca benzeri 8 bit cihaz ele alındığında grafik modlarında çoğunlukla bir byte’a birden fazla pixel denk gelir. Mesela 320×200 tek renk modunda çizim yapmak isterseniz bir byte’ın her bir biti bir pixele denk gelecektir. Sizin o byte’a tek tek “plot(x, y)” yapmanız demek aynı işlemi bir byte için 8 kere tekrar ettirmeniz demektir ki asıl kabus burada başlıyor. Daha da kötüsü önceki yazdığınız değerlerin silinmemesi için byte’a doğrudan değer yazmanız mümkün değil, bitleri ORlamak durumundasınız. Commodore 64 Assembly (6502/6510) diliyle ifade etmek gerekirse;
lda bitMask
ora plotAddress
sta plotAddress
...
lda bitMask
ora plotAddress
sta plotAddress
...
şeklinde 8 kere tekrar eden bir kod çıkıyor ortaya. Ama biz yukarıdaki örnekte ilk satırda iki pixel, ikinci satırda tek pixel, üçünsü satırda yine iki pixel şeklinde çizim yaptırmamız gerektiğini biliyoruz. Yani aslında şu kod işimizi görüyor (bu defa adres değerine y index’ini de ekleyip daha gerçekçi bir kod oluşturalım)
ldy y1
lda #%11000000
ora (plotAddress),y
sta (plotAddress),y
iny
lda #%00100000
ora (plotAddress),y
sta (plotAddress),y
iny
lda #%00011000
ora (plotAddress),y
sta (plotAddress),y
İşte ulaşmak isteyeceğimiz ideal kod budur. Ama tüm (x1, y1) – (x2, y2) olasılıkları için böyle bir açık (unrolled) kod oluşturmak ağırlıklı olarak 16k – 128k arasında belleklere sahip 8 bit makineler için imkansıza yakındır ya da çok sınırlı bir alana çizim yapabilmenizi sağlayacaktır. Peki nasıl bir çözüm bulabiliriz?
En tepede paylaştığım Commodore 64 örneğinde kaynak kodları, özellikle “line.asm” dosyasını incelediğinizde şöyle bir çözümle karşılaşacaksınız. Bu benim bulduğum büyük bir inovasyon değil, scenerlar yıllardır bu yöntemin çeşitli varyasyonlarını kullanıyorlar. Örneğin Codebase64 sitesinde şu şekilde paylaşılmış bir örnek mevcut.
Lines by Bitbreaker / Oxyron ^ Arsenic ^ Nuance
Benim burada hazırladığım rutin Commodore 64’ün multicolor çözünürlüğüne özel. Çözünürlük 160×200 ve her bir karakter bloğu 4×8 çözünürlükte. Yani çizgi çizmeye başlama ihtimali olan pozisyonlar sadece 4 ihtimale düşüyor. Bu da bize çok daha az ihtimalde (4+3+2+1 = 10 ihtimal) hızlı bir açık rutin yazabilmemizi sağlıyor. Ancak bu durumda da karşımıza her pixelin iki bitten oluşması ve 3 farklı renk için üç farklı bit patternine özel kod yazılması gereksinimi ortaya çıkıyor. Burada makrolar yardımımıza koşuyor. Kodu bir defa yazıp, makroları farklı parametrelerle çağırarak farklı renkler için gerekli kodları üretebiliyoruz.
İlk olarak nereden başlamamız gerekiyor? İlk iş kaç karakter bloğu uzunluğunda çizim yapacağımızı bulmak
lda x1
and #3
sec
adc dx
lsr
lsr
sta charBlock
Bu rutin x1’in karakter içinde kaçıncı pixele denk geldiğini alıp (0-3 aralığı), buna dx’i yani çizginin yataydaki genişliğini ekleyip, sonucu 4’e bölerek toplam karakter sayısını buluyor. Artık kaç bytelık alanda ilerlememiz gerektiğini biliyoruz.
Özel olarak değerlendirmemiz gereken 3 durum var;
- Tek karakter bloğu: Çizimin aynı karakter kolonu içerisinde başlayıp yine aynı karakter kolonu içinde bitmesi ihtimali. Bunu özel olarak ele almalıyız. İlk bu ihtimal değineceğiz.
- Normal karakter blokları: Bunlar arada kalan bloklar. Sadece çizginin ilk başlangıç noktası için giriş bölümünde ufak bir değişiklik yapmak gerekiyor ama hem başlangıç, hem de arada kalan karakter blokları aynı kod ile çizdirilebiliyor. Normal karakter bloklarının özelliği hangi pikselden başlarsa başlasın karakterin sonuna kadar ilerlemesi.
- Son karakter bloğu: Çizginin sona erdiği noktada hangi pikselde duracağımızı iyi değerlendirmemiz gerekiyor. Burada da ihtimaller çok fazla değil. O nedenle normal karakter bloklarında olduğu gibi açık rutinlerle tüm ihtimaller hızlı bir biçimde çizdirilebiliyor ve doğru noktada çizim sonlandırılabiliyor.
Şimdi tek tek ele alalım.
Tek Karakter Bloğu
“charBlock” değerinde özel olarak ele almamız gereken bir durum değerin 0 olması. Yani bu ne demek? Aynı karakter bloğu (kolonu) içinde başlayıp, yine aynı karakter bloğu içinde çizimi bitireceğiz demek. Y’de ilerleyebiliriz, ama X’de aynı karakter kolonunda başlayıp bitiriyoruz çizimi. Çizim 4 pixel genişliğinde olduğu durumda diğer karakter bloğuna çizim yapmasak da ulaşmış oluyoruz, yani karakterin sonuna eriştiğimiz noktada charBlock değeri 0 değil 1 oluyor. Bizim burada özel olarak ele almamız gereken;
- Sadece 1. piksel (1000)
- 1. pikselden 2. piksele (1100)
- 1. pikselden 3. piksele (1110)
- Sadece 2. piksel (0100)
- 2. pikselden 3. piksele (0110)
- Sadece 3. piksel (0010)
şeklinde 6 çizilme ihtimali çıkıyor. Elbette ki bu arada yine eğime göre Y’de ilerlemeyi hesaplamamız gerekiyor. Kodun içerisinde “singleCharBlock” olarak geçen bölümde bunun nasıl ele alındığını görebilirsiniz, kendiniz de bu ihtimalleri daha hızlı çalışacak biçimde ele alabilirsiniz. Sadece en fazla 3 plotla sınırlı kaldığı için ben bu kısmı hafızada az yer kaplayacak şekilde kodlamayı tercih ettim. Rekor kırmak gibi bir niyetiniz varsa örnektekinden daha iyisini kodlamanızı öneririm.
stx temp // X'de bulunan o noktadaki değer stepCounter
ldx x1
jmp singleCharBlock
...
singleCharBlockNext:
lda temp
sec
sbc dy
bcs !+
adc dx
stepY(dir)
!: sta temp
inx
singleCharBlock:
lda mask,x
ora (plotAddress),y
sta (plotAddress),y
cpx x2
bcc singleCharBlockNext
rts
Örnek kod bu biçimde, “singleCharBlock”dan rutine giriyoruz ve loop ile çizim yapıyoruz, aynı adrese de birden fazla kez yazma durumumuz söz konusu, yani bahsetmiş olduğum optimizasyonlar burada kullanılmıyor.
Gelelim geriye kalan iki farklı çizim rutinine.
Normal Karakter Blokları
Burada yine ihtimalleri göz önüne almamız lazım. İhtimaller şunlar.
- Bloğun 1. pikselinden çizmeye başlamak (b_1000)
- Bloğun 2. pikselinden çizmeye başlamak (b_0100)
- Bloğun 3. pikselinden çizmeye başlamak (b_0010)
- Bloğun 4. pikselinden çizmeye başlamak (b_0001)
Kodun içinde parantez içinde verilen etiketleri inceleyebilirsiniz. Öncelikle b_1000 ihtimalini ele almak istiyorum.
b_1000:
txa
sbc dy
bcc d_1000
sbc dy
bcc d_1100
sbc dy
bcc d_1110
sbc dy
bcc d_1111
tax
lda #(%11111111 & colorMask)
jmp b_1000_plotAddress+2
Kodun içinde İngilizce olarak yorumlar yazmış durumdayım. Burada yorumlar sildim. Tek tek ne olup bittiğini açıklayacağım. Burada yaptığımız şey şu. “sbc dy” satırları önceden bahsettiğim piksel piksel ilerlerken “stepCounter” değişkeni üzerinde ilerlememizle aynı şey. Ancak burada pozitif değil, negatif yönde ilerliyoruz. Aksi taktirde dx’den büyük mü diye sorgulamak için fazladan bir compare (cmp) komutu kullanmak zorunda kalırdık. dx’den geriye doğru ilerleyip, değer sıfırdan küçük olunca dx ekliyoruz diye düşünebilirsiniz. Asıl dikkat edilmesi gereken şey şu, dx’de ilerliyoruz ama sıfıra ulaşmadığı sürece çizim yapmıyoruz. İşte bu rutinin kilit noktası bu. ne zaman ki sıfıra ulaşıyoruz, oradaki ihtimallerden biri gerçekleşiyor. Diyelim “bcc d_1110” noktasında değer sıfırın altına düştü. Doğrudan “1110” şeklinde tek solukta pikselleri yazıyoruz. Daha sonra bir alt ya da üst (çizginin yönüne göre) satıra geçip, çizime kaldığımız noktadan devam ediyoruz. Şimdi örnek icabı “d_1110″ı inceleyelim.
d_1110:
tax
lda #(%11111100 & colorMask)
jmp b_0001_plotAddress
b_0001_plotAddress:
ora (plotAddress),y
sta (plotAddress),y
iny
b_0001:
txa
adc dxMinusDy
...
Çizim noktasına ulaştığımızda ilk iş olarak accumulatordeki “stepCounter” değerimizi x’e aktararak korunmasını sağlıyoruz. Sonra doğrudan accumulator’e %11111100 şeklinde yazmamız gereken değeri yazıyoruz (1110 ihtimalindeyiz, hatırlatırım). Her bir piksel iki bit ile ifade edildiği için durum bu şekilde. Ancak burada farklı renkler kullanmak isteseydik yazmamız gereken değerler %10101000 ve %01010100 olacaktı. Bunu da makromuza geçtiğimiz “colorMask” değişkeni ile AND’leyerek sağlıyoruz. colorMask %11111111 olduğu durumda birebir aynı sonucu alırız ama %10101010, %01010101 gibi değerler vererek diğer 2 rengi, %10011001 gibi değerlerle dithering için gereken değerleri elde edebiliriz.
Değer elimizde hazır. Peki değeri nasıl yazdıracağız? Burada “nasıl?”dan daha önemli olan sorun “nerede?”. Çizdirdiğimiz patern neydi? “1110”. Bu durumda alt satırda 4. pikselden devam etmemiz lazım, öyle değil mi? Yani gitmemiz gereken rutin “0001”. Rutinlerin öncesinde plot rutinleri de yer alıyor. Yani “b_0001″in hemen üstünde “b_0001_plotAddress” bulunuyor. Burada doğrudan “ora/sta” ile değer yazıldıktan sonra “iny” ile bir alttaki satıra ulaşılıyor. Kodu inceleyecek olursanız “iny” yerine “stepY(dir)” şeklinde bir makro göreceksiniz. Aşağı değil yukarı ilerlediğimiz durumda “dey” kullanmamız gerektiği için bu noktalarda bu şekilde kullanımlar var. Ancak en başta ele aldığımız grafikteki çizgiyi çektirmek için gereken ihtimal yukarıda verdiğim kod parçasıyla uyuşuyor.
Şimdi son dikkat çekilmesi gereken yere geldik. “b_1000” rutinimiz “sbc dy” ile başlamıştı. Ancak burada “adc dxMinusDy” bulunuyor. Bunun sebebi burada aslında yapmamız gerekenin şu olması;
adc dx
sbc dy
Neden? Çünkü sayacımız sıfırın altına düştü de buraya ulaştık, önce sayacı dx ekleyerek pozitif noktaya getirmemiz, sonra bir sonraki adım için yine dy’yi çıkarmamız gerkiyor. Bunun için bu rutinlere ulaşmadan önce çizgi rutinimizin başlarında;
lda dx
sec
sbc dy
sta dxMinusDy
şeklinde tek bir değer hesaplıyoruz. Sonra “adc dxMinusDy” ile tek opcode’da sayacı ihtiyacımız olan değere getiriyoruz.
Evet, biraz uzun ve çok fazla dallanan bir kod yapısı ama sabredin, sonlarına geldik. En hızlı yol genellikle en kolay ve okunaklı yol değildir.
Son olarak şöyle bir durumla karşılaşıyoruz. Bu “txa” ve “adc dxMinusDy” ile giren rutinler sadece alt satıra geçilmişse bu şekilde başlamalı. Ya çizginin başlangıç noktası 2., 3., 4. piksellere denk gelirse ne olacak? Evet, o ihtimaller için de başı “txa” ve “sbc dy” ile başlayan versiyonları üretmek lazım ama sırf şu kadarcık iş için o kadar büyük kopyalar oluşturmak istemeyerek, hızdan az bir ödün vererek bu ihtimaller için şu rutinleri oluşturdum.
b_0100_s:
txa
sbc dy
jmp b_0100+3
b_0010_s:
txa
sbc dy
jmp b_0010+3
b_0001_s:
txa
sbc dy
jmp b_0001+3
Yani dy’yi çıkardıktan sonra diğer rutinlerin ilk 3 byte’ını es geçerek yoluna devam et. İlk 3 byte “txa” ve “adc dxMinusDy” komutlarının toplamı (1+2).
Bu rutinleri çağıran ilk girişteki kod da şu şekilde.
lda x1
and #%00000011
beq b_1000
cmp #1
beq b_0100_s
cmp #2
beq b_0010_s
jmp b_0001_s
Yani x1’in alt 2 bitine bak (0-3 aralığı bir değer çıkacaktır). Sıfır ise “b_1000″e git, en baştan temiz temiz çizmeye başla. 1 ise “b_0100_s”e git, 2 ise “b_0010_s”e git, son ihtimalde de “b_0001_s”e git ve çizim rutinlerine düzgün noktadan giriş yap.
En karışık ihtimal buydu ve bunu noktalamış olduk. Gelelim son ihtimale.
Son Karakter Bloğu
Bir karakter bloğunu bitirdiğimiz zaman blok sayacımızı (charBlock) bir azaltıyoruz ve bunun son blok olup olmadığına bakıyoruz. Son blok değilse bir sonraki kolona göre bazı ayarlamalar yapıp (plotAddress’e 128 ekleyip) sonra yine karakterin başından çizim yapacak rutine sıçrıyoruz. Bunu yaptığımız kod kısmı şurası.
b_1000_plotAddress:
ora (plotAddress),y
sta (plotAddress),y
tya
eor #$80
tay
bmi !+
inc plotAddress+1
!:
dec charBlock
bne b_1000
128 ($80) eklemek için adc/sbc yerine eor kullanımı carry flag ile uğraşmamak için, yoksa amaç sadece toplama yapmak, 128 hariç hiç bir değer için de bu şekilde kullanamazdık, byte’ın tam yarısına denk gelmesinin avantajı. 16×16 karakterlik bir alana çizim yaptığımızdan 16*8 = 128 olduğu için güzel denk gelen bir durum bu.
Burada son bölüme dikkat edelim. “charBlock” bir azaltılıyor. 0’dan başka bir değerse “b_1000″e gidiliyor, tanıdık bir rutin, bloğun başından çizim yapılmaya devam ediliyor. Peki sıfıra ulaşmışsak ne olacak?
lda x2
clc
adc #1
and #3
beq !out+
cmp #2
beq l_1100
cmp #3
beq l_1110
l_1000:
lda #(%11000000 & colorMask)
ora (plotAddress),y
sta (plotAddress),y
!out:
rts
Arada atlanılan “l_1100” ve “l_1110” harici oldukça kısa ve açık bir rutin. x2’ye bir eklememizin nedeni x2’ye kadar değil x2 de dahil çizim yapmak istememiz. Aslında XOR (C64’de bilinen ismiyle EOR) filler tarzı kodlarda bir önceki pikselde bitirmek daha doğru oluyor (ki ben de şu anda bu rutini kendi kodlarımda x2 hariç çizecek şekilde kullanıyorum). Ama burada size tam düzgün bir rutin göstermek istediğim için bir ekledim.
Eğer çizecek bir şey kalmamışsa çıkışa git, kalmışsa kaç piksel kalmış? 1 piksel kalmışsa zaten bas pikseli çık (l_1000), başka derdimiz yok. Ama 2 ya da 3 piksel kalmışsa ne olacak?
İki piksel kalmışsa (l_1100)
l_1100:
txa
sbc dy
bcs !+
lda #(%11000000 & colorMask)
ora (plotAddress),y
sta (plotAddress),y
stepY(dir)
lda #(%00110000 & colorMask)
ora (plotAddress),y
sta (plotAddress),y
rts
!:
lda #(%11110000 & colorMask)
ora (plotAddress),y
sta (plotAddress),y
rts
Arada diğer satıra geçmek gerekiyor mu gerekmiyor mu? Gerekiyorsa bu satıra 1000 yaz, diğer satıra geç ve 0100 yaz ve bitir. Eğer geçmek gerekmiyorsa doğrudan bu satıra 1100 yaz ve çık.
Üç piksel kalmışsa (l_1110)
l_1110:
txa
sbc dy
bcs !+
tax
lda #(%11000000 & colorMask)
ora (plotAddress),y
sta (plotAddress),y
stepY(dir)
txa
adc dxMinusDy
bcs !next+
lda #(%00110000 & colorMask)
ora (plotAddress),y
sta (plotAddress),y
stepY(dir)
lda #(%00001100 & colorMask)
ora (plotAddress),y
sta (plotAddress),y
rts
!next:
lda #(%00111100 & colorMask)
ora (plotAddress),y
sta (plotAddress),y
rts
!:
sbc dy
bcs !+
lda #(%11110000 & colorMask)
ora (plotAddress),y
sta (plotAddress),y
stepY(dir)
lda #(%00001100 & colorMask)
ora (plotAddress),y
sta (plotAddress),y
rts
!:
lda #(%11111100 & colorMask)
ora (plotAddress),y
sta (plotAddress),y
rts
İhtimaller artınca kod iyice uzuyor. Ama unutmayın, kod uzuyor ama çalışan kod bunun tamamı değil. Ya 1000 0100 0010 şeklinde üç satıra üç tane byte set edecek, ya 1100 0010 / 1000 0110 gibi iki byte ya da son ihtimalde doğrudan bulunduğu yere 1110 yazıp çıkıp gidecek.
Beklediğinizden daha karmaşık çıkmış olabilir. Ama en hızlıyı hedeflediğinizde yol böyle rutinlerden geçiyor. Bu arada elbette ki bunun en hızlı rutin olması gibi bir iddiam da yok, büyük ihtimalle üzerinde bir kaç saat daha çalışsam önemli derecede hızlandırabilirim rutinleri. Ama daha ilk optimizasyonları yapmaya başladığım gibi (bazı jmp’lardan kurtulmak için rutinlerin sıralarını değiştirdim) kod daha da anlaşılmaz bir hal almaya başladı. Biraz daha devam edecek olsaydım bu dökümanı hazırlamak, sizin kodları inceleyip anlamanız çok daha zor bir duruma gelecekti. O yüzden optimizasyonları henüz kod okunabilir durumdayken kesme kararı aldım.
Örnek sadece tek renk çizgi çekiyor. Diğer renkleri çizdirmek isterseniz ne yapmanız gerekiyor? Bu örneğin dışında uyarladığım yerlerde bu değişiklikleri yaptım. Kodun içinde;
// This needs to be changed for different colors
Şeklinde işaretlenmiş kısımlar var. Line rutinini çağırmadan önce bu kısımları ilgili renk seçeneğini oluşturacak tablolara ya da rutinlere yönlendirecek şekilde modifiye etmeniz gerekiyor.
Örneğin; drawLine11down, drawLine10down, drawLine01down, drawLine11up, drawLine10up, drawLine01up rutinlerinin başına “.align $100” koyarak 256 bytelık blokların başına gelecek şekilde yerleştirdim kendi örneklerimde. Bu sayede sadece high byte’larını değiştirerek istediğim rutine yönlendirebiliyorum. Eğer bu dediklerimi anlamamışsanız yazının buraya kadar geçen bölümünü de anlamış olma ihtimaliniz olmadığından gönül rahatlığıyla açıklamaları bu noktada kesiyorum.
Başka söylenebilecek neler var? x1’in x2’den büyük, y1’in y2’den büyük olması gibi ihtimaller sorun yaratır. Bunlar kodun için bazen sıralanarak yani (x1,y1) – (x2,y2) çiflerinin yerlerini değiştirerek, bazen farklı yöntemlerle çözülmüş durumda. Dik açılarda çizimden hemen hiç bahsetmedik, çünkü bu rutinin asıl amacı bit bit değil byte byte çizim yaptırmaya yönelik örnek oluşturmaktı. Dik açılardan zaten her byte’a 1 pixel denk geliyor, bu optimizasyonların hiç biri işe yaramıyor. Dik açıları çok basit ve yavaş sayılabilecek bir loop olarak bıraktım. Kendi örneklerimde bu line rutinini XOR fill ile kullandığım için zaten dik açıların sadece en üst piksellerini çizdiriyorum.
Mevcut örnekte geçmediği için bu kod parçasının örneğini de vereyim burada. Bu kod dik açılardan her bir piksel kolonunun sadece en üst pikselini çizer. XOR fill için gerekli olan çizim tekniği bu olduğu için böyle yapıyoruz. Ekranda sadece çizgi olarak gözükecek objelerde bu tekniği kullanmıyoruz.
narrowLine:
// do {
// setPixel(px, py);
// stepCounter += dy;
// while(stepCounter >= dx) {
// stepCounter -= dx;
// py++;
// }
// px++;
// } while(px < x2);
!narrowLoop1:
lda offsetXLo,x
sta plotAddress
.label offsetHiAddress1 = *+2
lda offsetX1Hi,x
sta plotAddress+1
ob1: lda orBitMask1,x
ora (plotAddress),y
sta (plotAddress),y
lda stepCounter
clc
adc dy
!narrowLoop2:
cmp dx
bcc !+
sbc dx
.label stepY2 = *
iny
bne !narrowLoop2-
!:
sta stepCounter
inx
cpx x2
bcc !narrowLoop1-
rts
128×128 Piksellik Alandan Daha Geniş Alanlara Çizim Yapılması
Bu rutinin en büyük dayanaklarından biri “Y” indeksiyle kolonlarda düzgün bir biçimde ilerleyebilmek. Ancak ne Commodore 64’ün ne de diğer 8 bit bilgisayar platformlarının normal grafik modları bu şekilde lineer ilerlemezler. Bunun için karakterleri kolonlarda alt alta dizip, kendimiz böyle bir çizim modu oluşturuyoruz. Doğrudan bitmap ekrana çizim yapmak istesek bu rutine bir çok yük bindirecek şeyi aralara eklememiz gerekir. Ama temel prensip değişmez. Ben size temel prensibi anlatmak için böyle bir örnek geliştirdim.
Bir diğer yöntem ise yine karakter seti üzerine çizim yapmak ama birden fazla karakter seti kullanmak. Peki bu nasıl oluyor, örnekleri var mıdır?
Örneğin “Snapshot / Glance” demosunda geçen bu robot kol efektimde 16×16 yerine 32×16’lık bir alana çizim yaptırmıştım. 32×16 = 512 karakter ediyor ve bir karakter seti 256 karakter ile sınırlı. Bu nedenle alt alta 2 tane 32×8’lik alan koyup, arada tarama yakalatarak karakter setlerini değiştiriyordum. Bu durumda kolon yükseklikleri yine 128 olsa da 64’ü geçip geçmediğini kontrol etmek, buna göre diğer karakter setine çizim yaptırmak gerekiyordu ama bunun ekstra maliyeti o kadar da yüksek değildir. Yani y’de ilerleme işleminde 64’e kadar ve 64’den sonra şeklinde kontroller koyarak bu rutini 256×128’lik bir alana çizim yapacak hale getirebilir. 3. 4. karakter setlerini de kullanarak tam ekran efektler de yapabiliriz. Ama kompleksitenin çok artmaması için size önerim 256’dan daha geniş alanlara bu şekilde çizim yaptırmaktan kaçınmanız yönünde olacaktır. 128 yerine bir karakter seti daha ekleyerek 192’lik alana çizim yaptıracak olursanız Atari 800 XL’de tüm ekran yüksekliğini kapladınız, C64’de ise 8 piksel boşluk kaldı demektir.
Diğer 8-Bit Cihazlar
Yazımızın başlığı “8 Bit Cihazlarda Hızlı Çizgi Çizdirme” ancak sadece Commodore 64 üzerinden örneklerle gittik. Sebebi elbette ki şahsen 8 bit cihazlar arasında en hakim olduğum cihazın Commodore 64, en hakim olduğum işlemci çipinin 6502 tabanlı işlemciler olması. Ancak şu günlerde z80 cihazlarla da flört eder durumdayım. Dolayısıyla ilerleyen dönemlerde farklı platformlara adaptasyonlarını paylaşabilirim sizinle. Bu olsun ya da olmasın, bu yazının temel amacı bu konudaki ana prensibi irdelemekti. Aslında bu yazıda anlatılan konular sadece çizgi çizdirme ile sınırlı da değil. 8 bit cihazlarda bütün (hatırı sayılır) demo efektleri bu şekilde planlanıp kodlanıyor. Tabii efektten efekte mücadeleler, yapılan optimizasyonlar, CPU ve hafıza arasında verilen savaşlar değişiyor. Ama prensip her zaman bir efektin mümkün olan en az cycle’ı harcayarak ekrana çizdirilmesi ve tabii hafızaya sığacak şekilde tasarlanması.
Kodlardaki yorumlar neden İngilizce de bu yazı Türkçe? Amacım kodları indiren bir yabancının da faydalanabilmesi idi. Nasılsa bu yazıda detayları Türkçe olarak da açıkladım. Talep gelirse Türkçe yorum satırları olan bir kaynak kod örneği de hazırlayabilirim.
Bol çizgili, nice 2 boyutlu, 3 boyutlu vektör efektlerine vesile olması dileklerimle, sonraki yazılarımda görüşmek üzere.
En Son Yorumlar