Merhaba. Bu yazı, önceki yumuşak kaydırma (smooth scroll) serisinin beklenmedik bir devamı olacak. Serinin ikinci yazısında Start Address yazmacını tanıtıp, istenirse VGA sayfalarıyla birlikte bu yazmaç kullanılarak, bellek bloklarını kopyalamaya gerek kalmadan da kaydırma efektinin yapılabileceğinden bahsettim. Yazıyı hazırlarken izlediğim aşağıdaki video nedeniyle bu yaklaşımı da kısaca göstermeye karar verdim.
Öncelikle bu kanalın sahibi retroprogramming konusunda çok iyi işler yapıyor. İlgisi olan herkese kanalı takip etmesini tavsiye ederim. Benim yaklaşımımın bellek transferi yüzünden CPU'yu yoğun olarak kullandığını, buna karşın VGA belleğinin yalnızca ekranda görünen kadarını kullandığını ifade etmiştim. Yukarıdaki videoda bir ASCII art metin, bir sinüs fonksiyonuna bağlı hızda yukarı ve aşağı kaydırılıyor.
Ben bu yaklaşımı daha az görsel şekilde uyguladım. Birincisi bu kadar güzel görsel işler yapmayı beceremiyorum, ikincisi de elimde bu iş için hazır kod vardı, onu biraz değiştirdim. Zamanında bir diskmag için basit bir okuyucu yazmıştım. İki yana yaslı yazılar benim hep hoşuma gitmiştir. Blog'un sayfa düzeninden de belli oluyordur. Diskmag satır başına 80 karakteri geçmeyen metin dosyalarda yayınlanıyordu. Ben bunları tam ekranda okumak için iki yana yaslama algoritmasını uygulamıştım. Asıl okuyucuda fazladan başlık, altbilgi satırı ve bazı Escape karakter kodları vardı.
Kodu bu kez github gist'e koydum ve yazının en sonuna embedledim (bakalım, güzel görünmezse bir dahakine sadece bağlantı eklerim). justify_to_80() fonksiyonu, bir satırı seksen karaktere tamamlamaya yarıyor. Bunun için satırın karakter sayısı ve boşlukları sayılıyor ve ne kadar daha boşluk eklenmesi gerektiği bulunuyor. Eğer satırı seksen karaktere tamamlamak için gereken karakter sayısı, satırdaki boşluklardan fazlaysa (örn. satır 3-4 uzun kelimeden oluşuyorsa) o zaman each değişkeni her boşluğa eklenmesi gereken boşluk sayısını tutuyor (satır 33). Tam tersi, satır kısa kelimelerden oluşuyor ve seksene tamamlamak için az sayıda boşluk gerekiyorsa, o zaman extra değişkeni eklenmesi gereken boşluk sayısını tutuyor. Elbette her iki değişken de aynı anda sıfırdan farklı olabilir ama istatistiksel olarak genellikle each sıfır olup extra sıfırdan farklı oluyor.
each'i eklemek kolay çünkü zaten her boşluğa eklenecek (satır 42). extra ile eklenecek boşlukların homojen dağıtılması biraz daha karışık. Satırdaki her boşluk başına fazladan ne kadar boşluk eklenmesi gerektiği weight değişkeninde tutuluyor. Eksik karakter sayısı, boşlukların sayısına tam bölünemediği için weight bir double değişken. Değeri her boşluk karakterinde extra / spaces kadar arttırılıyor (satır 46). Bu değer her tam sayı geçişinde, örn 1.9'dan 2.1'e ulaşması durumunda bir boşluk karakteri ekleniyor. Örneğin aşağıdaki satırı ele alalım:
They were hidden from the road by a shallow ridge, but there was only sparse
Karakter sayısı 76, fakat strlen(), CR LF'i de saydığı için 77 döndürüyor. Zaten bu nedenle otuz ikinci satırda bu sayı 81'den çıkarılıyor. missing = 4, boşluk sayısı 14. Dolayısıyla each = 0, extra = 4. Otuz altıncı satırdaki döngüde karakterler tek tek işleniyor. Boşluk karakterine denk gelinirse (satır 40) döngüdeki diğer for, bu örnekte each = 0 olması nedeniyle etkisiz. sp1 = 0 olduğundan, weight değişkeni ilk adımda 0. İkinci boşluk karakterinde weight = 1 * (4 + 1) / 14 ≈ 0.36. sp1'den ötürü bu sayı doğrusal artacağından her boşlukta aşağıdaki değerler elde ediliyor:
Buradaki tam sayı geçişleri dört (0.71 -> 1.07), yedi (1.79 -> 2.14), on (2.86 -> 3.21) ve on üçüncü boşluklarda (3.93 -> 4.29) gerçekleşiyor.
Aslında gerek açıklamasına gerekse satır sayısına bakılırsa bu fonksiyon smooth scroll algoritmasından daha karmaşık.
vga_set_base_addr() fonksiyonu lineP değerini seksenle çarpıp (satır 62), bunun yüksek byte'ını Start Address High ve düşük byte'ını Start Address Low yazmaçlarına yazıyor. lineP ekranın en üstünde hangi satırın görüneceğini belirleyen bir sayaç.
waitbl()'i önceki yazıda detaylı açıklamıştım.
vgaprint(), verilen string'i video belleğine kopyalıyor. Burada justify_to_80()'e giren satırlar '\n' olmadan, girmeyenler (paragraf sonları) '\n' ile geldiklerinden, seksen altıncı satırdaki gibi bir workaround üretmek gerekti. for döngüsünde (satır 89) seksen karakter basılıyor. Satır, seksen karakterden azsa bile (paragraf sonundaki satır), boşluk karakteriyle ekranda önceden bulunan karakterlerin üzerine yazılıyor. Kodun ondördüncü satırında linecount'u yerel değişken yapmayı TODO'ya eklemiştim. Aslında bu değişkenin değeri main()'de arttırılırsa kolaylıkla yerel yapılabilir. Ancak şimdi main()'e de bakınca, bu kısımların biraz aceleye geldiği anlaşılacaktır.
Öncelikle main(), görece büyük. İlk while bloğu (satır 121) ve kaydırma işini yapan ikinci while bloğu (satır 137) iki ayrı fonksiyonda ele alınabilirdi. İkinci olarak girdi sanitizasyonu belki biraz daha çeşitlendirilebilirdi, ama dismag'deki hiçbir dosyanın 80 karakterden uzun olmadığını biliyorum. Asıl önemlisi dosyanın 409 satırdan uzun olmaması gerekliliği. Renkli VGA text mod belleği B800:0000 ile B800:7FFF arasındaki 32 KB. Buraya 80 karakterden en fazla 409 satır sığabilir. Dosya büyüklüğüne bu durumda güvenilemez, çünkü biz dosyaya boşluk karakteri ekleyeceğiz.
İlk while bloğundan önce dosyadan bir satır okunuyor (satır 119) ve bunun paragraf sonu olmadığı varsayılıyor. While'ın içinde bir sonraki satır okunuyor ve bunun boş satır veya dosya sonu olup olmadığı kontrol ediliyor. Buradaki amaç, paragraf sonundaki satırı olduğu gibi bırakmak. Aksi halde satır 80 karaktere tamamlanıp bir sonraki satır işleniyor. Kısacası her adımda bir sonraki satır da kontrol ediliyor.
İkinci while bloğundan ESC tuşuyla çıkılıyor. Switch / case yapısında klavye girdisi kontrol ediliyor. Yukarı ok basılmışsa, ekranın en üst satırında olması gereken satır 1 azaltılıyor (satır 142), benzer şekilde aşağı ok basılmışsa, en üst satır bir arttırılıyor. Elbette bunlar yapılırken dosyanın satır sayısı da kontrol ediliyor ki, metnin olduğu alan hep kullanıcının önünde kalsın, yukarıdan veya aşağıdan ucu kaçıp gitmesin. Burada kaydırma efektleri bir önceki kod ile tamamen aynı, hatta bellek kopyalanmadığı için daha bile basit. Farklı olarak, burada ok tuşlarıyla yapılan kaydırma SCROLLSTEPFINE, Pg Up ve Pg Dn tuşlarıyla yapılan kaydırma SCROLLSTEPCOARSE parametresine bağlı. Bu değerleri küçültmek kaydırmayı yavaşlatıyor, büyütmek efektin etkisini azaltarak kaydırmayı hızlandırıyor.
Pg Up ve Pg Dn tuşlarıyla yapılan kaydırmanın mantığı ok tuşlarıyla yapılan ile tamamen aynı. Fakat burada kaydırma etkisi katması için satır sayısı 1 ile 24 arasında tek tek arttırılıyor veya azaltılıyor.
Efektin videosunu da aşağıya ekledim ancak videonun etkisinden dolayı Pg Up ve Pg Dn ile kaydırırken yumuşak geçişler klipte düzgün görünmüyor.
Merhaba. Bu yazıda VGA konusuna devam edeceğim, ve göstermek istediğim, 1994 yılından bir örneğim var. Bu koda biraz geniş yer ayırmak için, önceki yazıya sıkıştırmak istemedim.
Söz konusu kodu github repo'ma yükledim. Bunu 90ların sonunda indirmiş olmalıyım ki zaten açıklama satırına göre 1994'te yazılmış bir Basic kodu (William Yu bu yazıyı okuyorsan bana ulaş). Bu kodda dikkat çekmek istediğim iki nokta var. İlki, 12. satırda dokuzuncu CRT denetleyici yazmacına erişen kod parçası:
OUT &H3D4, 9 OUT &H3D5, 1
Bu yazmaç aşağıdaki bitlerden oluşur [1]:
Maximum Scan Line Register (Index 09h)
7
6
5
4
3
2
1
0
SD
LC9
SVB9
Maximum Scan Line
ve kodun değiştirdiği "Maximum Scan Line" alanı, grafik modda pikselleri dikey eksende bu değerin bir fazlası kadar genişletir. Yani yazılan 1 değeriyle, her piksel hemen altındaki diğer piksel de set edilmiş gibi iki kat büyük görünür. Bu alana 9 yazılsaydı pikseller 10 kat genişlikte olurdu. Ekran genişliği sabit olduğundan -bizim örneğimizde 640x480 piksel, mode 12h (satır 11)- pikselleri iki kat geniş yapmak demek, görünür ekranı küçültmek anlamına gelir. Yani bu örnekte ekranda 640x240 piksel çizilebilir. 10 kat genişletmiş olsaydık 640x48 piksel çözünürlük elde edecektik. Elbette VGA bellek büyüklüğü değişmediği için 240'dan büyük pikseller ekranda görünmez. Ancak bu alana tekrar 0 yazıldığında görünürler. Standart metin modunda bu alanda 15 değeri bulunur. Önceki yazıda da belirttiğim gibi bu bir karakterin piksel cinsinden yüksekliğidir. Bu alana daha büyük değerler yazılırsa satırların arası açılır, çok küçük değerler yazılırsa ekran okunmaz duruma gelir.
Bu koddaki hesaplar 640x240'a göre yapıldığından (örn. satır 30 ve 35) bu adım gerekli. Pikseller iki katı büyüdüğü için ekrandaki yazılar da iki kat büyüklükteler. 13 ve 20. satırlar arasındaki döngüde ekrana yıldızlar basılıyor, 22 ve 27. satırlar arasında gezegen çiziliyor. 35. satıra kadar olan bölümde bir üçgen (uzay aracı) çizdirilip, 43. satıra kadar olan bölümde bu üçgen ekranda blok olarak (GET / PUT) hareket ettiriliyor. Geri kalan grafik efektleri çok önemli da değil. Önemli olan kısım EarthQuake altprogramı (87 ile 94. satırlar). Burada sekizinci yazmaca sırayla değerler yazılıyor.
Delay = 5500 ' Increase this or decrease for earthquake delay
FOR X = 1 TO Delay OUT &H3D4, 8: OUT &H3D5, X NEXT X
Sekizinci yazmacın olayı şu [1]:
Preset Row Scan Register (Index 08h)
7
6
5
4
3
2
1
0
Byte Panning
Preset Row Scan
Benim gözlemime göre, burada "Preset Row Scan" alanının grafik modda hiç bir etkisi yok, veya DosBOX düzgün emule edemiyor. Bu alan, metin ekranda görüntünün orjinini piksel hassasiyetiyle kaydırmayı sağlıyor ve metin ekran için sorunsuz çalışıyor. Başka bir deyişle CRT denetleyici, görüntüyü bu alandaki değer kadar piksel yukarı kaydırıyor. Bu benim de yumuşak kaydırma için kullandığım yazmaç. "Byte Panning" alanıysa, görüntüyü bir karakter sola kaydırıyor. Dolayısıyla ekran, bu alandaki değere bağlı olarak 1, 2 veya 3 karakter genişliğinde kaydırılabiliyor. Mode 12h için bu, karakter başına 8 piksel (640 piksel / 80 karakter).
Bu yazmaç dışında dökümanda sözü edilen, ve benim de değinmek istediğim bir yazmaç çifti daha var. Bunlar Start Address Register Low ve High.
Start Address Low Register (Index 0Dh)
7
6
5
4
3
2
1
0
Start Address Low
Start Address High Register (Index 0Ch)
7
6
5
4
3
2
1
0
Start Address High
Bunların herhangi bir bit alanı yok. Normalde ekranın orjini olan sol üst köşenin VGA ekran modlarında (metin / grafik fark etmeksizin) adresi 0'dır. Bazen ekranın tam ortasının orjin alınması daha avantajlı olabilir. Mode 13h'da (320 x 200) ekranın tam ortasını orjin yapmak demek aslında görüntüyü 160 x 100 pikselinden çizdirmeye başlamak demektir. Bu pikselin doğrusal adresi 320 * 100 + 160 = 32160 = 7DA0h bulunur. Buradan;
OUT &H3D4, &HD: OUT &H3D5, &HA0 OUT &H3D4, &HC: OUT &H3D5, &H7D
kod parçasıyla orjin ortaya çekilir. Bundan sonra ekran belleğinin sıfırıncı offsetine yazılan piksel, ekranın tam ortasında görüntülenir. Qbasic'teki WINDOW komutunun da yaptığı kabaca budur. Elbette QBasic verilen negatif koordinatları kendi dönüştürür, daha düşük seviye dillerde bu işlem programcıya bırakılmıştır.
Bu yazmaca değerler birer birer arttırılarak yazılırsa ekranda sola kaydırma efekti elde edilir. Elbette kaydırılan aslında karakterler değil orjindir. Benzer şekilde, değerler yazmaca ekran genişliği kadar arttırılarak (metin ekranda satır başına düşen karakter sayısı, grafik ekranda satır başına düşen piksel kadar) yazılırsa yukarı kaydırma efekti elde edilir. Burada, karakterler bellekte bir yerden bir yere taşınmaz, işlemci sadece portlara değerleri yazmakla meşguldur. VGA'da tüm ekranı kaydırmanın en optimal yoludur. Ancak aşağı kaydırmada, en alttaki satırın en üste taşınması veya yukarı kaydırmada en üstteki satırın en alta taşınması gibi durumlarda bellek bloklarının taşınması kaçınılmazdır.
VGA Metin Modun Yapısı
VGA metin modu oldukça basittir. Ben 80x25 metin modundan bahsedeceğim. 40x25 metin modunun yapısı çok benzer olsa da bazı adres değerlerinin bu mod için tekrar hesaplanması gerekir. Monokrom mod video belleği 0xB000 segmentinden 0xB7FF segmentine ve renkli mod 0xB800 segmentinden 0xBFFF segmentine kadar olmak üzere 32 KB'tır. Her karakter için 1 word ayrılmıştır. Bu word'un düşük anlamlı byte'ı karakterin ASCII kodu, yüksek anlamlı byte'ı karakterin ve arkaplanının renk kodlarından oluşur [3]. Renk byte'ının düşük anlamlı 4 biti karakterin rengidir. Standart VGA renkleri 0: siyah, 1: mavi, 2: yeşil, 3: cyan, 4: kırmızı, 5: mor, 6: kahverengi / sarı, 7: gri olmak üzere 8 tanedir. Bunlara 8 eklenmesiyle aynı renklerin açıkları (parlakları - high intensity) elde edilir. 4, 5 ve 6. bitler arkaplan rengidir. En yüksek anlamlı bit (yedinci), karakterin ekranda yanıp sönmesini sağlar. Aşağıda bunlara ilişkin bir örnek var. 'u' ve 'g' karakterleri normalde yanıp sönüyorlar.
VGA metin modu 8 sayfadan oluşur. Her ekranın 80 x 25 = 2000 karakterden oluştuğunu ve her karakter için bir word ayrıldığını söylemiştim. Bu durumda görünen ekran 4000 byte (0x0FA0) uzunluktadır. Video belleği 32 KB olduğundan, 8 sayfaya bölünebilir. İlk sayfa 0xB800:0 adresinden başlar, sonraki 0xB800:0x0FA0, sonraki 0xB800:0x1F40 vb. Her bir ekran modunun kaç sayfadan oluştuğu şu tabloda görülebilir ve Int 10h/AH=05h ile sayfalar arasında geçiş yapılabilir.
Bu kadar ön bilgiden sonra artık kendi yumuşak kaydırma koduna gelebilirim.
Öncelikle bu kodda kaydırma için neden Start Address yazmacını kullanmadım? En kısa yanıt: Kullanabilirdim. Sıfırıncı sayfayı birinci ve ikinciye kopyalayıp, görünen sayfayı da birinci sayfa olarak ayarlarsam, aynı sayfanın bir üstte bir de altta birer kopyasını elde ederim. Sonrasında "Start Address Register"ini arttırarak veya azaltarak kaydırma efektini elde edebilirdim. Bu şimdilik başka bir yazıya kalsın.
Benim yazdığım kod, diğer sayfalarla en az şekilde (yalnızca 1 satır) etkileşime giriyor, yukarıdaki metoda göre farkı bu. Kodu github hesabıma yükledim.
Kodu Turbo C v3.0'da yazdım. Bu haliyle sorunsuz şekilde derlenip çalıştırılabiliyor. Önceki yazıda değindim, herşey DosBOX'ta yapıldı. Turbo C'de true ve false, built-in veri tipi olarak bulunmadığından, bunları yedinci ve sekizinci satırlardaki gibi tanımlıyorum. Onaltıncı ve onyedinci satırlarda iki tane gösterici tanımladım. İlki VGA metin ekran göstericisi [4]. İkincisi, ekran belleğinin lokal kopyasını tutan bir gösterici (pointer) ama adı her ne kadar DoubleBuff olsa da tam anlamıyla double buffering [2] yapmıyor. Bu konuya ileriki bir yazıda değineceğim. Bu bir short pointer ve 23. satırda 80x25 word uzunlukta bellek ayrılıyor. Neden word? Bunun iki nedeni var. Birincisi veriyi byte byte işlemektense word word işlemek daha hızlı. Karakterler renk koduyla birlikte tek seferde işlenebiliyor. İkinci nedense, kodun okunabilirliği.
Kodda yukarı ve aşağı ok tuşlarının kodu alınıyor ve bunun sonucuna göre fp fonksiyon göstericisine (function pointer) ScrollUp() veya ScrollDown() fonksiyonları atanıyor. Her iki fonksiyon da ekranı yalnız bir satır kaydırıyor. Bunların for döngüsünde 25 kere çağırılmasıyla tam ekran kaydırma elde ediliyor.
ScrollUp
Row scan alanı arttırılarak ekran yukarı kaydırılırken, birinci sayfanın normalde görünmeyen ilk satırı kısmen görünür hale gelir. Bundan ötürü kodun 60. satırında ekranın en üst satırını sonraki sayfanın en üst satırına kopyalıyorum. Ardından, ekranın ilk satırı en altta ve sonraki satırlar ara belleğin ilk satırından başlamak üzere (yani N+1. satır ara belleğin N. satırında) DoubleBuff dizisine kopyalıyorum.
Sonrasında row scan alanının ilk değerini alıp, bu değeri 0'dan 15'e kadar arttırıyorum (satır 75). Böylelikle ekran piksel piksel yukarı kayıyor görünüyor. Bu sırada, altmışıncı satırda en üstten kopyalanan, ve başta görünmeyen satır görünür olmaya başlıyor. Sekseninci satırda inline assembly ile DoubleBuff'ta dizdiğim ekranı görünür ekrana kopyalıyorum. Burada assembly'i hız için kullandım çünkü aynı işi yapan C kodu daha yavaş çalışıp ekranda titremeye (flicker) neden oluyordu.
En sonda row scan alanına tekrar eski değerini yazıyorum. Burada aslında eski değer değil doğrudan sıfır yazılması daha doğru olurdu.
ScrollDown
ScrollDown()'da ScrollUp()'takinden farklı bir yaklaşım kullandım. Row scan ekranı yukarı kaydırdığından, aşağı kaydırma efekti için öncelikle ekrandan kaybolacak olan en alt satırı lokal diziye alıp (satır 108), ekran belleğini kaydırdıktan sonra en üst satıra yazıp, row scan'e en büyük değeri yazıyorum (satır 113).
Ekran belleğini, hız için ScrollUp()'taki gibi assembly koduyla kaydırdım. SI'de 4000 ve DI'de 4160 değeri var. Yani SI birinci sayfanın ilk karakterini, DI birinci sayfada ikinci satırın ilk karakterini gösteriyor. CX'te sayaç olarak 2000 değeri varken (136. satırda 4000 shr ile ikiye bölünüyor) kopyalayınca, işlem birinci sayfanın ilk karakterinden başladığı için sonda bir karakter kopyalanmıyordu. SI ve DI'yi bir word azaltmak için 4 byte kod gerekiyor (satır 137 .. 140, dec komutları), ben onun yerine CX'i arttırıp (satır 141) bir karakter fazla kopyalıyorum, ama 4 yerine 1 byte'lık komutla hallediyorum. 132. satırdaki değeri başta azaltabilirdim ama onu azalttığımda (yanılmıyorsam) sayacı azalttığım için CX'i sonradan arttırmam gerekecekti. Son olarak direction flag'i set edip geri geri (pointer değeri azalacak şekilde) kopyalıyorum. Ekranı aşağı yönde kaydırdığım için, göstericiyi arttırarak kopyalarsam daha sonradan ihtiyacım olacak verinin üzerine yazmış olurum. Kopyalama (rep movsw, satır 148) bittikten sonra direction flag'i resetleyip eski durumuna getiriyorum.
Bundan hemen sonra en üst satır boş kaldığı için, fonksiyonun başında TempLine'a kopyaladığım satırı geri yazıyorum (156. satır. bunun neden assembly ile yapmamışım acaba?) ama row scan alanına en büyük değeri verdiğim için bu satırın yalnız bir pikseli görünüyor. Yüzaltmışıncı satırda row scan alanını yavaş yavaş azaltarak kaydırma işlemini tamamlıyorum.
waitlinefull ve waitlinehalf
Tüplü monitörler, görüntüyü ekranı tarayarak oluştururlar. Elektron tabancası elektronları normalde ekranın tam ortasına gönderir. Ekranda birşey göstermek için bu elektron demeti dikey ve yatay saptırma bobinleri tarafından saptırılır (renkli ekranlar için). Tarama ekranın en üst solundan başlar, üst sağ köşesine kadar gider. Bu sırada dikey bobinde gerilim sabit olup, yatay bobine testere dişi dalga uygulanır. Bu şekilde ekranın ilk piksel satırı çizdirilmiş olur. Sonra dikey bobindeki gerilim arttırılır ve işlem her satır için tekrarlanır. Ta ki tarama ekranın sağ alt köşesine ulaşıncaya kadar. Elbette bu sırada elektron tabancaları pikselin olduğu yerlere elektron göndererek anlamlı bir görüntü oluşturur. Eğer sürekli açık kalırlarsa tek renkli bir ekran elde edilir.
Bu tarama işlemine terminolojide "vertical retrace" (VR) denir ve bu işlem sürerken video belleği değiştirilirse görüntü titriyor (flicker) gibi görünür. Bu işlemin durumunu kontrol etmek için imdada VGA'nın 0x3DA kontrol yazmacı yetişir. Bu yazmacın üçüncü biti VR sırasında set edilir. Programcı ekrana birşey yazmadan bu biti kontrol edebilir (ve etmelidir). waitlinefull() ve waitlinehalf() fonksiyonlarında bu kontrol yapılıyor.
waitlinefull()'da ilk satırda (satır 173), eğer VR yoksa döngüde bekliyor. Çünkü VR o an aktif olmasa bile, video belleğindeki işlemler bitmeden başlayabilir ve yine görüntü titreşebilir. Eğer o anda VR zaten varsa bu satırın etkisi yoktur. Program sonraki satırdan devam eder. Sonraki satırda VR işlemi tamamlanana kadar döngüde kalınır.
waitlinehalf()'te yalnızca o an VR işlemi varsa döngüde kalınır, bir sonraki VR beklenmez. waitlinefull()'de bir sonraki VR başlayana dek beklemek, hızlı bilgisayarlarda çok sayıda CPU cycle beklemeye yol açar. Bu nedenle waitlinehalf()'te bu adım atılmıştır. waitlinehalf(), çok daha kısa süre bekler ama waitlinefull()'e kıyasla bazen titremeyi engelleyemeyebilir.
Ben iki fonksiyonu da koda koydum ve efekti tamamladıktan sonra ikisini de düzgün bir efekt elde edene kadar çeşitli satırlarda denedim. waitlinehalf() çok kısa sürede tamamlandığı için tüm beklemeleri waitlinefull() ile yaptım. İlk bölümün başında da belirttiğim gibi, DosBOX bir VGA emulasyonu olduğundan bu beklemelerin gerçek donanımda tekrar ayarlanması gerekecektir. Aslında waitlinefull()'u ben yalnızca kodda makinadan makinaya değişmeyen stabil bir bekleme süresi verdiği için tercih ettim.
Yazının başında da belirttiğim gibi ben burada VGA konusunun yalnız üzerini kazıdım. Smooth scroll efekti istediğim gibi oldu ama bu VGA ile yapılacakların küçük bir örneğiydi. Bu yazmaçlarla oynayarak çok çeşitli efektler oluşturmak mümkün. Sonraki yazılarda zaman bulabilirsem bir kaç efekte daha değinmek istiyorum. Son olarak aşağıda efektin videosu var.
Merhaba. Bu yazıya, 80'lerin Monospace fontlarla yazılmış bilgisayar kitaplarından çıkmış gibi duran bir başlık seçtim, çünkü bu kez oldukça düşük seviye bir yaklaşımla 80'lerin teknolojisinden bahsedeceğim.
80'lerin Teknolojisi (temsili) [1]
Daha anlaşılabilir bir şekilde, VGA Text modunda smooth scroll nasıl yapılır, kesme (interrupt) kullanmadan doğrudan VGA belleği ve yazmaçları (register) nasıl kullanılır ona değindim. Ama önce VGA'ya kısa giriş yaptım ve bazı yazmaçları anlattım, arada biraz da geyik yaptım. Haliyle yazı düşündüğümden uzun oldu, ben de ikiye böldüm. Smooth scroll'un özü sonraki yazıya kaldı, çünkü kaydırmayla birlikte double buffering'den bahsetmek istiyorum. Bu da uzunca bir konu.
Uyarı: VGA yazmaçlarına, kullanılan donanıma uygun olmayan değerler yazmak donanıma kalıcı zarar verebilir. Burada verilen bilgiler gerçek bir tüplü monitörde denenmemiştir ve bunları kullanmanın riski yalnızca size aittir. Herhangi bir zarar ihtimali varsa, metnin yazarı sizi uyarmakta ve bundan sonrası için sorumluluk kabul etmemektedir.
Yukarıdaki uyarıyı biraz açıklayayım. Gerçekten de VGA kartını, tüplü (CRT) monitörün desteklemediği bir frekansta çalıştırmak monitöre zarar verebilir [2]. Böyle birşeyle karşılaşmadım ama var olduğunu biliyorum. Öte yandan son CRT monitörümü yaklaşık on yıl önce çöpe attım. Ekran kartım gerçek bir VGA kart değil, yalnızca VGA uyumlu. Dolayısıyla eriştiğim yazmaçlar bile gerçek değil. Kodları DOSBox'ta geliştirip çalıştırdığım ve optimize ettiğim için, gerçek CRT'de muhtemelen timingler kayacak ve waitretrace'leri tekrar ayarlamak gerekecek (waitretrace sonraki yazıda). Bu kodlar 80386'da nasıl çalışırdı bilmiyorum.
Giriş
VGA kartına genellikle int 10h BIOS arayüzünü kullanarak erişiyoruz. Ekran modunu seçmekten, imleci hareket ettirmeye veya büyüklüğünü ayarlamaya kadar herşey bu kesmeyle yapılabiliyor. Kesmenin yaptığı da zaten VGA yazmaçlarına "doğru biçimde" erişmek. VGA BIOS, gerekirse int 10h rutinlerini karta uygun şekilde değiştirebiliyor.
Int 10h'un ciddi bir yavaşlık getirdiğini düşünmüyorum (putpixel vb. işlemler hariç), hatta kesmeyi tercih etmenin bir avantajı da kodda karmaşıklıktan kaçınmak. VGA'nın çok sayıda yazmacı var [3]*. Bunların bazısının işlevini anlamak için temel CRT bilgisi gerekiyor. Diğer yandan bu yazı için DOSBox kullandığımı belirttim. DOSBox bir çok VGA registerini doğru emüle edebilse de bazı (standart dışı) efektleri düzgün gösteremiyor. Dolayısıyla bazı DOS oyunları düzgün çalışmıyor (konfigürasyon hatalarını bir kenara). Yine de DOSBox'un hakkını teslim etmek gerek, Vmware veya VirtualBox'la karşılaştırıldığında daha uyumlu (evet elmayla armudu karşılaştırdım: biri DOS'a özel, diğerleri genel sanallaştırma).
* Adı geçen kaynakta 300+ yazmaçtan bahsediliyor ama 100 tane bile dökümante edilmemiş. Muhtemelen, farklı üreticilerin kendi ekledikleri standart dışı yazmaçlar sayılıyor. Standart yazmaçlar yaklaşık 60 tane, ama bu da az değil.
VGA Yazmaçları ve Karta Erişmek
Yukarıda da yazdığım gibi bir çok VGA yazmacı var. Şu linkte yazmaçlar altı grupta toplanıyor. Bu yazıda ben çoğunlukla CRT denetleyici (CRT controller - CRTC) yazmaçlarına erişeceğim. Gruplar kabaca erişimde kullanılan port numaralarına göre oluşturuluyor.
Bu yazmaçlara, kabaca 6 çift donanım portu üzerinden erişiliyor. Yani altmış yazmacın her birine bir port atanmamış. [3]'te portların bir listesi var. Genel olarak erişim, 0x3DX'e yazmaç index numarasını girdikten sonra, 0x3DX+1'den yazmaçtaki değeri okumak veya değere yazmak şeklinde. 0x3D0'ın bir istisnası var ama ona değinmeyeceğim. Yazının ileriki bölümlerinde bu yazmaçlarla ilgili örnek göstereceğim.
Burada tüm yazmaçlara yer verip yazıyı bir referans kitabı haline getirmek istemedim. O yüzden sadece ilgilendiğim kısma odaklanacağım. Örneğin: CRTC'ye, 0x3D4 ve 0x3D5 portları üzerinden ulaşılıyor. 0x3D4 yazmaç seçme, 0x3D5 ise veri okuma ve yazma portu [5].
Cursor Start Register (Index 0Ah)
7
6
5
4
3
2
1
0
CD
Cursor Scan Line Start
Şimdi [6]'daki imleci devre dışı bırakma kodunu ele alalım:
Önce 0x3D4 portuna 0xA yazmacına erişeceğimizi bildiriyoruz. Ardından bu yazmaca 0x3D5 portu aracılığıyla 0x20 değerini yazıyoruz. Bu değer 0xA'nın 5. bitini yani Cursor Disable bitini set ediyor [5]. Oldukça basit.
Cursor Scan Line Start, imlecin hangi pixelden başlayacağını gösteriyor. Standart 80x25 karakter ekran modunda (Mod 3), her karakter ve imlecin kendisi 8x16 pixelden oluşan bir görsel. Bu görseller değiştirilerek DOS'ta font yüklenebilir. Aşağıda, DOS için bir font düzenleme programından aldığım ekran görüntüsünde, örnek bir karakter yakından görülüyor. Font tablosu VGA BIOS'ta bulunuyor (meraklısına Int 10h / 1130h) ve özel fontlar bu alana geçici olarak yazılıyor (bilgisayar yeniden başlatılana kadar). Fontlar başlı başına bir yazı konusu, daha fazla detaylandırmayacağım.
Konuya geri dönersem, edit ortamında imleç 14. pixel satırından başlayıp 15.'de biter, ama edit'te Insert'e basıldığında 0. pixel satırından başlar. İşte bu etkiyi veren birinci öğe Cursor Start Register'in düşük anlamlı beş biti, ve ikinci öğe 0xB numaralı Cursor End Register'dir, daha doğrusu bunun düşük anlamlı beş biti:
Cursor End Register (Index 0Bh)
7
6
5
4
3
2
1
0
Cursor Skew
Cursor Scan Line End
Bu yazmaç imlecin alt pixel satırını tutar. Peki eğer bir karakter 16 pixel uzunluğundaysa neden 5 bit? VGA, metin modunda aslında 32 pixele kadar olan karakterleri destekler [6]. Örn. VGA font tablosu 8 KB uzunluktadır: Karakterlerin sayısı (256) * yüksekliği (32 px) * genişliği (8 px) / bit per byte (8). Bu yüzden imleç için de yazmaçta 5 bit yer ayrılmıştır, ancak dördüncü bitin hiçbir metin modunda anlamı yoktur. Cursor Skew bitleri EGA uyumluluğu için bırakılmıştır, yine VGA'da bir anlamı yoktur.
Anlaşılması kolay başka bir yazmaç çifti de Cursor Location High (0xE) ve Cursor Location Low (0xF) yazmaçlarıdır. İmlecin ekrandaki doğrusal konum bilgisini tutarlar. Bu değer, ekranın karakter çözünürlüğüne (bizim örneğimizde 80) kalanlı bölündüğünde bölüm, imlecin y-eksenindeki; ve kalan, imlecin x-eksenindeki yerini verir. Tersi ifadeyle D = Y * 80 + X. Bu yazmaçlar byte uzunlukta olduklarından D'nin yüksek anlamlı byte'ı 0xE'ye, düşük anlamlı byte'ı 0xF'e yazılır.
Cursor Location High Register (Index 0Eh)
7
6
5
4
3
2
1
0
Cursor Location High
Cursor Location Low Register (Index 0Fh)
7
6
5
4
3
2
1
0
Cursor Location Low
80'lere Geri Dönüş: QBasic
Şimdi bu iki çift yazmaçla küçük bir demo yapacağım ve bunun için ilginç bir şekilde QBasic kullanacağım. Aslında ben de pek çok kişi gibi programlamayı Basic'le öğrendim. Biraz C64 Basic, sonrasında GW-Basic (o zamanın TRT'si sağolsun, TRT4 Açıköğretim Bilgisayar derslerı) ve son olarak QBasic. Ve iddia ediyorum ki 80'lerde doğup 90'larda bilgisayarı olan herkes aşağıdaki IDE'yi en az bir kere görmüştür. Ben 90'larda .bat dosyaların yetersiz kaldığı durumlarda betiklerimi QB ile yazardım. Sonrasında QBasic'in hızı bir çok şey için yetersiz gelince, bu beni C ve Assembly öğrenmeye itmişti. -Arada kısa bir Pascal dönemim de oldu.- Bu arada QBasic bir yorumlayıcıydı (interpreter) ve .exe dosya üretememesi benim için başka bir eksiklikti. C ile yakın zamanlarda Quick Basic v4.5 ile tanışsam da, C'nin açtığı ufuk bambaşkaydı. Ayrıca o zamanlarda Quick Basic 4.5 IDE'yi bulmak da -en azından benim için- oldukça zordu.
Yalnız ne QBasic ne Quick Basic (kısaca QB) yetersiz programlar değildi. C ile yapabildiğim herşeyi QB ile de yapabiliyordum. Bulabildiğim eski kodlarıma baktığımda, fare için QB - Int33h arayüzü ve disk işlemleri için Int13h arayüzü yazmışım. Yine başkalarının yazdığı inanılmaz kodlar var. Ama yavaşlığı bir yana, QB'deki programlama mantığı da biraz "başkaydı", ve programlamanın gitmekte olduğu yerden farklıydı. Visual Basic'le de şansımı denedim ama o ara Windows'un genel olarak bana göre olmadığını fark ettim. 2000'lerin başında görsel programlamada Win32 Assembly ile uğraştım, ama Win32API bana ağır gelmişti.
Ve neredeyse on yıldır blog yazıp, bu yazılarda bir sürü dilde örnek program yazmama rağmen, tek bir QB örneği vermediğimi fark ettim. Oysa bu tür basit kod parçaları için QB bence daha kolay, çünkü ne Assembly kadar çok satıra gerek var, ne de C'deki gibi include ekle, type cast'lere dikkat et, buffer'ı kontrol et gibi konular yok. Kısacası bu kadar
retrospective yeter. Bu yazıdaki örneği QB ile veriyorum:
DECLARE SUB ENABLECURSOR (CURSTART%, CUREND%) DECLARE SUB DISABLECURSOR () DECLARE SUB MOVECURSOR (CURSORX%, CURSORY%)
FOR X% = 0 TO 15 FOR Y% = X% TO 15 CALL ENABLECURSOR(X%, Y%) SLEEP 1 CALL DISABLECURSOR SLEEP 1 NEXT Y% NEXT X%
CALL ENABLECURSOR(0, 15)
FOR Y% = 0 TO 10 FOR X% = 0 TO 10 CALL MOVECURSOR(X%, Y%) SLEEP 1 NEXT X% NEXT Y%
SUB DISABLECURSOR OUT &H3D4, &HA OUT &H3D5, &H20 END SUB
SUB ENABLECURSOR (CURSTART%, CUREND%) OUT &H3D4, &HA CS1% = INP(&H3D5) OUT &H3D5, (CS1% AND &HC0) OR CURSTART%
OUT &H3D4, &HB CE1% = INP(&H3D5) OUT &H3D5, (CE1% AND &HE0) OR CUREND% END SUB
OUT &H3D4, &HF OUT &H3D5, POSITION% AND 255 OUT &H3D4, &HE OUT &H3D5, POSITION% \ 256 END SUB
Kod biraz uzun ama genel olarak [4]'teki kodları içeriyor. İlk bölümde imlecin alabileceği kombinasyonları bir for döngüsünde oluşturdum. Parametreler ENABLECURSOR alt programında ilgili yazmaçlara gönderiliyor. Bu arada döngü içerisinde bir saniyelik bekleme (SLEEP) CTRL tuşu basılı tutularak geçilebilir.
İlk for döngüsünden sonra rahat görülebilmesi için imleci büyütüp, MOVECURSOR alt programıyla ekranın 10 x 10'luk bölümünde hareket ettirdim. MOVECURSOR'de ekranın 80 karakter genişlikte olduğunu varsayıp, (X, Y) koordinatlarından imlecin doğrusal konumunu hesaplattım.
Bir sonraki yazıda başta yumuşak kaydırma için gereken yazmaçlar olmak üzere diğer VGA yazmaçlarına değineceğim ve QB ile başka örnekler vereceğim. Ancak kaydırma işlemi yüksek hız gerektirdiği için onu Assembly + C ile yazdım, ve yazının başlarında değineceğimi söylediğim waitretrace fonksiyonunu kullandım.