int21h

Efektivní nastavování VESA videorežimů

Na toto téma se v časopise určitě sejde více článků, ale já bych se chtěl zaměřit na dva aspekty, které programátoři v pascalu většinou příliš neřeší.
A to aktivní vyhledávání videomódů a nastavování obnovovací frekvence monitoru.

1)aktivní vyhledávání:
Videomódy VESA zavádí pomocí funkce INT 10/AX=4F02h
Tedy něco takového:
MOV AX,4f02h
MOV BX,videorezim
INT 10h  

Jednoduché. Problém je, co zadat jako videorežim. Normy VESA v1.0 a v1.2 definovaly závazné kódy videorežimů. Např. 101h=640x480 8bit, 103h=800x600 8bit, 111h=640x480 16bit a tak dále. Tyto kódy naleznete všude možně po internetu, v tom problém není.
Jenže:
-Od verze VESA 2.0 už žádné nové módy závazně definovány nejsou a nebudou.
-Jednotliví výrobci ne vždy dodržují videomódy z VESA 1.0/1.2
-Tuplem nejisté je to s emulovanými prostředími jako DosBox nebo DOSemu
-Některé karty nemají módy 16bitové, ale 15bitové a některé karty místo 32bitových 24bitové. Tohle je potřeba ohlídat.

Proto je vhodné naprogramovat aktivní vyhledávání videomódů.
1) Napřed si pomocí VESA funkce 4F00h zjistíme ukazatel na seznam všech videorežimů karty
2) Pro každý kód z tabulky zavoláme funkci 4F01h (Vrať informaci o videomódu). Funkce vygeneruje tabulku s informacemi o dotazovaném videorežimu. V tabulce je (mimo jiné) uvedeno rozlišení a bitová hloubka.
3) Když údaje v tabulce vyhovují požadovanému videomódu, tak máme vyhráno a můžeme zavolat funkci 4F02h. Do registru BX dáme číslo onoho vyhovujícího módu.

Takto bude vypadat zdroják v Turbo pascalu v reálném módu:
Program VESAdemo;
Function NajdiVESArezim(xroz,yroz,bitu:word):word;
    Function TestMode(videomod,xroz,yroz:word;bitu:byte):boolean;
    { otestuje, zda ma Videomod pozadovane rozliseni a barevnou hloubku }
    var buffer:array[0..511] of byte;
        vysledek:byte;
        p:pointer;
    begin
    p:=@buffer;
    asm  
    push es
    push di
    mov ax,4f01h
    mov cx,videomod      { naseho kandidata dame do CX }
    mov es,word ptr p[2] { vygenerovanou tabulku umisti na }
    mov di,word ptr p[0] { tuto adresu }
    int 10h
    mov ax,es:[di]
    test ax,8            { otestuje, zda je to rezim graficky, ci textovy }
    jz @chyba
    @graficky_rezim:
    mov ax,es:[di+12h]   { souhlasi sirka? }
    cmp ax,xroz
    jnz @chyba
    mov ax,es:[di+14h]   { souhlasi vyska? }
    cmp ax,yroz
    jnz @chyba
    mov al,es:[di+19h]   { souhlasi bitova hloubka? }
    cmp al,bitu
    jnz @chyba
    mov ax,1             { Kandidat vyhovuje? }
    jmp @konec              { skok na konec }
    @chyba:              { Takze smula, kandidat neprosel }
    xor ax,ax
    @konec:
    pop di
    pop es
    mov vysledek,al  
    end;
    Testmode:=vysledek<>0; { Zkraceny zapis. Ekvivalentni zapisu if vysledek=0 then Testmode:=false else Testmode:=true }
    end;


var zakladni_info:array[0..511] of byte;
    p:pointer;
    tabulka_videomodu:^word;
begin
p:=@zakladni_info;
asm  
push es
push di
mov ax,4f00h
mov es,word ptr p[2]
mov di,word ptr p[0]
int 10h                              { nactem zakladni informace o videokarte }
db 66h;mov ax,es:[di+0eh]
db 66h;mov tabulka_videomodu.word,ax { vytahneme z ni ukazatel na seznam videomodu }
pop di
pop es  
end;


{Promenna Tabulka_Videomodu ukazuje na seznam vsech dostupnych rezimu VESA}


