DOS etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
DOS etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

25 Ekim 2026 Pazar

VGA Paleti #2


Merhaba. Dizinin bu yazısını tamamen mod 13h'teki palet kullanımına ayırdım. Bu mod bence her konuda ayrı olarak ele alınmayı hak ediyor. Veya ben mod 13h'e nostaljik ve yanlı bir açıdan bakıyorum. Bu yazıdaki konuları, önceki yazıda anlattığım bmp göstericisiyle bazı dosyaları görüntüleyerek açıklayacağım. Buna ek olarak yazıda paletle ilgili bir kaç detay daha göstereceğim.


Öncelikle, ilk yazıda diğer ekran modlarında paletle ilgili açıkladığım hemen her şey mod 13h'te de geçerli. Özetlersem: VGA DAC, mod 12h ve metin modunda 262 144 renkten yalnızca 16'sını, mod 13h'te 256'sını aynı anda gösterebiliyor. Paletteki her rengin bir indisi ve kanal başına 6-bit'ten 18-bit'lik bir RGB değeri var. Palet indisi 3C8h portuna yazıldıktan sonra, her renk kanalına ait değer 3C9h portundan byte-uzunluklu olarak ardarda yazılıyor. Aşağıda, BMP göstericinin paleti ayarlayan döngüsü görülüyor.

  for(i = 0; i < ColorCount; i++)    {
    outportb(0x3C8, i);    // Palette index
    outportb(0x3C9, VGApalette.color[i].B >> 2);  // Blue
    outportb(0x3C9, VGApalette.color[i].G >> 2);  // Green
    outportb(0x3C9, VGApalette.color[i].R >> 2);  // Red
  }


Mod 13h'te palet diğer modlara göre daha basit, çünkü bu modda her ne kadar attribute register'a değer atamak mümkün olsa da, ekranda zaten DAC'taki tüm renkler (256 renk) gösterilebildiği için, zaten gösterilebilir olan rengi başka bir renge atamanın anlamı yok. Dolayısıyla attribute register bu modda anlamsız.

Ben Wikipedia'nın VGA maddesindeki, üç papağan görselini temel örnek olarak alacağım. Bu görselde 256 rengin tamamı kullanıldığından VGA'nın yeteneklerini göstermek için oldukça kullanışlı.

Kaynak: Wikipedia [2]

İlk olarak bu görseli 320x200 olarak boyutlandırdım. Orjinal görüntü 1280x960 olduğu için en boy oranını değişti, ama onun dışında renklere veya palete dokunmadım. Aşağıda aynı görselin DOSBox içinden ekran görüntüsü var.


İki görsel karşılaştırıldığında belirgin bir fark görünmüyor. Paletin etkisini daha iyi anlamak için 208. satırdaki setPalette fonksiyonunu yorum satırı yapıp, bu görsel default mod 13h paletiyle nasıl görünürdü ona bakalım:

Görüldüğü gibi paletin olmaması ciddi bir fark yaratıyor.

Makinalarda RAM'in megabyte'la ölçüldüğü, bu nedenle yazılımda optimizasyonun birinci öncelik olduğu zamanlarda (90lar), aklıma "paletteki renkleri düzenleyerek VGA paletine uyarlayabilir miyim?" sorusu gelmişti. Böylelikle hem 400 byte civarındaki setPalette(...) fonksiyonundan hem de resim başına 1KB büyüklükteki paletten kurtulabilirdim. Bunun teorisini, aşağıda orjinalinden [1] küçülttüğüm görselle göstereceğim. Burada 97 328 renk var.


