0395 - Rozszerzajac CRT

W rubryce "Dla praktyków" padło już wiele propozycji rozwiązań zastępujących funkcjonalnie standardowy moduł CRT. Nie będę zatem prezentować podejścia do tego problemu. Przedstawię natomiast coś, co może stanowić bardzo przyjemny dodatek do wspomnianych prac - kilka procedur i funkcji niskiego poziomu znacznie rozszerzających możliwości zarówno pascalowskiego modułu CRT, jak i modułów zastępujących go. Przy okazji powiem kilka słów na temat szybkości działania programów w Pascalu, wykorzystania wbudowanego w kompilator asemblera i różnych zalet stąd płynących.

Jeżeli chcemy stworzyć jakiekolwiek procedury obsługi ekranu w trybie tekstowym, mamy do wyboru: skorzystać z funkcji udostępnianych przez BIOS i system operacyjny za pośrednictwem przerwań lub spróbować samodzielnie poprzestawiać coś bezpośrednio w pamięci ekranu. Pierwsze z rozwiązań ma co najmniej dwie wady - przede wszystkim funkcje dostępne poprzez przerwania nie oferują zbyt wielu możliwości, a poza tym często wykonują się niezwykle wolno (szczególnie w trybie chronionym procesora). Bezpośredni dostęp do pamięci ekranu nie zawsze jest znacząco szybszy, pozwala jednak na tworzenie właściwie dowolnych procedur zarządzania ekranem, ograniczonych jedynie inwencją programisty. Moje rozwiązanie opiera się właśnie na ingerencji w pamięć ekranu, wykorzystuje jednak w niektórych momentach przerwania.

Aby móc robić cokolwiek z pamięcią ekranu, trzeba znać jej strukturę i wiedzieć, jak się do niej dostać. Pamięć tę możemy traktować jako tablicę rekordów. Każdy rekord składa się z dwóch pól - znaku i odpowiadającego mu atrybutu. Zdefiniowany w module tScreenChar jest takim właśnie rekordem, a typ tScreen odzwierciedla w pełni strukturę pamięci ekranu. Zmienna Screen jest wskaźnikiem do tak zdefiniowanej tablicy. Należy ją jeszcze odpowiednio zakotwiczyć, czyli przypisać jej adres początku pamięci ekranu. Wiadomo, że w trybach tekstowych nr 2 i 3 adres ten to B800:0000h, a w trybie nr. 7 (tryb monochromatyczny) B000:0000h. Innych trybów nie rozpatrujemy, gdyż tylko te trzy umożliwiają nam wyświetlenie 80 znaków w 25 wierszach. Do detekcji aktualnego trybu graficznego użyjemy funkcji VideoMode, która zwraca wartość przechowywaną w komórce 0040:0049h. Nie można tutaj posłużyć się zmienną typu absolute, ponieważ mogłyby wtedy pojawić się trudności w trybie chronionym (z tego powodu występują identyfikatory SegXXXX). Wszystkie te zadania wykonuje procedura InitExtCrt. Jest ona wywoływana automatycznie w części inicjalizacyjnej modułu, dlatego nie ma potrzeby używania jej bezpośrednio w programie (chyba że dokonaliśmy zmiany bieżącego trybu pracy karty graficznej). Skoro mamy już zdefiniowany wskaźnik do pamięci ekranu, możemy operować na jej elementach bezpośrednio z poziomu Pascala. Dostęp do znaku w punkcie P(x,y) uzyskujemy, pisząc Screen^[y,x].Char. W module nie ma właściwie funkcji pascalowskich, dlatego tak zdefiniowany wskaźnik nie jest potrzebny (wystarczyłby zwykły typ Pointer), ale może przydać się w programie korzystającym z usług naszego modułu, zatem warto było go stworzyć.

Poszczególne procedury i funkcje modułu wykonują następujące zadania :

  • Frame - rysuje ramkę o zadanych wymiarach i grubości (single lub double),
  • WriteXY - wypisuje ciąg znaków Str począwszy od punktu (x,y),
  • ReadChar - zwraca znak znajdujący się na ekranie w punkcie (x,y),
  • ReadAttr - zwraca atrybut znaku znajdującego się w punkcie (x,y),
  • VideoMode - zwraca numer aktualnego trybu pracy karty graficznej,
  • ClrScr - czyści ekran, ustawia kursor w punkcie (1,1),
  • ScrFillRect - wypełnia prostokątny fragment ekranu znakiem c,
  • ScrFillAttrRect - wypełnia prostokątny fragment ekranu atrybutem a,
  • ScrGetRect - zapamiętuje na stercie prostokątny fragment ekranu i zwraca wskaźnik do tak utworzonego bufora.
  • ScrPutRect - kopiuje na ekran prostokąt zapamiętany wcześniej za pomocą funkcji ScrGetRect i zwalnia przydzieloną pamięć.

Moduł zawiera także podstawowe procedury obsługi drukarki :

  • PrintScreen - jej działanie daje taki sam efekt, jak naciśnięcie klawisza PrintScreen,
  • PrnReset - wykonuje reset drukarki i zwraca jej status,
  • PrnGetState - zwraca status drukarki,
  • PrnPrintStr - drukuje łańcuch znaków str, umożliwia przesyłanie do drukarki kodów sterujących (stałe PrnXXXX),
  • PrnPrintRect - drukuje prostokątny fragment ekranu, przesuwając wydruk do kolumny określonej argumentem AtPosition licząc od lewego marginesu (w przypadku niektórych drukarek trzeba będzie zmienić kody sterujące odpowiedzialne za przesunięcie poziome wydruku).