while tabulka_videomodu^<>$0FFFF do { seznam videomodu je ukoncen hodnotou FFFFh }
   begin
   if TestMode(tabulka_videomodu^,xroz,yroz,bitu) then { Otestuj rezim }
      begin
      NajdiVESArezim:=tabulka_videomodu^;          { Vyborne, nasli jsme ho! }
      Exit;
      end;
   inc(tabulka_videomodu);
   end;
NajdiVESArezim:=0;
end;


Procedure NastavRezim(rezim:word);assembler;
asm  
mov ax,4f02h
mov bx,rezim
int 10h  
end;


Procedure Zpatky;assembler;
asm
mov ax,3
int 10h  
end;


var rezim:word;
    sirka,vyska,hloubka:word;
begin
writeln('Zadej sirku:');readln(sirka);
writeln('Zadej vysku:');readln(vyska);
writeln('Zadej bitovou hloubku (pocet bitu na pixel):');readln(hloubka);
rezim:=NajdiVESArezim(sirka,vyska,hloubka);
if rezim=0 then writeln('Tvoje graficka karta tento rezim nepodporuje.') else
   NastavRezim(rezim);


{ A jsme tam ! }
readln;


Zpatky;
writeln(rezim);
end.  

Pro Freepascal je potřeba zdroják poněkud upravit. Vlastně přepsat.
Jde o to, že pokud nechceme použít protektové rozhraní rozhraní VESA (což nechceme, protože to by bylo ještě složitější), tak si musíme připravit buffer v konvenční paměti, protože buffer v paměti nad 1MB by VESA BIOS neviděl.
A taky musíme odbourat 16. bitové adresování v assembleru. Nejlépe tak, že žádný assembler nepoužijeme.

Program VESAdemo;
uses Go32,Dos;          { Ovladani konvencni pameti }


var VesaBaseInfo:array[0..511] of byte;
    VesaModeInfo:array[0..255] of byte;


PROCEDURE ReadVesaBaseInfos;
VAR       LowMemPtr : LongInt;
          Regs:Registers;
Begin
LowMemPtr:= Global_DOS_Alloc(512);       { Alokuj blok konvencni pameti }
{ Nepouzivame zde totiz rozhrani pro chraneny mod, protoze se nejsme jisti, zda }
{ ho nase karta podporuje. To totiz zvladaji jenom karty s VESA>=2.0 }
{ Takze pouzivame realmodove rozhrani a tudiz si musime pripravit blok 512 bajtu v KONVENCNI pameti }


 FillChar(VesaBaseInfo,SizeOf(VesaBaseInfo),0);
 VesaBaseInfo[0]:= byte('V');         { Mame VESU 2.0 nebo vyssi? }
 VesaBaseInfo[1]:= byte('B');
 VesaBaseInfo[2]:= byte('E');
 VesaBaseInfo[3]:= byte('2');


 DOSMemPut(Word(LowMemPtr shr 16),0,VesaBaseInfo,512); {LowMemPtr^:=VesaBaseInfo^}


 FillChar(Regs,SizeOf(Regs),0);
 Regs.eax := $4F00;                       { Zjisti zakladni informace o videokarte }
 Regs.es  := Word(LowMemPtr shr 16);      { segment bloku LowMemPtr }
 Regs.edi := 0;                           { Je 0. Global_DOS_Alloc to vzdy zaonaci tak, ze offset je 0 }
 RealIntr($10,Regs);


                                          { VesaBaseInfo^:=LowMemPtr^ }
 DOSMemGet(Word(LowMemPtr shr 16),0,VesaBaseInfo,512);
 Global_DOS_Free(Word(LowMemPtr));        { Konvencni pamet je vzacny zdroj, }
                                          { takze ji uvolnim jak nejdriv to jde }
End;


PROCEDURE ReadVesaModeInfos(Mode:Word);
VAR       LowMemPtr : LongInt;
          Regs:Registers;
Begin
 LowMemPtr:= Global_DOS_Alloc(256);       { Alokujem 256 bajtu v konv. pameti }
 FillChar(VesaModeInfo,SizeOf(VesaModeInfo),0);
 FillChar(Regs,SizeOf(Regs),0);


 Regs.es:= Word(LowMemPtr shr 16);
 Regs.cx:= Mode;
 Regs.ax:= $4F01;
 RealIntr($10,Regs);
                                          { VesaBaseInfo^:=LowMemPtr^ }
 DOSMemGet(Word(LowMemPtr shr 16),0,VesaModeInfo,256);
 Global_DOS_Free(Word(LowMemPtr));        { Konvencni pamet zase uvolnime }
