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; asmpush 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,alend; 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; asmpush 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 esend; {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; asmmov ax,4f02h mov bx,rezim int 10hend; Procedure Zpatky;assembler; asmmov ax,3 int 10hend; 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; asmmov ax,4f02h mov bx,rezim int 10hend; Procedure Zpatky;assembler; asmmov 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 asmmov 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,cxend; 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); asmpush 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 esend; {$ENDIF} end else begin { Muzeme nechat spolecne pro TP i FP } asmmov ax,4f02h mov bx,mode int 10hend; 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; asmmov 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
Diskuse
Zjistil jsem, že některé grafárny (konkrétně ta moje) zavoláním funkce $4F01 (info o režimu) nějakým záhadným způsobem přesunou tabulku dostupných režimů (zjištěnou z funkce $4F00), takže původně zjištěný ukazatel už na ni neukazuje! Je nutné ho funkcí $4F00 pokaždé znovu obnovit, jinak jde celá detekce režimů do kytek.
Už vím, čím to je. Tabulka režimů se totiž (někdy) ukládá do návratového bufferu funkce $4F00 a ne na nějaké "bezpečné místo", jak jsem si dříve myslel. Pokud si ho něčím přepíšeme (např. návratovým bufferem funkce $4F01), samozřejmě, že o tabulku přijdeme...
> Nový ohlas