Znajdujące się w oddzielnym pliku deklaracje stałych mają charakter pomocniczy. Najważniejsze spośród nich to stałe kodów sterujących drukarki. Są one zgodne ze standardem Epsona i działają poprawnie na większości popularnych drukarek (były tworzone dla drukarki Star LC-100) bez potrzeby wprowadzania znaczących zmian. Gdy ktoś używa drukarki akceptującej inne kody sterujące, będzie musiał znaleźć je w instrukcji obsługi i wpisać w miejsce poprzednich.

Celowo zamieściłem na wydruku tylko mały fragment swego pełnego rozwiązania, które określam nazwą ExtendedCrt. Opuściłem inne procedury niskiego poziomu, np. tworzące i obsługujące proste menu, gdyż ich opis wykraczałby poza ramy tego artykułu. Nie podałem również procedur zastępujących działanie standardowych mechanizmów modułu Crt - były one już prezentowane na łamach PCKuriera.

Podstawową wadą mojego rozwiązania jest ograniczenie się tylko do trybów tekstowych 80 znaków w 25 wierszach. Jest to świadomy wybór związany z szybkością działania. Wiadomo bowiem, że im dana procedura jest bardziej uniwersalna, im więcej rzeczy musi sprawdzać i przeliczać w zależności od okoliczności, tym jest wolniejsza. Przerobienie wszystkich funkcji tak, by pracowały również w innych trybach, nie jest sprawą trudną i z pewnością upora się z tym każdy, komu taka przeróbka będzie potrzebna.

Wszystkie procedury modułu napisałem początkowo w Pascalu, ale ze względu na to, że są one odpowiedzialne za operacje niskiego poziomu, zdecydowałem się na przepisanie ich w asemblerze. Zysk na szybkości był imponujący - przy niektórych operacjach procedury asemblerowe były ponad cztery i pół raza szybsze, generując przy tym znacznie krótszy kod. Nie zamierzam w tym miejscu namawiać wszystkich piszących w Pascalu do przejścia na asembler, ale jeśli zamierzamy stworzyć wydajne procedury niskiego poziomu, to czasami po prostu nie ma innego wyjścia.

Pojawił się w tym miejscu ważny problem - szybkość pracy programów. Byłem ciekaw, na ile zapisanie pewnych operacji w asemblerze przyspieszy ich wykonywanie. W tym celu pozostawiłem sobie starszą wersję mojego modułu, zawierającego funkcje w Pascalu, i porównałem ją z nową, asemblerową wersją. Napisałem specjalny program, który wykonywał kolejne operacje ekranowe w pętlach for po kilka i kilkadziesiąt tysięcy razy, mierząc przy tym czas. Zapisywałem wyniki testu dla jednego modułu i porównałem z wynikami dla drugiego. Co się okazało ? Zgodnie z moimi przewidywaniami asembler okazał się znacznie szybszy.

  • Procedury ScrFillRect i ScrFillAttrRect napisane w Pascalu za pomocą podwójnej pętli for :
     Procedure ScrFillRect(Const x1,y1,x2,y2: Byte; Const C: Char);
     Var x,y : byte;
     Begin
       For Y := Y1 To Y2 Do
         For X := X1 To X2 Do Screen^[Y,X].Char := Byte(C);
       {}
     End;
    wykonywały się przez 49,22 sekundy, zaś te same procedury napisane w asemblerze - przez 12,80 sekundy, czyli 3,84 raza szybciej.
  • Dla procedur ScrGetRect i ScrPutRect napisanych w Pascalu w podobny sposób, również za pomocą pętli for, przesyłających jednak całe słowa, a nie bajty, pomiar szybkości dał wynik 79,91 sekundy. Te same procedury napisane w asemblerze wykonywały się przez 13.84 sekundy, a więc 5,75 raza szybciej.
  • Procedura Frame w Pascalu rysowała ramki przez 30,16 sekundy, a napisana w asemblerze przez 9,28 sekundy (3,25 raza szybciej).

Wyniki tak przeprowadzonego testu nie są jednak w pełni obiektywne : żaden program nie wykonuje przecież jednej i tej samej operacji w kółko tysiące razy. Poza tym okazało się , że wyniki te znacząco różnią się od siebie w zależności od użytego do testu sprzętu. Wpadłem więc na inny pomysł. Napisałem program, który symulował działanie rzeczywistej aplikacji, tzn. rysował różne okna, wypełniał je wzorkami i tekstem, przesuwał po ekranie w różnych kierunkach (wykorzystując funkcje buforowania obrazu) i oczywiście mierzył czas całej tej zabawy. Tutaj okazało się, że rzeczywisty zysk na szybkości nie jest aż tak wielki, aczkolwiek dostrzegalny gołym okiem. Wynosił on, w zależności od sprzętu, od 3,02 do 3,56 raza. Jest to naprawdę dużo - trzykrotnie szybciej działający program daje przecież takie złudzenie, jakbyśmy zwiększyli częstotliwość taktowania zegara w naszym komputerze powiedzmy z 33 Mhz do 100 Mhz.