End;


Function NajdiVESArezim(sirka,vyska,hloubka:word):longint;
var segm,ofss,i:word;
    mode:array[0..255] of word;
    sv,vv:word;
    dd:longint;


begin
ReadVESABaseInfos;
Move(VesaBaseInfo[$0e],dd,4);
segm := Segment_To_Descriptor(dd shr 16);
ofss := dd and $FFFF;
seg_move(segm, ofss, get_ds, longint(@mode), SizeOf(mode));
for i:=0 to 255 do
   if mode[i]=$FFFF then Exit(0) else
      begin
      ReadVESAmodeInfos(mode[i]);
      move(VESAMODEINFO[$12],sv,2);
      move(VESAMODEINFO[$14],vv,2);
      if (sv=sirka)
         and
         (vv=vyska)
         and
         (VESAMODEINFO[$19]=hloubka)
         then Exit(mode[i]);
      end;
NajdiVESArezim:=0;
end;


Procedure NastavRezim(rezim:word);assembler;
asm  
mov ax,4f02h
mov bx,rezim
int 10h  
end;


Procedure Zpatky;assembler;
asm
mov ax,3; int 10h;
end;


var rezim:word;
    sirka,vyska,hloubka:word;
begin
writeln('Zadej sirku:');readln(sirka);
writeln('Zadej vysku:');readln(vyska);
writeln('Zadej bitovou hloubku (pocet bitu na pixel):');readln(hloubka);
rezim:=NajdiVESArezim(sirka,vyska,hloubka);
if rezim=0 then writeln('Tvoje graficka karta tento rezim nepodporuje.') else
   NastavRezim(rezim);


{ A jsme tam ! }
readln;


Zpatky;
writeln(rezim);
end.  

2)nastavení obnovovací frekvence monitoru:
Od verze VESA 3.0 je definováno rozhraní pro nastavení obnovovací frekvence monitoru. Pochopitelně to má význam jenom na klasických CRT monitorech. Obraz se na nich vytváří následujícím způsobem:
Uvnitř monitoru je zařízení, kterému se říká elektronový emitor nebo také elektronové dělo. Ten vysílá proud elektronů na přesně stanovené místo na monitoru (na jeden pixel). Na místě dopadu se rozsvěcí fluorescenční vrstva (to je termín z monochromatických obrazovek, nevím, jak se tomu říká u barevných) a dělo zamíří na vedlejší pixel. Takto projíždí celou obrazovku odshora dolů vždy zleva do prava. Vpodstatě tedy takto.
repeat
For y:=o to MaxY do
    For x:=0 to MaxX do
        NechToChviliPusobit(cas);
until Vypnuti_pocitace;  { Jde to vypnout i programove, ale to nas ted nezajima }  
Z uvedeneho vyplývá následující: paprsek vždy putuje jedním směrem.
Co se tedy stane, když paprsek dorazí na konec řádku? Emitor se na chvilinku vypne a zamíří na první pixel následující řádky.
Tento děj se nazývá horizontální návrat paprsku (horizontal refresh).
Pokud dorazí na poslední bod poslední řádky, vrací se na pozici [0,0] což se nazývá vertikální návrat paprsku (vertical refresh). Tento děj je pro programátora mnohem důležitější, než horizontální návrat, protože právě během této doby můžeme měnit obsah videopaměti bez rizika, že se na monitoru objeví tzv. blikání.
Pokud je ovšem celý tento děj příliš pomalý (opakuje se málokrát za vteřinu neboli obnovovací frekvence je příliš nízká), zírání do monitoru unavuje oči a práce u počítače je nepříjemná.
Dá se říct, že obnovovací frekvence 100Hz (obnova 100x ze vteřinu) je perfektní, 80Hz velmi dobré, 70Hz vyhovující, 60Hz nevyhovující a míň to už je na blázinec.
Klasická rozlišení VGA mívají frekvenci mezi 70 a 80Hz. Textové módy mají myslím 75Hz.
SVGA rozlišení ovšem standardně klesají někam k 60Hz a to už je blbé. Naštěstí rozhraní VESA VBE 3.0 umožňuje nastavit si frekvenci vlastní.