Palet oluşturma problemi, bu sorunun benim aklıma gelmesinden çok önce kafa yorulmuş bir konu. İlk giriş noktası median cut (1982) adlı algoritma. Bu algoritmayla renk uzayındaki tüm renklerin medyanı alınarak, bu rengin en az ortalama hata ile diğerlerini temsil edebileceği varsayılıyor. Hedeflenen renk derinliğine göre özyinelemeli (recursive) bir şekilde aralıkların medyanları alınarak görsel 2n (16 veya 256) renk derinliğine indirgeniyor. Ancak bu algoritma renk değişimlerinin çok olduğu görsellerde, küçük sapmaları görmezden geldiği için, fotoğraflarda doğal olmayan renk adalarına neden oluyor. Bu bozulmayı göstermek için görseli median-cut'la 16 renge indirdim (aşağıda). Arkaplandaki ve kuşun karnındaki adacıklar özellikle belirgin.


Bu nedenle de, dithering ve/veya error diffusion gibi teknikler uygulanarak, paletteki yakın renkler aralara serpiştirilip büyük renk adalarına hafif bir gürültü (noise) ekleniyor. Böylelikle gözü daha az rahatsız eden bir sonuç elde ediliyor.


Hey gidi günler... 🥲

Median cut, yalnızca renklerin azaltılmasında kullanılıyor olsa da, error diffusion tekniği palet dönüşümlerinde de kullanılır. Algoritmalara bu yazıda değinmeyeceğim, yer kalırsa sonraki yazıya saklıyorum. Ben bu dönüşümleri zamanın Paint Shop Pro'suyla (PSP) denedim ve güzel sonuçlar elde ettim. Aşağıda, örnek görselin paletini VGA paletine uyarladığımda elde ettiğim görsel üstte olmak üzere orjinaliyle yanyana koydum. Error diffusion nedeniyle yeşil kuşta ve arkaplandaki yolda bazı ilgisiz renkler olsa da fena sayılmayacak bir sonuç.


Dönüştürdüğüm görseli setPalette(...) olmadan ekranda görüntüleyince:

Bu görseli, bir de programı modifiye etmeden görüntüledim ve GIMP'te bunları üstüste oturtup birbirinden çıkardığımda beklenildiği gibi elimde siyah bir görüntü kaldı.

Palet dönüşürme için ilk olarak, önceki yazıda bahsettiğim gibi 0x3C7 portundan indis numarasını gönderip, 0x3C9 portundan renk değerlerini okudum ve bunları dörtle çarpıp bir dosyaya JASC (Paint Shop Pro) formatında yazdırdım. Bu format üç satırlık bir başlık ve boşluk karakteriyle ayrılmış R, G, B kodlarından oluşan basit bir metin dosyası. Ve güzel tarafı GIMP'in de bu formatı okuyabilmesi.

OPEN "13.PAL" FOR OUTPUT AS #1
PRINT #1, "JASC-PAL"
PRINT #1, "0100"
PRINT #1, "256"

SCREEN 13
FOR I% = 0 TO 255
    OUT &H3C7, I%
    R% = INP(&H3C9)
    G% = INP(&H3C9)
    B% = INP(&H3C9)
    PRINT #1, LTRIM$(STR$(R% * 4));
    PRINT #1, RTRIM$(STR$(G% * 4));
    PRINT #1, RTRIM$(STR$(B% * 4))
NEXT I%

CLOSE #1

Paint Shop Pro'yu oldversion.com'dan indirip Wine ile çalıştırabildim. Dönüştüreceğim dosyayı açıp Colors -> Load Palette'i seçtim. Önceki adımda oluşturduğum 13.PAL'i yükleyip dönüştürme algoritmasını "Error diffusion dithering" olarak seçtim. Burada farklı algoritmaları deneyerek karşılaştırmak mümkün.

GIMP'teyse önce palet penceresinde (Windows -> Dockable Dialogs -> Palettes) boş bir yere sağ tıklayıp 13.PAL'i import etmek gerek. Sonra dönüştürmek istediğim dosyayı açtım ve Image -> Mode -> Indexed'le gelen pencerede "Use custom palette"i ve altında 13.PAL'i seçtim. Ben hangi dithering metodunu seçersem seçeyim, GIMP maalesef bu dönüşümü PSP'den daha iyi yapamadı.