Kolejną zaletą prezentowanego modułu jest to, że nie korzysta on z żadnych modułów zewnętrznych, dołączanych poprzez "Uses". W gotowym programie również nie musimy tego robić - mamy przecież do dyspozycji wszystkie mechanizmy potrzebne do zrobienia krótkiego, szybkiego programu. Unikanie dołączenia bibliotek znacznie skraca kod wynikowy, nie jest więc sprawą drugorzędną.

Przyszła pora na wyjaśnienie tajników technicznej realizacji poszczególnych procedur. Starałem się nie używać żadnych sztuczek czy niezrozumiałych konstrukcji, zatem ci, którzy znają choć trochę asembler, nie powinni mieć z niczym problemów. Wszystkie procedury, mające za zadanie zapisać coś na ekran bądź coś z niego odczytać począwszy od danego punktu (x,y), muszą najpierw obliczyć adres tego punktu. Jest to adres elementu tablicy, jednak znający przekład programów pascalowskich na asembler zauważą, że zastosowany tutaj sposób różni się nieco od tego, jaki generuje kompilator Pascala. Ten jest o kilka taktów procesora szybszy, kosztem mniejszej uniwersalności stosowania (w tym przypadku nie gra to roli). Adres początku pamięci ekranu ładowany jest do rejestrów ES:DI (w jednym przypadku do DS:SI), następnie obliczane jest przesuniecie względem początku (ze wzoru [(y-1)*80+x-1]*2) i dodawane do rejestru DI. W funkcjach ScrFillRect i ScrFillAttrRect do rejestru DX wpisuje się obliczoną liczbę wierszy do wypełnienia, a do rejestru CX liczbę elementów w każdym wierszu (jest ona zapamiętywana w BX, aby nie liczyć jej za każdym razem od początku). Następnie w AL umieszczana jest wartość, którą będziemy wypełniać zadany obszar, ustalany jest kierunek wypełniania (CLD) i za pomocą rozkazów operacji na łańcuchach wypełniany jest co drugi bajt w wierszu. Następnie zmniejsza się licznik wierszy (DX), odtwarza licznik elementów w wierszu (CX, pamiętane w BX), w prosty sposób oblicza adres pierwszego elementu w następnym wierszu (DI) i powtarza całą zabawę z wypełnianiem tak długo, aż licznik wierszy (DX) osiągnie zero. W innych procedurach wygląda to podobnie. W ScrGetRect obliczany jest najpierw rozmiar w bajtach danego obszaru na ekranie, następnie przydzielana jest pamięć za pomocą GetMem (duża czekolada dla tego, kto powie mi, jak wywołać GetMem bezpośrednio z kodu asemblerowego). Sprawdzamy teraz, czy aby nasz wskaźnik nie ma wartości nil; tak jak w innych procedurach liczymy adres pierwszego elementu bloku i wszystkie inne dane: liczbę wierszy w DX, elementów w wierszu w CX (pamiętana w BX) i przesyłamy kolejno całe słowa do bufora. W procedurze ScrPutRect wszystko działa w odwrotnym kierunku, dane wędrują z bufora na ekran, na końcu obliczany jest rozmiar bloku i zwalniana jest pamięć (FreeMem). Ciekawa jest procedura Frame. Wędruje ona kolejno po adresach, najpierw zwiększając je, rysuje górny i prawy bok ramki, następnie, zmniejszając, rysuje dolny i lewy bok. WriteXY to nic innego, jak napisane razem GotoXY i PrintStr, nie wymaga więc omówienia. Procedury obsługi drukarki są równie proste. PrnPrintStr ładuje najpierw do CX pierwszy bajt łańcucha, czyli jego długość, a następnie przesyła kolejne bajty do drukarki poprzez funkcję 0 przerwania 17h.

Tu pojawia się problem. Wszystkie te procedury wykonują operacje niskiego poziomu, aby zatem przyśpieszyć czas ich wykonywania, ważne było maksymalne ich uproszczenie. Z tego powodu nie sprawdzają one poprawności przekazywanych argumentów. To programista powinien zadbać o to, by np. wartości y były w przedziale [1...25], a wartości x w przedziale [1...80], oraz o to, by wywołania procedur miały logiczny sens, czyli np. by współrzędne lewego górnego wierzchołka prostokąta nie były większe od współrzędnych prawego dolnego. Szczególną uwagę należy zwrócić na operacje buforowania fragmentów ekranu na stercie. Funkcja ScrGetRect wykonuje alokację dokładnie takiej ilości pamięci, jaka jest potrzebna do zapamiętania określonego fragmentu ekranu. Procedura ScrPutRect zwalnia ten obszar, jednak do obliczenia jego rozmiaru wykorzystuje współrzędne danego prostokąta. Z tego powodu należy zwrócić uwagę na to, by obszar wypełniany procedurą ScrPutRect był taki sam, jak obszar wcześniej pobrany poprzez ScrGetRect. Źle dobrany rozmiar powoduje błędy przy alokacji pamięci.

Moduł pracuje poprawnie zarówno w trybie rzeczywistym procesora, jak i w trybie chronionym. Napisany został przy użyciu kompilatora Borland Pascal 7.0. Kompiluje się bez problemów również Turbo Pascalem 7.0. Przy kompilacji wersją 6.0 mogą wystąpić drobne problemy - powinno je rozwiązać usunięcie słowa const z nagłówków procedur i funkcji.

