‘Scene’ Kategorisi Arşivi

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:

Multicolor Line

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:

Kick Assembler

Sonucu PC üzerinde görmek isterseniz de;

Vice

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;

  1. 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.
  2. 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.
  3. 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.

6502 Yeniden Üretimde (W65C02S6PG-14)

Açıkçası benim açımdan inanılmaz heyecan verici bir durum yok. Çünkü ne yazık ki bu yeni üretilen çip 6510’un, yani Commodore 64’ün içindeki çipin yerini alabilecek birşey değil. Ancak yine de 6502 uyumlu ve 6502’nin 1 MHZ’lik hızının yanısıra 14 MHZ hızında üretilmiş olması çok güzel bir gelişme.

6502 uyumlu W65C02S6PG-14

6502 uyumlu W65C02S6PG-14

WDC (Western Design Center) firması tarafından 40 pin olarak üretilen çip £4.90 fiyatından satışa sunulacakmış. Ayrıca firma FPGA’ler için 65C02 isimli bir de sanal versiyon yayınlayacağını duyurmuş. Yani modern donanımlarda 14 MHZ’lik 6502 kullanarak ilginç retro projelere imza atılabilir ve elimizdeki 30+ senelik 6502 tabanlı kod birikimiyle hızlıca bu yeni cihazlar için çeşitli yazılımlar, demolar, oyunlar geliştirebiliriz.

“Commodore 64” 30 Yaşında!

Benden az daha genç olan Commodore 64 ilk kez Commodore International firması bünyesinde yer alan Commodore Business Machines ya da yaygın kullanılan kısaltmasıyla CBM tarafından 1982 yılının Ocak ayında tanıtılmıştı. Aynı yılın bahar aylarında üretime giren ünlü cihaz, Ağustos ayında 595 dolarlık fiyatıyla elektronik/bilgisayar marketlerin raflarında yerini almıştı.

 

Commodore Logosu

Commodore 64, sık sık C64 ya da Commodore’un sol tarafta gördüğünüz ünlü logosu yüzünden C=64 şeklinde kısaltmalarla kullanılmıştır.

 

 

 

Commodore 64’ün donanımsal özellikleri şu şekilde özetlenebilir.

İşlemci Çipi: MOS Teknoloji 6510 – ~1 Mhz (PAL: 0.985 Mhz, NTSC: 1.023 Mhz)
Bellek: 64 KB RAM + 20 KB ROM
Görüntü Çipi: VIC-II (320×200 tek renk ya da 160×200 16 renk grafik modu ve 8 donanımsal yaratık desteği)
Ses Çipi: SID (8 bit 3 kanal, eski modellerde 6581, yeni modellerde 8580 kullanılmıştır)
Bağlantı Noktaları: Güç ünitesi, 2 x joystick (ya da mouse/paddle v.b.), kartuş, anten bağlantısı (RF), eski component A/V çıkışı (luma, chroma, mono audio), disk drive, teyp ve RS-232

Commodore 64 Modelleri:

Aslında detaya girildiğinde daha fazla model olduğu görülse de özellikle dış görünüş ve SID çipleri birbirinden farklı olan 2 ana model aşağıdaki gibi kategorilendirilebilir.

Eski kasa (ilk üretim):

Commodore 64 (Eski Kasa)

Yeni kasa:

Commodore 64 (Yeni Kasa)

30 Yılda Neler Değişti?

 

Bu konuda uzun uzun çok şey yazılabilir ancak şu şekilde açıklanabilir sanırım.

Yie Ar Kung-Fu (Orjinal 1985)

Yie Ar Kung-Fu (By Veto 2010)

Bu grafiklerdeki ilerlemeye basit bir örnek. Üretildiği tarihlerde kesinlikle mümkün olmayan ve yaklaşık olarak 90’ların başından günümüze kadar kademe kademe gelişen yeni grafik modlarının kalitede en son ulaştığı seviye diyebileceğim NUFLI modundan başka örnekler verecek olursak;

NUFLI 1

NUFLI 2

İşte 80’lerdeki o basit görünüşlü oyun ve demolardan görsel kalite açısından gelinen nokta budur.

Geçen 30 Yıl İçersinde Donanımın Sınırlarındaki Değişim

 

Değişim elbette ki sadece grafik tarafında olmadı. Aslında ilk üretilen Commodore 64’den hiçbir farkı olmayan cihazların donanım sınırları zamanla genişledi. Yani eskiden donanımın yapabileceği öngörülmeyen şeyler, günümüzde oldukça sıradan bir biçimde yapılabiliyor. Örneğin Commodore 64’de 320×200 çözünürlükteki iç ekranın dışında kalan çerçeve (border), daha 80’li yıllarda açılarak bu alana yaratıklardan (sprite) oluşan grafikler basılmaya başlandı. Önce alt/üst, sonrasında da iki yandaki çerçeveler bazı donanım aldatmacalarıyla açılabildi. İlginçtir ki bu o dönemde birbirinden tamamen bağımsız olarak üretilmiş birçok bilgisayarda benzer yöntemlerle yapılabiliyordu.

Yıllar ilerledikçe başlangıçta kullanılması tavsiye edilmeyen illegal opcodelar yani kullanımı onaylanmayan ve dökümante edilmemiş işlemci komutları (Commodore 64’ün Programcı’nın El Kitabı’nda bu özellikle belirtilmiştir), yıllar ilerledikçe hemen her Commodore 64 programcısının başı sıkıştığında baş vurduğu yöntemler haline geldi.