[1]: https://commons.wikimedia.org/wiki/File:Perched_blue_and_orange_bird_%28Unsplash%29.jpg
[2]: https://en.wikipedia.org/wiki/Video_Graphics_Array#/media/File:Birds_VGA256.png

29 Eylül 2026 Salı

Bitmap (BMP) Dosyalar


Merhaba. Aslında bu yazıya VGA Paleti serisinin ikinci yazısı olarak başlamıştım ama ikinci yazı için geliştirdiğim bitmap (.bmp) dosya görüntüleyici ve buna bağlı olarak bitmap dosyaları bu yazıda ayrı ele almaya karar verdim. Böylece konuyu dağıtmadan sonraki yazıda tekrar mod 13h'teki VGA palet işlemlerine döneceğim.


BMP Dosya Formatı

Önceki yazıda C ile mod 13h için küçük bir bmp dosya göstericisi geliştirdiğimden bahsetmiştim. Bitmap (bmp) dosyalar yapı olarak oldukça basitler. Dolayısıyla konunun tamamını bir yazıda ele alabilirim.

BMP dosyalar, iki parçalı basit bir dosya başlığı (file header) ve piksel datasından oluşur. Birinci başlık dosya boyutu ve datalara ait bir gösterici içerir. İkinci başlıkta görseli ekranda gösterebilmek için gereken genişlik, yükseklik, renk derinliği, sıkıştırma bilgisi gibi bilgiler vardır. Birinci başlık yanda kırmızıyla, ikinci başlıksa sarıyla işaretlenen alan. Gri alan yazının devamında ele alacağım palet verisi. Bu dosya 320x200 boyutunda bir görsel. Yanda en, boy gibi bilgileri kutu içinde gösterdim.

Teknik olarak BMP dosyalar RLE (run length encoding) ile sıkıştırılabilse de, sıkıştırılmış bir bmp dosya bulmak pek mümkün değil. BMP dosya formatı Microsoft ve IBM tarafından geliştirilmiş olmasına rağmen, hatırladığım kadarıyla MS Paint(brush) bile sıkıştırılmış BMP kaydedemiyordu. Bu nedenle RLE sıkıştırmasına hiç değinmeyeceğim.

Bu bölümde kodun ayrıntısına girmek istemesem de, dosya formatına ait yapıları kodun header dosyasındaki ( VGA13.H ) veri yapılarından açıklamam daha kolay olacak. Dosyanın en başında 'BM' dosya imzası var. bitmapfileheader yapısında tek önemli alan pixel verilerini gösteren OffsPixelArray göstericisi (pointer), yukarıdaki resimde 36h, 04h değerlerinin olduğu alan. Bundan sonra görselle ilgili verileri tutan bitmapimageheader geliyor. Ben verilerin dosya offsetlerini de bu dosyada belirttim.

Wikipedia'daki BMP dosya formatı maddesine göre, bitmapimageheader yapısının yedi farklı sürümü var. Benim de temel aldığım 40-byte uzunluktaki BITMAPINFOHEADER sürümü en yaygın sürüm. Ben kendi kodumu, GIMP de dahil, farklı görüntü işleme yazılımlarıyla kaydettiğim ve internetten indirdiğim toplam 23 dosyayla test ettim. Bu dosyalardan birini ekleyemiyorum, ancak 22 dosyalı test setini de linkledim. Bu arada GIMP, en son sürüm BITMAPV5HEADER'ı geliştirmiş olmasına rağmen kaydederken default bunu kullanmıyor.

Bitmap dosyalar dört farklı renk derinliğini destekliyor. Bunlar BitsPerPixel alanında kayıtlı. BitsPerPixel;

  • 1 ise görsel siyah ve beyaz olmak üzere iki renklidir. Her byte 8 piksel içerir.
  • 4 ise görsel onaltı renklidir ve bu onaltı renk için bir palet alanı da bulunur. Her byte iki piksel içerir.
  • 8 ise görsel 256 renklidir. Dosya palet içerir. Pixel dizisinde her byte bir renk içerir.
  • 24 ise görselin bir pikseli, her biti 8 bitten oluşan R, G, B üçlülerinden oluşur. Dosya palet içermez.