Na koniec dodam kilka słów na temat korzystania z wbudowanego w kompilator asemblera. Daje on bardzo duże możliwości, nawet mniej zaawansowanemu programiście. W wielu przypadkach zwalnia nas z obowiązku pamiętania o wszystkich szczegółach, z jakimi mamy do czynienia przy pisaniu programu od podstaw w asemblerze. Mimo to jest kilka rzeczy, których znajomość w dużym stopniu upraszcza nasze zmagania z tym niełatwym przecież językiem. We wbudowanym asemblerze najczęściej piszemy całe procedury lub funkcje. Najwygodniej jest wtedy opatrzyć taką procedurę czy funkcję słowem kluczowym assembler i pominąć standardowe begin i end. Kompilator generuje wtedy znacznie krótszy kod, właściwie pozbawiony wszelkich, nie zawsze potrzebnych operacji. Nie musimy także w takim przypadku korzystać z predefiniowanego identyfikatora @Result. Jeśli nasza funkcja ma zwracać wynik, to jest on po prostu zawartością odpowiednich rejestrów. I tak np. w przypadku typów Byte, Char, Boolean po wyjściu z funkcji zwracana jest zawartość rejestru AL, dla typów Integer, Word itp, jest to rejestr AX, dla typów Pointer i Longint - para rejestrów DX:AX, zaś dla typów rzeczywistych: Real - rejestry DX:BX:AX, a charakterystycznych dla koprocesora - ST(0) w koprocesorze. Tylko wtedy, gdy wynikiem funkcji jest dana String, musimy posłużyć się identyfikatorem @Result.

Często podczas pisania procedur w asemblerze zastanawiamy się, do jakiego stopnia możemy wykorzystywać poszczególne rejestry, nie powodując konfliktów z innymi elementami programu. Wbudowany asembler jest pod tym względem wyjątkowo przyjazny. Wolno nam robić wszystko z dowolnymi rejestrami oprócz SS, DS, CS SP i BP. Jakakolwiek procedura zmieniająca ich wartość musi po zakończeniu przywrócić stan początkowy.

Kolejna sprawa to optymalizacja kodu procedur asemblerowych. Jest to niezwykle ważny element, szczególnie w przypadku funkcji wykonujących operacje niskiego poziomu, których wywołania występują w programie po kilkadziesiąt i więcej razy. Starajmy się tutaj, o ile to możliwe, unikać pożerających czas mnożeń; tworząc pętlę, wykorzystujmy wbudowane w tym celu mechanizmy (LOOP, REP itp.); używajmy poleceń operujących na łańcuchach (STOS, MOVS, LODS) zamiast tworzyć samemu skomplikowane konstrukcje. Nawet taki drobiazg, jak np. napisanie XOR DH, DH zamiast MOV DH, 0 daje nam zysk w postaci jednego taktu procesora. Warto również czasami prześledzić przekład naszego programu na asembler za pomocą Turbo Debuggera - widać wtedy wyraźnie, co można usprawnić w funkcji pascalowskiej, przepisując ją na asembler. Pisząc program pracujący w trybie chronionym, oprócz unikania częstych odwołań do różnych bloków pamięci, starajmy się ograniczyć korzystnie z funkcji udostępnianych poprzez przerwania. Pracują one dużo wolniej niż inne funkcje w porównaniu z trybem rzeczywistym.