Commodore 64 her ne kadar donanımsal grafik kaydırma özelliğini desteklemese de, yine VIC-II çipini kandırmak suretiyle çok az işlemci gücü harcayarak grafikleri ekranda dilediğimiz gibi kaydırmak mümkün oldu. Bu da özellikle 80 sonu, 90 başlarında çok kaliteli ve çok renkli grafikleri olan platform oyunları, shoot’em up’lar olarak bize geri döndü.

Elbette ki Commodore 64’ün limitlerini zorlama konusunda en çok çaba sarfeden ve sonuç alanlar Commodore 64 scenerları oldu. Demoscene dünyasındaki rekabet her geçen gün birbirinden üstün demoların yayınlanmasına ve “Commodore 64’de yapılması olanaksız” denen birçok efektin yapılabilmesine neden oldu.

Örnek Demolar:

[youtube_sc url=”yFdjWSaDlIo” title=”Edge%20Of%20Disgrace%20-%20Part%201″ width=”384″]

[youtube_sc url=”0b4uGv-9xpw” title=”Edge%20Of%20Disgrace%20-%20Part%202″ width=”384″]

[youtube_sc url=”Z8trliwndrU” title=”Snapshot%20-%20Part%201″ width=”384″]

[youtube_sc url=”PEmf4FBakBE” title=”Snapshot%20-%20Part%202″ width=”384″]

Commodore 64 hakkında daha yazılacak çok şey var ancak bu seferlik

30. yaş günün kutlu olsun eski dostum ve çocukluk arkadaşım

demekle yetineceğim. Belki ilerleyen süreçte Commodore 64 üzerine daha detaylı blog yazıları da yayınlayabilirim, kim bilir.

BT Haber Röportajı

BT Haber’in scene geçmişimle ilgili yapmış olduğu röportaja aşağıdaki resimlere tıklayarak ulaşabilirsiniz.

BT Haber Röportajı - Sayfa 2

BT Haber Röportajı - Sayfa 2

BT Haber Röportajı - Sayfa 1

BT Haber Röportajı - Sayfa 1

Puls / Rrrola (256 byte demo)

Puls / Rrrola

İnsanlar ile hayvanları ayıran özellik zekadır. Peki biz zeki ve insansak, 256 byte’da bu efekti yapanlar hangi kategoriye giriyor? Demoya soldaki resime tıklayarak ulaşabilirsiniz.

Bu güne kadar çok fazla “bundan ötesi olmaz” dedirten 256 byte gördüm, geçirdim. Ama bu da artık boyutunun farkında değil. Kendini 1k falan zannediyor olsa gerek.

İşin komik tarafı tam da blog’uma ray tracing ile ilgili bir yazı hazırlıyordum ki bu demoyu gördüm. Büyük ihtimalle bundan sonraki yazımda anlatacağım bir tekniğin en küçük boyutta en muhteşem örneği budur.

Jak T Rip / DMagic Evlendi

Gerçek ismi “Tim Jakob Voos” olan Commodore 64 scenerı sevgili arkadaşım Jak T Rip, 26 Ağustos 2009 tarihinde evlenmiş. Aslında ben haberi geç duydum ancak duyar duymaz tebriklerimi ilettim kendisine. Kimdir bu “Jak T Rip” diyebilirsiniz, haklısınızdır da. Kendisi çok ünlü bir scener değildir. Ancak 2004 yılında düzenlediğimiz 7D4 Demo Party‘e 256 byte kategorisinde yurtdışından katılım göstermiş ve sonrasında ürün ile ilgili kendisine teşekkür etmediğim için beni azarlamış bir arkadaşımdır.

Jakob’a ve eşi Mi Chen’e ömür boyu mutluluklar diliyorum. Ha bir de minik scenerlar istiyorum kendilerinden.

Muon Baryon

Son yıllarda biraz öksüren, ancak toparlanmaya başlayan Türk Scene’i açısından çok önemli bir ürün Muon Baryon. Aslında bir Türk yapımı değil, uluslararası bir yapım. Ancak ürünün kodunun yarısı Decipher / YUP ^ Resident’a ait ki kendisi Finlandiya’da yaşayan bir Türk’tür.

Muon Baryon - Assembly 2009 4K Demo Yarışması Birincisi

Muon Baryon - Assembly 2009 4K Demo Yarışması Birincisi

Demoda başarılı Volume Ray Marching örnekleri görüyoruz, gerçekten izlenmeye değer. Yüzeylerde ve yer yer arkaplanda görülen noise efektleri ise Perlin Noise algoritması ile elde edilmiş.

4 KB’a sıkıştırılmış bu demoda kategorisine göre oldukça başarılı bir de müzik bulunuyor. Müziği oluşturan kodun Assembly’den yazılmış olması da ayrıca hoş bir detay.

Assembly 2009‘da 4K demo yarışmasında sağlam rakipler arasından birincilik elde eden bu demo hatrı sayılır bir başarı elde etmiştir.

Demonun yarışmada yayınlanan versiyonunda shader kullanımıyla ilgili performans sorunları yaşandığı halde birincilik almayı başarması da ayrıca dikkat çeken bir konudur. Decipher, partinin bitiminden birkaç gün sonra demonun çok daha optimize ve reflection gibi bazı ek efektler içeren bir son versiyonunu yayınlamayı ihmal etmedi.

Bu demonun beni en mutlu eden tarafı ise böyle bir başarının Türk Scene’inin toparlanmaya ihtiyacı olduğu ve tam toparlanmaya başladığı döneme denk gelmiş olmasıdır. Yani ben bunu bir “başlangıç” olarak kabul ediyorum. Tebrikler Decipher!