Ben sadece 8 bit per pixel görsellere odaklandım, çünkü hem mod 13h'le daha fazlasını zaten gösteremem, hem de palet yalnız 4 ve 8 bit renk derinliğinde var. Yazıdaki görselleri Wikipedia'da VGA maddesindeki, telin üzerinde oturan üç papağan görselini 320x200'e boyutlandırarak ürettim.

Doğrudan Link

8-bitlik görseller için bir parantez açmam gerek. Mantıken gri tonlamalı palet, i indis olmak üzere i: (i, i, i) şeklinde. Haliyle paleti dosyaya kaydetmeye gerek yok. Ben, dosya 8-bit renk derinliğinde ve palet yoksa gri tonlamalı ele alınıyor, ama bazı programlar uyumluluk için paleti yine de dosya başlığına kaydediyor biliyordum. GIMP'le kaydettiğim gri tonlamalı görsel paletli kaydedildi ve paleti elle silip dört farklı programla denediğim halde dosyayı açamadım. Üstelik Wikipedia'da renk derinliği 8 veya daha küçük dosyalarda palet zorunlu diyor. Öte yandan test setindeki bmp_08.bmp dosyası bu tür paletsiz 8-bit dosyaya bir örnek. Kısacası, bu üzerinde anlaşmaya varılamamış bir konu. Kodlama açısından benim işime geldi, paletsiz durum için ekstra birşey yapmama gerek kalmadı. Benim kodum adı geçen dosyayı açıyor, ama gri tonlamalı paleti yüklemesi gerektiğini bilmediğinden default mod 13h paletiyle açıyor.

Palet verisi, bitmapimageheader'ı takip ediyor. bitmapfileheader'ın büyüklüğü sabit 14-byte. bitmapimageheader'ın büyüklüğüyse HeaderSize alanında veriliyor. Yani paletin offseti bu değere 14 eklenerek bulunabilir. Bu toplamın sonucu bitmapfileheader.OffsPixelArray değerine eşitse, paletin olmadığı anlaşılır (örn. 24-byte renk derinliği için). Her palet girdisi sırasıyla R,G,B,A'yı temsil eden 4 byte'dan oluşur. Ancak alfa kanalı ve transparanlık .bmp dosyalarda standart değildir. Bu nedenle A değeri her zaman sıfırdır. Toplamda 2BitsPerPixel kadar palet girdisi bulunur.

Paleti piksel verileri takip eder. Palet bitmapfileheader.OffsPixelArray ofsetinde bitmelidir. Burada pikseller soldan sağa ve aşağıdan yukarı kaydedilmiştir. Yani ilk veri sol alttaki piksele aittir. Eğer görselin genişliği dördün katı değilse, satırlar dördün katı olacak şekilde sıfırla tamamlanarak saklanır. Örn. 133 piksel genişlikteki bir görselde, veriyi 136'ya tamamlamak için her satırın sonunda üç tane 0 bulunur.

DOSBox Ekran görüntüsü

BMP Gösterici

Yukarıda, programın ekran çıktısını gösteren bir örnek var.

Ben kodu 174. satırdaki main(...) fonksiyonundan başlayıp inceleyeceğim. Burada ilkin her iki header için bellekte statik olarak yer ayrılıyor (satır 177-178). Eğer bir bmp dosya programa parametre olarak verilmemişse, kullanıcıya dosyanın adını soruyor (satır 188) ve bu dosya okunmak üzere açılıyor (satır 193).

readFirstHeader(...) fonksiyonu dosyayı doğrudan bir veri yapısına okuyor ve dosya imzası 'BM'ye eşit değilse (satır 43) sıfırdan farklı bir hata değeriyle çıkıyor. Bu programı da sonlandırıyor (satır 199).