Mam nadzieję, że przedstawione procedury będą atrakcyjnym dodatkiem do wszystkich rozwiązań mających za zadanie zastąpić cieszący się wątpliwym powodzeniem moduł Crt.

 Wydruk 1.

 {*** Extended Crt Unit v 2.1         ***}
 {*** Borland Pascal 7.0 with Objects ***}
 {*** DOS real, DOS protected         ***}
 {*** Piotr Chlebowicz                ***}
 { FRAGMENTY }

 Unit ExtCrt;

 {$A+, B-, D-, G-, L-, Q-, R-, S-, V-, X+, Y-}

 {$IFDEF DPMI}
   {$G+}
   {$C MOVEABLE DEMANDLOAD DISCARDABLE}
 {$ENDIF}

 Interface

 {$I EXTCRT.INC}

 Type pScreenChar = ^tScreenChar;
      tScreenChar = Record
        Char : Byte;
        Attr : Byte;
      End;

      pScreen = ^tScreen;
      tScreen = Array[1..25,1..80] Of tScreenChar;

 Var Screen: pScreen;

 Procedure Frame(Const x1, y1, x2, y2, thickness: Byte);
 Procedure ScrFillAttrRect(Const x1, y1, x2, y2, a: Byte);
 Procedure ScrFillRect(Const x1, y1, x2, y2: Byte; Const C: Char);
 Procedure WriteXY(Const x, y: Byte; Const Str: String);
 Procedure ClrScr;

 Function ReadAttr(Const x, y: Byte): Byte;
 Function ReadChar(Const x, y: Byte): Char;
 Function VideoMode: Byte;
 Function ScrGetRect(Const x1, y1, x2, y2: Byte): Pointer;

 Procedure ScrPutRect(Var Buf: Pointer; Const x1, y1, x2, y2: Byte);
 Procedure PrintScreen; InLine($CD/$05); {int 5h}

 Function PrnReset: Byte;
 Function PrnGetState: Byte;

 Procedure PrnPrintStr(Const Str: String);
 Procedure PrnPrintRect(Const x1, y1, x2, y2, AtPosition: Byte);
 Procedure InitExtCrt;

 Implementation

 Procedure Frame(Const x1, y1, x2, y2, thickness: Byte);
 Type FrameCharacters = Array[0..5] Of Byte;
 Var FrameChr : FrameCharacters;
 Const SingleFrame : FrameCharacters = ( 
         196, 179, 218, 191, 192, 217
       );
       DoubleFrame : FrameCharacters = (
         205, 186, 201, 187, 200, 188
       );
       { Kody ASCII dla ramek }
 Begin
   If Thickness = Single Then FrameChr := SingleFrame
                         Else FrameChr := DoubleFrame;
   Asm 
     LES   DI, Screen
     MOV   AL, y1
     XOR   AH, AH
     DEC   AX
     MOV   DX, 50h
     MUL   DX
     MOV   DL, x1
     ADD   AX, DX
     DEC   AX
     SHL   AX, 1
     ADD   DI, AX
     MOV   AL, BYTE PTR [FrameChr + 2]
     CLD
     STOSB         { Pierwszy narożnik }
     MOV   BL, x2
     XOR   BH, BH
     SUB   BL, x1
     DEC   BX
     MOV   CX, BX
     JCXZ  @@2
     MOV   AL, BYTE PTR [FrameChr]
     @@1:
     INC   DI
     STOSB         { Górny bok }
     LOOP  @@1
     @@2:
     INC   DI
     MOV   AL, BYTE PTR [FrameChr + 3]
     STOSB         { Drugi narożnik }
     MOV   DL, y2
     XOR   DH, DH
     SUB   DL, y1
     DEC   DX
     MOV   CX, DX
     JCXZ  @@4
     MOV   AL, BYTE PTR [FrameChr + 1]
     @@3:
     ADD   DI, 9Fh
     STOSB         { Prawy bok }
     LOOP  @@3
     @@4:
     ADD   DI, 9Fh
     MOV   AL, BYTE PTR [FrameChr + 5]
     STD
     STOSB       { Trzeci narożnik }
     MOV   CX, BX
     JCXZ  @@6
     MOV   AL, BYTE PTR [FrameChr]
     @@5:
     DEC   DI
     STOSB    { Dolny bok ]
     LOOP  @@5
     @@6:
     DEC   DI
     MOV   AL, BYTE PTR [FrameChr + 4]
     STOSB     { Czwarty narożnik }
     MOV   CX, DX
     JCXZ  @@8
     MOV   AL, BYTE PTR [FrameChr + 1]
     @@7:
     SUB   DI, 9Fh
     STOSB       { Lewy bok }
     LOOP  @@7
     @@8:
   End;
 End;

 Procedure ScrFillAttrRect(Const x1, y1, x2,y2, a: Byte); 
 Assembler;
 Asm
   LES   DI, Screen
   MOV   AL, y1
   XOR   AH, AH
   DEC   AX
   MOV   DX, 50h
   MUL   DX
   MOV   DL, x1
   ADD   AX, DX
   DEC   AX
   SHL   AX, 1
   ADD   DI, AX
   MOV   AL, y2
   XOR   AH, AH
   SUB   AL, y1
   INC   AX
   MOV   DX, AX
   MOV   CL, x2
   XOR   CH, CH
   SUB   CL, x1
   INC   CX
   JCXZ  @@2
   MOV   BX, CX
   MOV   AL, a
   XOR   AH, AH
   CLD
   @@1:
   INC   DI
   STOSB
   LOOP  @@1
   DEC   DX
   JZ    @@2
   MOV   CX, BX
   SHL   BX, 1
   SUB   DI, BX
   SHR   BX, 1
   ADD   DI, 0A0h
   JMP   @@1
   @@2:
 End;

 Procedure ScrFillRect(Const x1, y1, x2, y2: Byte; Const c: Char); 
 Assembler;
 Asm
   LES   DI, Screen
   MOV   AL, y1
   XOR   AH, AH
   DEC   AX
   MOV   DX, 50h
   MUL   DX
   MOV   DL, x1
   ADD   AX, DX
   DEC   AX
   SHL   AX, 1
   ADD   DI, AX
   MOV   AL, y2
   XOR   AH, AH
   SUB   AL, y1
   INC   AX
   MOV   DX, AX
   MOV   CL, x2
   XOR   CH, CH
   SUB   CL, x1
   INC   CX
   JCXZ  @@3
   MOV   BX, CX
   MOV   AL, c
   XOR   AH, AH
   CLD
   JMP   @@2
   @@1:
   INC   DI
   @@2:
   STOSB
   LOOP @@1
   DEC  DX
   JZ   @@3
   MOV  CX, BX
   SHL  BX, 1
   SUB  DI, BX
   SHR  BX, 1
   ADD  DI, 0A0h
   JMP  @@1
   @@3:
 End;

 Procedury WriteXY(Const x, y: Byte; Const str: String);
 Assembler;
 Asm
   MOV   AH, 2H   { GotoXY }
   MOV   DL, x
   MOV   DH, y
   DEC   DL
   DEC   DH
   XOR   BH, BH
   INT   10h
   PUSH  DS   { PrintStr }
   LDS   SI, str
   CLD
   LODSB
   XOR   AH, AH
   XCHG  AX, CX
   MOV   AH, 40h
   MOV   BX, 1
   MOV   DX, SI
   INT   21h
   POP   DS
 End;

 Procedure ClrScr; Assembler;
 Asm
   MOV   AH, 600h
   MOV   BH, 07h
   XOR   CX, CX
   MOV   DX, 184Fh
   INT   10h
   MOV   AH, 2H
   XOR   DX, DX
   XOR   BH, BH
   INT   10h
 End;

 Function ReadAttr(Const x, y: Byte): Byte; 
 Assembler;
 Asm
   LES   DI, Screen
   MOV   AL, y
   XOR   AH, AH
   DEC   AX
   MOV   DX, 50h
   MUL   DX
   ADD   AL, x
   DEC   AX
   SHL   AX, 1
   ADD   DI, AX
   MOV   AX, ES:[DI] { Odczyt całego słowa }
   MOV   AL, AH      { zwraca starszy bajt }
 End;

 Function ReadChar(Const x, y: Byte): Char;
 Assembler
 Asm
   LES   DI, Screen
   MOV   AL, y
   XOR   AH, AH
   DEC   AX
   MOV   DX, 50h
   MUL   DX
   ADD   AL, x
   DEC   AX
   SHL   AX, 1
   ADD   DI, AX
   MOV   AX, ES:[DI]
 End;

 Function VideoMode: Byte; Assembler;
 Asm
   MOV   ES, [System.Seg0040]
   MOV   AL, ES:[49h]
 End;

 Function ScrGetRect(Const x1, y1, x2, y2: Byte): Pointer;
 Var Buf : Pointer;
     Size : Word;
 Begin
   Asm
     MOV   AL, y2 {Liczy rozmiar podanego bloku }
     XOR   AH, AH
     SUB   AL, y1
     INC   AX
     MOV   DX, AX
     MOV   AL, x2
     SUB   AL, x1
     INC   AX
     MUL   DX
     SHL   AH, 1
     MOV   Size, AX
   End;
   GetMem(Buf, Size);
   Asm
     PUSH  DS
     LES   DI, Buf
     MOV   AX, ES
     OR    AX, DI
     JZ    @@2 { if buf = nil then exit }
     LDS   SI, Screen
     MOV   AL, y1
     XOR   AH, AH
     DEC   AX
     MOV   DX, 50h
     MUL   DX
     MOV   DL, x1
     ADD   AX, DX
     DEC   AX
     SHL   AX, 1
     ADD   SI, AX
     MOV   AL, y2
     XOR   AH, AH
     SUB   AL, y1
     INC   AX
     MOV   DX, AX
     MOV   CL, x2
     XOR   CH, CH
     SUB   CL, x1
     INC   CX
     JCXZ  @@2
     MOV   BX, CX
     CLD
     @@1:
     REP   MOVSW { przenosi całe słowa }
     DEC   DX
     JZ    @@2
     MOV   CX, BX
     SHL   BX, 1
     SUB   SI, BX
     SHR   BX, 1
     ADD   SI, 0A0h
     JMP   @@1
     @@2:
     POP   DS
   End;
   ScrGetRect := Buf;
 End;

 Procedure ScrPutRect(Var Buf: Pointer; Const x1,y1,x2,y2: Byte);
 Var Size : Word;
 Begin
   Asm
     PUSH  DS
     LES   DI, Screen
     LDS   SI, Buf
     LDS   SI, DS:[SI]
       { argument Buf przekazywany jest jako var, }
       { dlatego musi być podwójne wskazanie }
     MOV   AX, DS
     OR    AX, SI
     JZ    @@2
     MOV   AL, y1
     XOR   AH, AH
     DEC   AX
     MOV   DX, 50h
     MUL   DX
     MOV   DL, x1
     ADD   AX, DX
     DEC   AX
     SHL   AX, 1
     ADD   DI, AX
     MOV   AL, y2
     XOR   AH, AH
     SUB   AL, y1
     INC   AX
     MOV   DX, AX
     MOV   CL, x2
     XOR   CH, CH
     SUB   CL, x1
     INC   CX
     JCXZ  @@2
     MOV   BX, CX
     CLD
     @@1:
     REP   MOVSW  { przenosi całe słowa }
     DEC   DX
     JZ    @@2
     MOV   CX, BX
     SHL   BX, 1
     SUB   DI, BX
     SHR   BX, 1
     ADD   DI, 0A0h
     JMP   @@1
     @@2:
     POP   DS
     MOV   AL, y2 { liczy rozmiar bloku }
     XOR   AH, AH
     SUB   AL, y1
     INC   AX
     MOV   DX, AX
     MOV   AL, x2
     SUB   AL, x1
     INC   AX
     MUL   DX
     SHL   AX, 1
     MOV   Size, AX
   End;
   FreeMem(Buf, Size);
   Buf := Nil;
 End;

 Function PrnReset: Byte; Assembler;
 Asm
   MOV   AH, 1
   XOR   DX, DX
   INT   17h
   MOV   AL, AH { zwraca status drukarki }
 End;

 Function PrnGetState: Byte; Assembler;
 Asm
   MOV   AH, 2
   XOR   DX, DX
   INT   17h
   MOV   AL, AH
 End;

 Procedure PrnPrintStr(Const Str: String); Assembler;
 Asm
   PUSH  DS
   LDS   SI, Str
   CLD
   LODSB  { pobiera długość }
   MOV   CL, AL
   XOR   CH, CH
   JCXZ  @@2
   XOR   DX, DX
   @@1:
   LODSB
   XOR   AH, AH
   INT   17h
   LOOP  @@1
   @@2:
   POP   DS
 End;

 Procedure PrnPrintRect(Const x1,y1,x2,y2,AtPosition: byte);
 Assembler;
 Asm
   PUSH  DS
   LDS   SI, Screen
   MOV   AL, y1
   XOR   AH, AH
   DEC   AX
   MOV   DX, 50h
   MUL   DX
   MOV   DL, x1
   ADD   AX, DX
   DEC   AX
   SHL   AX, 1
   ADD   SI, AX
   MOV   AL, y2
   XOR   AH, AH
   SUB   AL, y1
   INC   AX
   MOV   DX, AX
   MOV   CL, x2
   XOR   CH, CH
   SUB   CL, x1
   INC   CX
   JCXZ  @@3
   MOV   BX, CX
   CLD
   DEC   SI
   @@1:
   PUSH  DX
   XOR   DX, DX
   MOV   AL, 1Bh    { wysyła do drukarki }
   XOR   AH, AH     { kody sterujące dla }
   INT   17h        { przesunięcia w prawo }
   MOV   AL, 66h    { do kolumny AtPosition }
   XOR   AH, AH
   INT   17h        { kody dla Epsona to hex.: }
   XOR   AX, AX     { 1B, 66, 0, AtPosition }
   INT   17h        { dla innej drukarki należy }

   MOV   AL, AtPosition

   XOR   AH, AH     { je zmienić lub usunąć }
   INT   17h        { cały ten blok }
   @@2:
   INC   SI
   LODSB
   XOR   AH, AH
   INT   17h
   LOOP  @@2
   MOV   AL, 0A0h   { 0A0h - Line Feed }
   XOR   AH, AH
   INT   17h
   POP   DX
   DEC   DX
   JZ    @@3
   MOV   CX, BX
   SHL   BX, 1
   SUB   SI, BX
   SHR   BX, 1
   ADD   SI, 0A0h
   JMP   @@1
   @@3:
   POP   DS
 End;

 Procedure Error(Const ExitCode: Word);
 Begin
   Halt(ExitCode);
 End;

 Procedure InitExtCrt; Assembler;
 Asm
   CALL  VideoMode
   CMP   AL, 7
   JNE   @@1
   MOV   DX, [System.SegB000] { jesli tryb 7 }
   JMP   @@3
   @@1:
   CMP   AL, 2
   JE    @@2                  { czy to tryb 2 ? }
   CMP   AL, 3
   JE    @@2                  { a może 3 ? }
   MOV   AL, 3                { jesli nie, to }
   XOR   AH, AH               { ustawia 3 }
   INT   10h
   CALL  VideoMode
   CMP   AL, 3
   JE    @@2
   MOV   AX, 1                { gdy nie można }
   PUSH  AX                   { ustawić trybu 3 }
   CALL  Error                { to wywołuje Error }
   @@2:
   MOV   DX, [System.SegB800]
   @@3:
   XOR   AX, AX
   MOV   WORD PTR [Screen], AX   { ustawia wskaźnik }
   MOV   WORD PTR [Screen+2], DX
 End;
 Begin
   InitExtCrt;