Následující zdroják je přeložitelný v TP i FP, ale v TP nemusí fungovat správně kvůli příliš malému rozsahu čísel Longint.
Taky nemusí fungovat pod běžícími windows, protože jejich ovladače mohou blokovat uživatelské nastavování obnovovací frekvence. Zkoušejte to tedy raději v čistém DOSu.

Program refresh;
{$Q-}


{$IFDEF FPC}
{$ASMMODE INTEL}
uses Go32,Dos;
{$ENDIF}
const
HNEG = 1 shl 2;
VNEG = 1 shl 3;


type CRTC_info=packed record
     HorizontalTotal:word;
     HorizontalSyncStart:word;
     HorizontalSyncEnd:word;
     VerticalTotal:word;
     VerticalSyncStart:word;
     VerticalSyncEnd:word;
     Flags:byte;
     PixelClock:longint;  { v Hz }
     RefreshRate:word;    { v setinach Hz }
     reserved:array[0..39] of byte;
     end;


{$IFNDEF FPC}dword = longint;{$ENDIF}


Procedure Vypocitej_crct_casovani(xres,yres,xadjust,yadjust:longint;var crtc:CRTC_info);
{ Nema smysl snazit se tohle pochopit. Proste to tak je. }
{ Akorat jenom: To divne nasobeni desetinnymi cisly je proto, ze skutecna }
{ sirka obrazovky je rekneme o par procent vetsi nez udavane rozliseni, }
{ protoze kolem zobrazovaci plochy zustava uzke nezobrazovaci okoli. }
var
HTotal, VTotal:longint;
HDisp, VDisp:longint;
HSS, VSS:longint;
HSE, VSE:longint;
HSWidth, VSWidth:longint;
SS, SE:longint;
doublescan:boolean;
begin
doublescan:=false;
  if (yres < 400) then
      begin
      doublescan := TRUE;
      yres :=yres*2;
      end;
HDisp := xres;
Htotal:=round(HDisp*1.27) and (not 7);
HSWidth := round((HTotal - HDisp) / 5) and (not 7);
HSS := HDisp + 16;
HSE := HSS + HSWidth;
VDisp := yres;
VTotal := round(VDisp * 1.07);
VSWidth := round(VTotal / 100) + 1;
VSS := VDisp + round((VTotal - VDisp) / 5) + 1;
VSE := VSS + VSWidth;
SS := HSS + xadjust;
SE := HSE + xadjust;
if (xadjust < 0)  then
   if SS < HDisp + 8 then
      begin
      SS := HDisp + 8;
      SE := SS + HSWidth;
      end else
   else
   if HTotal - 24 < SE  then
      begin
      SE := HTotal - 24;
      SS := SE - HSWidth;
      end;
HSS := SS;
HSE := SE;
SS := VSS + yadjust;
SE := VSE + yadjust;
if (yadjust < 0)  then
   if SS < VDisp + 3 then
      begin
      SS := VDisp + 3;
      SE := SS + VSWidth;
      end else
   else
   if VTotal - 4 < SE then
      begin
      SE := VTotal - 4;
      SS := SE - VSWidth;
      end;
VSS := SS;
VSE := SE;
crtc.HorizontalTotal     := HTotal;
crtc.HorizontalSyncStart := HSS;
crtc.HorizontalSyncEnd   := HSE;
crtc.VerticalTotal       := VTotal;
crtc.VerticalSyncStart   := VSS;
crtc.VerticalSyncEnd     := VSE;
crtc.Flags               := HNEG or VNEG;
if doublescan then
   crtc.flags:=crtc.flags or byte(doublescan);
end;


Function get_closest_pixel_clock(mode_no:word;vclk:longint):dword;
{ Pixel clock urcuje, jak dlouho se ma paprsek zdrzet na miste nez prejde na dalsi pozici }
{ Sice jsme to uz vypocitali v procedure Vypocitej_CRTC_casovani, ale jde o to, }
{ ze graficky cip nemuze generovat uplne jakoukoliv hodnotu "pixel clock". }
{ Nastesti umi rict, jaky umi vygenerovat nejpodobnejsi }
{$IFDEF FPC}
var r:registers;
begin
r.ax:=$4f0B;
r.bl:=0;
r.ecx:=vclk;
r.dx:=mode_no;
intr($10,r);
if r.ah<>0 then get_closest_pixel_clock:=0 else get_closest_pixel_clock:=r.ecx;
{$ELSE}
var l:longint;
begin
asm  
   mov ax,4f0bh
   xor bl,bl
   db 66h;mov cx,vclk.word
   mov dx,mode_no
   int 10h
   cmp ah,0
   jnz @preskoc
   db 66h; xor cx,cx