199. satırdaki readSecondHeader(...) fonksiyonu da benzer şekilde dosyadan ikinci başlığı veri yapısına okuyor. Burada kolaylık olsun diye showBMPinfo fonksiyonuyla (satır 22) dosyanın başlığını ekrana yazdırıyorum. Bu fonksiyon çağrısı istenirse kapatılabilir veya bir debug parametresine bağlanabilir. Eğer açılan dosya bizim gösterebileceğimiz özellikte değilse, örn. renk derinliği sekizden farklıysa veya sıkıştırma varsa, fonksiyon ve program bir hata değeriyle sonlanıyor. 69. satırda, dosyanın büyüklüğü 320x200'den büyükse ekrana bir uyarı yazılıyor ancak dosya açılıyor.

mode13() ve textmode() fonksiyonlarını kolaylık olması için assembly ile yazdım (satır 78 ve 86).

Yukarıda bitmapimageheader için aslında birden fazla sürüm olduğunu yazmıştım. Bu sürümlerin birçoğunun ortak noktası, genişlik, yükseklik ve renk derinliği alanları. Eğer başlık tesadüfen BITMAPINFOHEADER değilse (farklı uzunlukta bir başlıksa) readSecondHeader(...) fonksiyonu başlığı tamamen okuyamamış olacak. Bu nedenle 207. satırda olası okunmayan byte'lar atlanıp, dosya göstericisi paletin başına çekilerek setPalette(...) ile palet işlemlerine geçiliyor. Bu fonksiyonda palet, ColorCount adedinde 4-byte'lık parçalarla okunuyor. 105. satırda, ilk yazıda da anlattığım gibi renk indisi 0x3C8 portuna ve ardından rengin R,G,B değerleri 0x3C9 portuna yazılıyor. VGA 18-bit olduğu için 8-bit'lik değerler iki sağa kaydırılarak 6-bit'e dönüştürülüyor.

210. satırdaki fseek(...) yine BITMAPINFOHEADER'dan farklı header sürümlerinde doğru yeri okumak için var, aksi takdirde zaten dosya göstericisinde o anda firstHeader.OffsPixelArray değeri olmalı. setImage(...), piksel dizisini okuyup ekrana basmakla görevli. Bu fonksiyonu iki kere yazdım. 134. satırdaki ilk sürümde dosya byte-byte okunduğu için verimsizdi. İkinci sürümde görsel satır satır okunuyor. Bunun için genişliği, ceilTo4 ile dördün katı olacak şekilde yukarı yuvarlıyorum, ve bu büyüklükte bir bellek alanını ayırıyorum (satır 160). Sonra byte'ları dosyadan okuyup for döngüsü içerisinde (satır 167) ekrana yazdırıyorum. Burada PutPixel fonksiyonu yerine şöyle birşey yapılabilirdi:

lds si, linebuffer
push A000
pop es
mov ax, i
mov di,ax
shr ax,8
shr di,6
add di,ax
mov cx, c4
rep movsb

Bu kod parçası yalnızca bir taslak. linebuffer göstericisi DS:SI'ye yükleniyor. i, 320 ile çarpılıp (shr ile) DI'ye yükleniyor. Satır genişliği de CX'e yüklenip, rep movsb ile linebuffer blok blok VGA ekran belleğine yazılabilirdi. Ancak bu durumda MIN makrosunu da assembly'e uyarlamak gerekiyor. Bu şekilde, daha öncekinden bile hızlı çalışması gerekir ama bunu uygulamadım, çünkü C içinden assembly ve far pointer'larla uğraşmak istemedim, yoksa debugging süreleri inanılmaz uzun sürebilirdi.

25 Ağustos 2026 Salı

VGA Paleti #1


Merhaba. Bu yazı dizisinde VGA'da renk paleti kullanımından söz edeceğim. Palet detaylı bir konu olduğundan birden fazla yazıda ele almayı düşündüm. Bu bölümde renk paleti nedir, nasıl okunur, nasıl değiştirilir; bunları ilk önce metin moduyla VGA 640 x 480 (nam-ı diğer screen 12) moda odaklanarak ele alacağım. Mod 13h'ü ayrı bir yazıda sonra ele alacak ve paletle yapılacak bazı görsel efektlere değineceğim.