End.

--- * --- * ---

 Wydruk 2.

 { EXTCRT.INC - Deklaracje stałych dla ExtCrt }

 Const { grubosć ramki }
       Single = 1;
       Double = 2;

       { Kody sterujące drukarki (standard Epson) }
       PrnDraft            = #27#120#0;
       PrnNLQ              = #27#120#1;
       PrnSelectNLQFont    = #27#107;
         { wymaga przesłania dodatkowego argumentu }

       PrnStandard         = #27#53#27#70#27#72;
       PrnItalic           = #27#52
       PrnSemiBold         = #27#69
       PrnBold             = #27#71;
       PrnNoItalic         = #27#53;
       PrnNoSemiBold       = #27#70;
       PrnNoBold           = #27#72;
       PrnUnderline        = #27#45#1;
       PrnNoUnderline      = #27#45#0;
       PrnSuperScript      = #27#83#0;
       PrnSubScript        = #27#83#1;
       PrnNoSuperSubScript = #27#84;
       Prn10Cpi            = #27#80;
       Prn12Cpi            = #27#77;
       PrnCondensed        = #15;
       PrnProportional     = #27#112#1;
       PrnNoCondensed      = #18;
       PrnNoProportional   = #27#112#0;
       PrnExpanded         = #27#87#1;
       PrnNoExpanded       = #27#87#0;
       PrnNormalSize       = #27#104#0;
       PrnDoubleSize       = #27#104#1;
       PrnQuadrupleSize    = #27#104#2;
       PrnNormalHeight     = #27#119#0;
       PrnDoubleHeight     = #27#119#1;
       PrnLineFeed         = #10;
       PrnRevLineFeed      = #27#10;
       PrnFeedXLines       = #27#102#1
         { wymaga przesłania dodatkowego argumentu }

       PrnFormFeed                = #12;
       PrnCarriageReturn          = #13;
       PrnBackSpace               = #8;
       PrnDisablePaperOutDetector = #27#56;
       PrnEnablePaperOutDetector  = #27#57;
       PrnOffLine                 = #19;
       PrnOnLine                  = #17;
       PrnBell                    = #7;
       PrnBiDirectional           = #27#85#0;
       PrnUniDirectional          = #27#85#1;
       PrnResetPrinter            = #27#64;

       { kody fontów drukarki }
       FntCourier  = #0;
       FntSanSerif = #1;
       FntOrator1  = #7;
       FntOrator2  = #8;
       FntDraft    = #9;

       { kody statusu drukarki }
       PrnTimeOut  = $01;
       PrnError    = $08;
       PrnSelected = $10;
       PrnPaperOut = $20;
       PrnReady    = $40;
       PrnNotBusy  = $80;

