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 &H3D5, 1
Bu yazmaç aşağıdaki bitlerden oluşur [1]:
| 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.
FOR X = 1 TO Delay
OUT &H3D4, 8: OUT &H3D5, X
NEXT X
Sekizinci yazmacın olayı şu [1]:
| 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.
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| Start Address Low | |||||||
| 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, &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.
SCROLL.C: VGA Metin Kipinde Yumuşak Kaydırma Efekti
Ö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.
![]() |
| Saptırma Bobinleri (Kaynak: Wikipedia) |
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.
[1]: http://www.osdever.net/FreeVGA/vga/crtcreg.htm#09
[2]: http://wiki.osdev.org/Double_Buffering
[3]: https://en.wikipedia.org/wiki/VGA_text_mode#Text_buffer
[4]: https://stackoverflow.com/questions/47588486/cannot-write-to-screen-memory-in-c




Hiç yorum yok:
Yorum Gönder