VGA'nın getirdiği iki tane önemli grafik ekran modu var. Biri 640 x 480 x 16 renkten oluşan mod 12h ve diğeri de 320 x 200 x 256 renkten oluşan malum mod 13h. Diğerleri eski ekran kartlarına uyumluluk için bırakılmış modlar. VGA ekran kartının DAC'i (Dijitalden analoğa dönüştürücü) 18 bit RGB renk gamutundaki renkleri (218 = 256 K = 262 144 renk) ekranda gösterebilse [1] de mod 13h'te bunlardan yalnızca 256'sını ve mod 12h'de 16'sını aynı anda ekranda gösterebilir. İşte bu renk gamutundan (gamından) ekranda gösterilmek üzere seçilen renk altkümesine palet adı verilir.

Renk bilgisi, bir indis ve VGA için 6+6+6 olmak üzere 18 bitlik bir RGB renk değerinden oluşur. Örneğin indis 1 ve RGB kodu 0,0,63 ise birinci renk mavi, indis 2 ve RGB kodu 24,24,24 ise ikinci renk gridir vb.. İndis değeri sıfırdan başlar ve sıfırıncı renk arkaplan rengidir. Dolayısıyla sıfırıncı renk genellikle siyahtır (RGB 0,0,0). Eğer 0'dan 255'e kadar olan indisleri doğrusal artan ardışık t değerleriyle RGB t,t,t şeklinde doldurulursa gri tonlu (greyscale) veya RGB 0,r,0 şeklinde doldurulursa yalnız yeşil tonlu bir görüntü elde edilir.

Renkler ekran kartının DAC'i aracılığıyla işlenir. Dolayısıyla palet işlemleri VGA kartının DAC yazmaçlarıyla yapılır. Bunlar üç* tanedir [3]:

DAC Address Read Mode Register (Write at 3C7h)
76543210
DAC Read Address

Aslında 3C7h'nın iki görevi vardır. Bu yazmaç okunursa, en düşük anlamlı iki bit DAC'ın okuma modunda mı yoksa yazma modunda mı olduğunu gösterir. Ancak, zaten bir out işlemi yapmışsam yazıyorumdur, in işlemi yapmışsam okuyorumdur. Bu yüzden bu işlevi pek kullanılmaz. Paleti okumak içinse bu yazmaca indis değeri yazılır. Ardından 3C9h portundaki DAC data yazmacı byte uzunlukta üç kere okunur:


DAC Data Register (Read/Write at 3C9h)
76543210


DAC Data

Palete yazılacağı zaman renk indisi 3C8h portuna yazılır ve DAC data yazmacına üç tane byte uzunluklu değer gönderilir.


DAC Address Write Mode Register (Read/Write at 3C8h)
76543210
DAC Write Address

*Aslında DAC yazmaçları dört tanedir. Yukarıda söz etmediğim DAC maskeleme yazmacına 3C6h portundan erişilir ve her zaman 0FFh değerini içerir. Buraya başka bir değer yazılması DAC'ı erişime kapatır [2].

Yukarıda byte uzunluklu değer okunur veya yazılır ifadesiyle kast edilen in al,dx ve out dx,al işlemleridir. Ancak okunan ve yazılan değerin en düşük 6 biti anlamlıdır. Renk gamutunun 6+6+6=18 bit olduğunu hatırlayınız.


İlk olarak metin modu ve mod 12h'ya odaklanalım. Bu iki modda ekranda en fazla 16 renk gösterilebilir. Bu bakımdan birbirine benzerler. Dikkatlerden kaçan bir ayrıntı, metin modunda da paletin değiştirilebilir olmasıdır. İlk VGA yazısında [5] metin modda yazı ve arkaplan renklerinin nasıl değiştirilebildiğini göstermiştim. Şimdi basit bir BASIC koduyla metin ekrandaki palete bakalım:

FOR I% = 0 TO 63
    OUT &H3C7, I%
    PRINT "("; I%; "="; INP(&H3C9); INP(&H3C9); INP(&H3C9); ")";
NEXT I%

DEF SEG = &HB800

FOR J% = 1 TO 15
    FOR I% = 1 TO 159 STEP 2
        POKE (I% + J% * 160), J%
    NEXT I%
NEXT J%

İlk bölümde 3C7h'den indis değerini gönderip, sonra 3C9h'dan renk kodlarını okuyup ekrana yazdırdım. İkinci bölümde metin mod ekran belleğine erişip birinci satırı birinci, ikinci satırı ikinci renk kodu vb. olmak üzere renklendirdim:

Metin modunda 16 renk kullanılabildiğini söyledim, ama ekrana 64 renk kodu yazdırdım, ve ilginç biçimde [16, 63] arasındaki indislere de bir kod atanmış olduğu görülüyor. [64, 255] arası indislerin kodu sıfır, bu yüzden onları yazdırmadım ama istenirse onlara da renk kodu atanabilir. Peki bu ne demek? Normalde metin ekranda bir karakter, bellekte bir byte karakterin ASCII kodu, diğeri renk kodu olmak üzere 2 byte'dan oluşur. Renk byte'ının düşük anlamlı 4 biti karakterin rengi, yüksek anlamlı 4 biti karakterin arkaplan rengi olduğunu biliyoruz. Renk için 4 bit ayrılmışken 16 ve üstü indislere renk kodu atamanın ilk bakışta anlamı yok görünüyor, ama burada bir trick devreye giriyor: VGA Attribute Register (3C0h) [4] ile bir rengin palet indisi değiştirilebilir. Çok ayrıntıya girmeden basit bir kod parçasıyla:

A% = INP(&H3DA)
OUT &H3C0, 5
OUT &H3C0, 60
OUT &H3C0, &H20

Yukarıda beşinci attribute yazmacına 60 değerini yazarak 60. rengi beşinci renge atamış oldum.

Ekran görüntüsüne geri dönersek, bir önceki kodla ekrana yazdırdığım palette sekizinci renk (0, 0, 21) lacivert / koyu mavi, dokuzuncu renk (0, 0, 63) yani mavi olarak görünüyor. Ancak sıfırdan başlayarak saymak koşuluyla sekizinci satırın koyu mavi olmasını beklerken aslında koyu gri, ve dokuzuncu satırın daha baskın bir mavi olmasını beklerken neon mavisi tabir edilen soluk bir mavi olduğu görülüyor. Eğer ekrandaki renk kodları gerçekten satırların rengi olsaydı aslında ekran aşağıdaki görselin sağ tarafı gibi görünürdü. Gerçekte görünen sol taraf. Daha kolay karşılaştırılması için iki görseli yan yana koydum:


Buradan çıkarılacak sonuç, aslında metin modunun [8, 15] renkleri için attribute yazmaçlarını zaten kullanıyor olduğu. Ben görselin sol yanını elde etmek için ilk [1,15] renk aralığını elle paletteki [1,15] renklerine eşledim.

Metin moddaki herşey mod 12h için de geçerli. 16 renk kullanılabilmesine rağmen palette 64 renk kayıtlı. Bu noktada bir de ek bilgi: Eğer mod 13h'ten sonra mod 12h'ye veya metin moduna geçilirse, mod 13h'e geçişte yüklenen palet diğer modlarda da geçerli olur. Yani bilgisayarın (veya DosBox'ın) açılışında [64, 255] arası renklerin hepsi (0, 0, 0) iken mod 13h'e girip tekrar metin moduna veya mod 12h'ye geçince bunlar sıfırdan farklı değerler alır.

Yine metin moddakine benzer şekilde ekrandaki renkler paletle seçilir ama attribute register da ikinci bir dönüştürme katmanı sağlar. Aşağıda tüm bunları görselleştirmek için bir BASIC kodu var:

SCREEN 12
CONST K = 20

FOR I% = 0 TO 63
    OUT &H3C7, I%
    PRINT "("; I%; "="; INP(&H3C9); INP(&H3C9); INP(&H3C9); ")";
NEXT I%

SLEEP

FOR I% = 0 TO 15
    ' PRINTING RECTANGLES
    LINE (0, I% * K)-(640, (I% + 1) * K), I%, BF
NEXT I%

FOR I% = 0 TO 15
    ' SETTING PALETTE
    OUT &H3C8, I%
    OUT &H3C9, I% * 4
    OUT &H3C9, I% * 0
    OUT &H3C9, 63 - I% * 4
NEXT I%

SLEEP

' Change fifth color
A% = INP(&H3DA)
OUT &H3C0, 5
OUT &H3C0, 60
OUT &H3C0, &H20
 

Birinci kısımda [0, 63] arası renk paletindeki renk kodları ekrana yazdırılıyor. Sonraki kısımda ekrana [0, 15] arası renklerle boyutu K sabitiyle ayarlanan 16 dikdörtgen bastırılıp sonra palete maviden kırmızıya kayan 16 renk atanıyor. Görüldüğü gibi palete renk ataması, palet indisinin 3C8h portuna yazılıp, renk verisinin 3C9h portundan gönderilmesiyle yapılıyor.

Burada sıfırıncı renge mavi atandığı için arkaplan rengi mavi oluyor. Son bölümdeyse yine beşinci renk altmışıncı palet girdisiyle eşleştirilip renk farkı belirgin bir biçimde ortaya konuyor.



[1]: https://en.wikipedia.org/wiki/Video_Graphics_Array
[2]: https://wiki.osdev.org/VGA_Hardware#Port_0x3C6
[3]: http://www.osdever.net/FreeVGA/vga/colorreg.htm
[4]: http://www.osdever.net/FreeVGA/vga/attrreg.htm
[5]: https://trapflag.blogspot.com/2025/11/vga-metin-kipinde-yumusak-kaydrma.html

21 Aralık 2025 Pazar

VGA Metin Kipinde Yumuşak Kaydırma #3: Base Adres Değiştirme


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:

0.00   0.36   0.71   1.07   1.43   1.79   2.14   2.50   2.86   3.21   3.57   3.93   4.29   4.64

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.

Ve son olarak kaynak kod aşağıda:

6 Kasım 2025 Perşembe

VGA Metin Kipinde Yumuşak Kaydırma İşlemi #2


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)
76543210
SDLC9SVB9Maximum 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)
76543210

Byte PanningPreset 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)
76543210
Start Address Low


Start Address High Register (Index 0Ch)
76543210
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.


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

21 Ağustos 2025 Perşembe

VGA Metin Kipinde Yumuşak Kaydırma İşlemi #1


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.

Orjinal VGA Kart (wikipedia)
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)
76543210


CDCursor Scan Line Start


Şimdi [6]'daki imleci devre dışı bırakma kodunu ele alalım:

void disable_cursor()
{
    outb(0x3D4, 0x0A);
    outb(0x3D5, 0x20);
}

Ö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)
76543210

Cursor SkewCursor 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)
76543210
Cursor Location High


Cursor Location Low Register (Index 0Fh)
76543210
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

SUB MOVECURSOR (CURSORX%, CURSORY%)
    POSITION% = CURSORY% * 80 + CURSORX%

    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.



[1]: DEC PDP8 Family User's Guide TSS/8 (1970). Link
[2]: https://retrocomputing.stackexc....damage-my-vga-card-by-programming-it-in-assembly-throu
[3]: http://wiki.osdev.org/VGA_Hardware
[4]: http://wiki.osdev.org/Text_Mode_Cursor
[5]: http://www.osdever.net/FreeVGA/vga/crtcreg.htm
[6]: https://en.wikipedia.org/wiki/VGA_text_mode#Fonts