--- * --- * ---

 Wydruk 3.
 Program Demo;

 { Bardzo prosty przykład wykorzystania modułu }
 { ExtCrt. Program rysuje okienko i za pomocą  }
 { funkcji buforowania obrazu umożliwia jego   }
 { przesuwanie po ekranie. Enter kończy pokaz. }

 Uses ExtCrt;

 Var BackGround, Window : Pointer;
     x1, y1, x2, y2 : Byte;
     k : Word;

 Const kbUp    = $4800; { najprostsza obsługa klawiatury }
       kbDown  = $5000; 
       kbLeft  = $4B00;
       kbRight = $4D00;
       kbEnter = $1C0D;

 Function ReadKey: Word; Assembler;
 Asm
   XOR   AH, AH
   INT   16h
 End;

 Procedure Desktop;
 Begin
   ScrFillRect(1,1,80,25,'_'); { rysowanie tła }
   ScrFillAttrRect(1,1,80,25,blue*16+yellow);
   Frame(8,3,24,6,Single);
   ScrFillRect(9,4,23,5,' ');
   WriteXY(10,4,'Okienko 1');
   ScrFillAttrRect(8,3,24,6,green*16+white);
   Frame(59,12,78,19,double);
   ScrFillRect(60,13,77,18,'.');
   WriteXY(63,13,'Okienko 2');
   ScrFillAttrRect(59,12,78,19,black*16+lightred);
   Frame(2,14,65,23,single);
   ScrFillRect(3,15,64,22,'#');
   WriteXY(4,15,'Okienko 3');
   ScrFillAttrRect(2,14,65,23,brown*16+lightgray);
   Frame(35,2,56,12,double);
   ScrFillRect(36,3,55,11,' ');
   WriteXY(37,3,'Okienko 4');
   ScrFillAttrRect(35,2,56,12,magenta*16+lightred);
   Frame(37,5,51,10,double);
   Scr(FillRect(38,6,50,9,'&');
   WriteXY(39,7,'Okienko 5');
   ScrFillAttrRect(37,5,51,10,red*16+lightblue);
   BackGround := ScrGetRect(x1,y1,x2,y2);
     { zapamiętanie tego co pod spodem okienka }
   Frame(x1,y1,x2,y2, double);
     { rysowanie okienka }
   ScrFillRect(x1+1, y1+1, x2-1, y2-1, ' ');
   WriteXY(x1+3, y1+2, 'Okienko');
   WriteXY(x1+3, y1+3, 'testowe');
   ScrFillAttrRect(x1,y1,x2,y2,black*16+magenta);
   ScrFillAttrRect(x1+1,y1+1,x2-1,y2-1,black*16+white);
   WriteXY(66,25,'ENTER - koniec');
 End;

 Procedure MoveLeft;
 Begin
   If x1 = 1 Then Exit;
   Window := ScrGetRect(x1,y1,x2,y2);
   ScrPutRect(BackGround, x1,y1,x2,y2);
   Dec(x1);
   Dec(x2);
   BackGround := ScrGetRect(x1,y1,x2,y2);
   ScrPutRect(Window,x1,y1,x2,y2);
 End;

 Procedure MoveRight;
 Begin
   If x2 = 80 Then Exit;
   Window := ScrGetRect(x1,y1,x2,y2);
   ScrPutRect(BackGround,x1,y1,x2,y2);
   Inc(x1);
   Inc(x2);
   BackGround := ScrGetRect(x1,y1,x2,y2);
   ScrPutRect(Window,x1,y1,x2,y2);
 End;

 Procedure MoveUp;
 Begin
   If y1 = 1 Then Exit;
   Window := ScrGetRect(x1,y1,x2,y2);
   ScrPutRect(BackGround,x1,y1,x2,y2);
   Dec(y1);
   Dec(y2);
   BackGround := ScrGetRect(x1,y1,x2,y2);
   ScrPutRect(Window,x1,y1,x2,y2);
 End;

 Procedure MoveDown;
 Begin
   If y2 = 25 Then Exit;
   Window := ScrGetRect(x1,y1,x2,y2);
   ScrPutRect(BackGround,x1,y1,x2,y2);
   inc(y1);
   inc(y2);
   BackGround := ScrGetRect(x1,y1,x2,y2);
   ScrPutRect(Window,x1,y1,x2,y2);
 End;
 
 Begin
   x1 := 5;
   y1 := 5;
   x2 := 17;
   y2 := 11; (współrzędne początkowe okienka }
   Desktop;
   Repeat
     K := ReadKey;
     Case K Of
       kbLeft : MoveLeft;
       kbRight : MoveRight;
       kbUp : MoveUp;
       kbDown : MoveDown;
     End;
   Until K = kbEnter;
   ScrPutRect(BackGround,x1,y1,x2,y2);
   ClrScr;
 End.
{}

Piotr Chlebowicz, PC Kurier 03/95, 2 Lutego 1995r.