@preskoc:
   db 66h; mov l.word,cx
end;
get_closest_pixel_clock:=l;
{$ENDIF}
end;


Function VESA_version_3_available:boolean;
{ Nechce se mi s tim patlat. Zjistuje se to z funkce 4f00h }
begin
VESA_version_3_available:=true;
end;


Function Najdi_Videorezim(xr,yr,bpp:longint):word;
{ Nechce se mi s tim patlat. }
begin
Najdi_Videorezim:=$103+$4000;
end;


Procedure Nastav_grafiku(xr,yr,bpp,frek:longint);
var xadjust,yadjust:longint;
    crtc:CRTC_info;
    vclk,c,long:dword;
    mode,w:word;
    segm,ofsm:word;
    f0:double;
    {$IFDEF FPC}regs:Registers;{$ENDIF}
begin
xadjust:=0;  { jemne horizontalni }
yadjust:=0;  { a vertikalni centrovani obrazu }


mode:=Najdi_Videorezim(xr,yr,bpp);


if VESA_version_3_available then
   begin
   Vypocitej_crct_casovani(xr,yr,xadjust,yadjust,crtc);
   vclk := dword(crtc.HorizontalTotal * crtc.VerticalTotal * frek);
   vclk := get_closest_pixel_clock(mode, vclk);
   end else vclk:=0;


if (vclk <> 0) then
   begin
   f0 := vclk / (crtc.HorizontalTotal * crtc.VerticalTotal);
   c:=round(f0+0.5);
   crtc.PixelClock  := vclk;
   crtc.RefreshRate := frek * 100;
   {$IFDEF FPC}
   long:=Global_DOS_alloc(sizeOf(CRTC_info));
   w:=Hi(long);
   dosmemput(w,0,crtc, sizeof(CRTC_info));
   Regs.eax:=$4F02;
   Regs.di := 0;
   Regs.es := w;
   Regs.ebx:=mode or $0800;
   RealIntr($10, Regs);
   Global_DOS_free(Lo(long));
   {$ELSE}
   segm:=seg(crtc);
   ofsm:=ofs(crtc);
   asm  
   push es;push si
   mov ax,4f02h
   mov bx,segm
   mov es,bx
   mov di,ofsm
   mov bx,mode
   or mode,800h
   int 10h
   pop si; pop es
   end;
   {$ENDIF}
   end else


   begin
   { Muzeme nechat spolecne pro TP i FP }
   asm
   mov ax,4f02h
   mov bx,mode
   int 10h
   end;
   end;
end;




Procedure Test;
begin
{$IFDEF FPC}
OutPortb($3C8,0);
OutPortb($3C9,63);
OutPortb($3C9,63);
OutPortb($3C9,63);
{$ELSE}
Port[$3C8]:=0;
Port[$3C9]:=63;
Port[$3C9]:=63;
Port[$3C9]:=63;
{$ENDIF}
readln;
end;


Procedure Zpatky;assembler;
asm
mov ax,3; int 10h;
end;


var sirka,vyska:longint;
begin
writeln('Napred zkusime nastavit 800x600 8bit na 55Hz');readln;
sirka:=800;
vyska:=600;
Nastav_grafiku(sirka,vyska,8,0); { sirka, vyska, bpp, frekvence }
Test;
Zpatky;
writeln('Ted 800x600 8bit na 100Hz. Je to lepsi?');readln;
Nastav_grafiku(sirka,vyska,8,100); { sirka, vyska, bpp, frekvence }
Test;
Zpatky;
end.  
2006-11-30 | Laaca
Datum: 8.8.2007 13:51
Od: Mircosoft
Titulek: Pozor!
Výstupní buffer funkce $4F01 (info o režimu) někdy přepíše výstupní buffer funkce $4F00 (info o vese). Jestli chceme obě tabulky používat současně, musíme tu první ručně zkopírovat někam do bezpečí.
Reklamy: