3.12.10 Graustufen

Graustufen mit S/W-Display, wie geht das?
Wenn man 2 Schwarzweißbilder hat, in einem sind die "Grauanteile" in dem anderem die "Schwarzanteile", und diese dann mit hoher Geschwindigkeit wechselt, wobei man die "Grauanteile" nur halb so lang anzeigt, wie die Schwarzanteile, dann erhält man einen Graustufen-Effekt, da einmal das Display zu träge ist - sozusagen "nachleuchtet" - und auch das Auge nicht so schnell reagiert...

Wenn man die Farben binär kodiert und eine jeweils dunklere Bildebene immer jeweils doppelt so lange anzeigt, dann klappt es...

Also (am Beispiel):
-------------------
Bild #0: 01010101
Bild #1: 00110011
Bild #2: 00001111
Wie oft muß ein Bild pro Sequenz angezeigt werden?
1xBild #0
2xBild #1
4xBild #2

Wie wählt man am besten die Bildreihenfolge?


möglichst "gemischt", so daß das möglichst nicht mehrmal hintereinander das selbe Bild gezeigt wird...

Z.B.: 0,2,1,2,1,2,2

(Es läßt sich nicht vermeiden, daß das Bild #2 zweimal hintereinander gezeigt werden muß...)

Was zeigt das Display dann?
Was sind das für Bits in den Bildern?


Die "Bits" in den Bildern seien jeweils ein Pixel... Von links nach rechts (0 ist weiß, 1 ist schwarz - wie im Bildspeicher)... Dann haben wir einen glatten Grauübergang: von weiß (links) über 6 weitere Graustufen nach schwarz (rechts).

Man kann diese Verfahren (solange das Umschalten schnell genug ist), soweit betreiben wie man will: 4 Graustufen, 8 Graustufen, 16 Graustufen, 32, 64, 128, 256, 512, 1024, ...., 1048576 Graustufen... 16,7 Millionen Graustufen...

Nur: Das umschalten geht halt nicht SO schnell... (weil das Bild ja auch mindestens einmal vom Display angezeigt werden muß und dieses 70 Bilder pro Sekunde zeigt)

Benutzen des Timer-Interrupts für die GrayLib:


Da das "Umschalten" ständig und immer passieren muß, könnte man eigentlich nicht vernünftige Interaktivität programmieren, besonders, wenn Teilprogramme etwas längere Zeit benötigen...
Ich benutze im Moment den Timer-Interrupt (Int 83h) für das "Umschalten" der Bildebenen... Das hat zwar den Nachteil, daß das Bild nur 40 mal pro Sekunde gewechselt wird (ich kann den Timer leider nicht umprogrammieren - vielleicht gibts ja bald ne Info von Casio...), aber dafür läuft der Bildwechsel vollkommen im Hintergrund, man muß sich nicht mehr darum kümmern...
Da aber das ganze bei 4 Graustufen schon flimmert, und 8 Graustufen noch mehr speicher frißt (von dem RAM von dem wir so schon zu wenig haben), habe ich die Graustufenroutinen erstmal auf 4 Graustufen programmiert...
Wenn ich rausbekomme, wie man die 70Hz oder mehr einstellt, werde ich auch eine 8 Graustufen-Version veröffentlichen...

Leider haben die Graustufen den Nachteil, daß man jetzt vollkommen auf sich alleingestellt ist, also die Grafikroutinen komplett selbst schreiben muß... Und das ist Bit-Fummel-Arbeit, da jedes Pixel im RAM nur 1 Bit belegt und dadurch je 8 Pixel in einem Byte gespeichert werden, was bedeutet, daß man das Byte erst laden dann das "richtige" Bit modifizieren und dann das Byte wieder schreiben muß, um einen einfachen Pixel zu setzten!!!

Folgendes Problem: Da erst ich nicht wußte, daß es so knapp mit dem Arbeitsspeicher wird, hab ich das ganze auf Geschwindigkeit und nicht auf Speicherbenutzung optimiert... Dadurch belegt die GrayLib wohl etwa 17KB RAM und zwar des "near" Speichers!!! (von dem eigentlich nur 20KB da sind...)
Ich werde das aber demnächst reduzieren auf: ca. 14KB, indem ich den "Bildzwischenspeicher" des Betriebssystems mitbenutzen werde... Außerdem werde ich auch versuchen den belegten Speicher in den "far" Bereich zu verschieben... Z.B. indem das Hauptprogramm den Speicher "liefert"

Außerdem benutzt auch die GrayLib "DoubleBuffering", daß heißt: Es wird ein "Zwischenbild" erstmal im normalen Speicher erzeugt und erst wenn es fertig ist, angezeigt... Dann sieht man den Bildaufbau nicht und es Flackert viel weniger!
Aber: Es braucht auch mehr Speicher... (Im Moment 6400 Bytes, nach "Mitbenutzung" des "Bildzwischenspeichers" des Betriebssystems nur noch 3200 Bytes)

Okay, was hat die GrayLib zu bieten?


Wie bau ich die GrayLib ein?
Erstmal ist "GrayLib.c" zu den includes hinzuzufügen...
#include "GrayLib.c"
Und die Dateien GrayLib.c und intproc2.a86 in das Verzeichnis "C" des Projektes zu kopieren.
Außerdem ist die Datei Makefile abzuändern, da wir noch die Datei mit den Assemblerroutinen hinzu-"linken" müssen...

Folgende Zeile ist so abzuändern:

Von dieser Zeile:
-----------------
APLOBJS = $(ODIR).obj

In diese Zeilen:
----------------
APLOBJS = $(ODIR).obj 
$(ODIR)intproc2.obj 

Wobei statt , der jeweilige Name der *.c - Datei ist, die das Hauptprogramm enthält...

setintvec(); / FastIntOn();


Einschalten des automatischen umschalten des Bildschimspeichers durch den Int 83h... (empfohlen)

FastIntOff(); / resetintvec();


Ausschalten des automatischen umschaltens... Und wiederherstellen der Originalzustandes des Interruptvektors - ist dringend davon abzuraten, das Programm zu beenden, ohne resetintvec(); aufzurufen, auch wenn das Betriebssystem in regelmäßigen abständen die Interruptvektoren wieder überschreibt (warum auch immer)...

clrscr();


Löscht den Bildschirmzwischenspeicher...

manualput();


Für manuelles Umschalten des Bildschirmspeichers, wenn man den Interrupt nicht verwenden möchte... (nicht empfohlen)

putscreen();


Schreibt den Bildschirmzwischenspeicher in den Bildschirmspeicher, der dann angezeigt wird... Ist jeweils aufzurufen, wenn man mit dem erzeugen des nächsten Bildes fertig ist, auch wenn man manualput(); verwendet...

putspr(int x, int y, byte far *Sprite);


x,y... linke obere Ecke (zum relativen 0,0 - muß nicht die wirkliche linke, obere Ecke des Bildes sein)
Sprite... Adresse des Sprites, das man darstellen will...
Eine Spriteroutine mit transparenz und clipping, für Sprites die eigentlich so hoch sein können wie sie wollen und im Moment bis zu 96 Pixel breit sein dürfen, damit das Clipping richtig funktioniert... Und sonst bis 160 Pixel, wenn das Bild den Bildschirm nicht weiter als 96 Pixel verläßt... Breitere "Sprites" sollte man einfach vertikal zerschneiden und einzeln anzeigen... Das Spriteformat ist etwas komplizierter und verbraucht bei vielen kleinen durchsichtigen Stellen sehr viel Speicher, während es bei größflächigen "Löchern" Speicher spart... Außerdem ist auch die Spriteroutine auf Geschwindigkeit getrimmt... Es gibt keine Unterschiede im Spriteaufbau für Transparenz und nicht Transparent...

Spriteaufbau:
Die Sprites werden in einzelne waagerechte "Linien" (Blöcke) zerlegt, welche eine Länge >0 und jeweils eine relative x- und y-Koordinate für zu dem "Punkt" haben, der dann später beim Aufruf von putspr angegeben wird (das kann also beispielsweise der Mittelpunkt einer Figur oder aber der Punkt ganz links oben der Grafik sein)... Es werden erst alle "Grauanteile" in Linien und dann alle "Schwarz-/Weißanteile" gespeichert...

1 Word: Anzahl Blöcke
Blöcke:
1 Word - Relative X-Position (Integer)
1 Word - Relative Y-Position (Integer)
1 Byte - Anzahl der "vollständigen" Bytes im Block
1 Byte - Byte mit den "restlichen" Bits
1 Byte - Anzahl der "benutzen" Bits im letzten Byte

Transparenz wird dadurch erreicht, daß eine Zeile eines Sprites einfach aus mehreren Blöcken (oder gar keinem Block) besteht, und dadurch dieser Bereich beim Zeichnen ausgelassen wird...

Anzahl der "vollständigen" Bytes im Block???
Byte mit den "restlichen" Bits???
Anzahl der "benutzen" Bits im letzten Byte???

Da immer 8 Pixel in einem Byte sind, wird hier erstmal gespeichert werden, muß genau festgehalten werden, wieviele Bytes zu verarbeiten sind, und wieviele Pixel im Letzten Byte wirklich drin sind...
Wem das zu kompliziert ist, der sollte einfach den Konverter verwenden, den ich demnächst mit veröffentlichen werde, der aus 8Bit-Graustufen (256 Graustufen) Bildern oder sogar farbigen Bildern 2Bit Graustufen (4 Graustufen) Bilder macht, die putspr(..); dann versteht... Außerdem konvertiert das Programm komplette Animationen, was sehr zeitsparend sein kann... man muß die Bilder nur durchnummerieren...

putpix(int x, int y, int c);


Setzt einen Pixel an der Stelle x,y auf dem Bildschirm (links oben ist 0,0) mit der Farbe c (0 ist weiß, 1 ist hellgrau, 2 ist dunkelgrau, 3 ist schwarz).
auch putpix unterstützt das Clipping... Diesmal aber fast ohne Begrenzung - man sollte aber nicht übertreiben... Ab Koordinaten >5000 oder <-5000 kann es zu Grafikfehlern kommen... (Abstürzen dürfte der PV aber eigentlich nicht)

hline(int x,int y,int l,int c); UND
vline(int x,int y,int l,int c);


Zeichnet eine horizontale (hline) oder vertikale (vline) Linie, angefangen vom Punkt x,y mit der Länge l und der Farbe c.
Koordinaten und Farben wie bei putpix: (0,0) ist links oben, Farben: 0 weiß, 1 hellgrau, 2 dunkelgrau, 3 schwarz!

putplate(int,int,byte far *);


Eine Spriteroutine, die eigentlich nur für PVWar gedacht ist, aber auch in anderen Spielen eingesetzt werden könnte... Sie unterstützt das Clipping nur begrenzt, d.h. nur wenn Teile des Sprites rechts oder links den Bildschirm bis maximal 96 Pixel verlassen, funktioniert es! Wenn es noch weiter außerhalb versucht wird darzustellen, erscheint ein Teil des Sprites auf der "anderen" Seite UND: besonders im oberen und unteren Bereich des Bildschirms passiert es dann, daß andere Speicherbereiche überschrieben werden könnten und dadurch ein Abstürz des PV verursacht wird!!!
(Bei putspr(); ist das nicht so!)

char pic1[5120]; und char pic2[5120];


Das sind die Zwischenspeicher des Bildes für evtl. manuelle Bearbeitung, z.B., wenn man irgendwo einen ganzen Bereich auf eine bestimmte Farbe setzten möchte... Wozu auch immer...

unsigned char counter;
unsigned int counter2;


Die Variable "counter" enthält die Werte 0 bis 3, bzw. welches Bild gerade anzeigt wird - möglichst nicht ändern, da es sonst zu Grafikfehler kommt...

In der Variable "counter2" zählt das Programm von 0 bis 65535 hoch und fängt dann bei 0 wieder an... Diese Variable kann genutzt werden, wenn man das Timing im Programm regulieren will. Z.B.: Eine kurze Pause einbauen will oder die Zeit für ein Spiel messen will (wenn der Spieler meinetwegen 30 Sekunden Zeit hat) etc... Diese Variable kann ruhig gesetzt werden... Es wird dann ab diesem Wert weitergezählt... Das "weiterzählen" erfolgt alle 1/40 Sekunden (40 mal pro Sekunde)

DER PROFILER DER GRAYLIB:
char proc;
unsigned int ptime[32];


Die GrayLib beinhaltet zusätzlich noch einen Profiler!!! D.h. ein kleine Programm, das im Hintergrund die Zeit mißt, die in verschiedenen Routinen "verbraucht" wurde... Damit läßt sich herausfinden, wo der PV länger zu knabbern hat. Man findet so heraus, welche Routinen man optimieren sollte, um das Programm zu beschleunigen... Der Profiler hat leider nur die "Auflösung" von 40 Hz, d.h. er ist relativ ungenau... Außerdem funktioniert er im Moment nur bis etwa 27 Minuten, die ein einzelner zu messender Prozess verbrauchen darf, danach kann man die Werte theoretisch wegwerfen... Das liegt daran, daß der Zähler nur ein "Word" ist und bis maximal 65535 zählt und danach wieder bei 0 anfängt... Es führt aber nicht zu Abstürzen!

ACHTUNG: Der Interrupt (und damit das automatische "Graustufenbild-umschalten") muß aktiviert sein...

Wie benutzt man den Profiler?


=> Interrupt einschalten:
setintvec();
FastIntOn();

=> am Ende des Programms wieder ausschalten:
FastIntOff();
resetintvec();
LibJumpMenu();

ptime[] enthält die Zeit, in 1/40 sekunden, die ein Programmteil verwendet, der die entsprechende "ID-Nummer" zugewiesen bekommen hat...

Nehmen wir an, wir haben 2 Teile, die in einer Funktion sind:


void testfunction() {
proc=1;
/* Hier ist der Programmteil der unter ID 1 gemessen werden soll... */
.
.
.

proc=2;
/* Hier ist der Programmteil der unter ID 2 gemessen werden soll... */
.
.
.

proc=0;
/* Die restliche Zeit soll unter ID 0 gemessen werden... */
.
.
.
}

Nehmen wir an, wir rufen eine Funktion aus verschiedenen anderen zu messenden Programmteilen auf... und wollen diesen Programmteil "gesondert" messen...


z.B.:
void test3() {
.
.
.
}

void test1() {
proc:=1;
/* Hier macht test1(); irgendwas, das unter ID 1 zu messen ist... */
.
.
.
test3(); /* Jetzt wird test3(); aufgerufen, das unter ID 3 zu messen sein soll... */
.
.
.
/* Hier macht test1(); irgendwas, das unter ID 1 zu messen ist... */
proc:=0;
}

void test2() {
proc:=2;
/* Hier macht test2(); irgendwas, das unter ID 2 zu messen ist... */
.
.
.
test3(); /* Jetzt wird test3(); aufgerufen, das unter ID 3 zu messen sein soll... */
.
.
.
/* Hier macht test2(); irgendwas, das unter ID 2 zu messen ist... */
proc:=0;

}



void main() {
setintvec();
FastIntOn();

test1();
test2();
test3();

FastIntOff();
resetintvec();
}
Jetzt müßte man überall, wo:

test3();

steht,

proc:=3;
test3();
proc:=;

hinschreiben... Und dazu müßte man erstmal suchen, wo denn test3(); überall aufgerufen wird...

Einfacher ist es test3(); entsprechen umzuschreiben:
void test3() {
int lproc;
lproc=proc;
.
.
.
proc=lproc;
}


Folgende Routine ist unabdenkbar, wenn man die GrayLib verwendet:

/* event mask defines */

#define EVENT_TCH 1
#define EVENT_CRADLE 4
#define EVENT_BLD1 8

void PollEvent(TCHSTS far* tsts, byte event_mask)
/*----------------------------------------------------------------------------
description : poll on events
thanx to Wittawat Yamwong
parameters : tsts - pointer to touch status struct
event_mask - specifies events to poll
return : %
----------------------------------------------------------------------------*/
{
  union REGS reg;

  reg.x.ax = 0x0200 | event_mask;
  reg.x.di = FP_OFF(tsts);
  reg.x.es = FP_SEG(tsts);
  int86(0x50,®,®);
}

Wie man sieht ist sie von "Wittawat Yamwong" und sie fragt das Touchscreen und andere "Events" ab...
PollEvent funktioniert genauso (soweit ich weiß...), wie die Routine LibTchWait(...); mit dem kleinem aber feinen Unterschied, daß diese Routine nicht Ewigkeiten wartet, und erst nach 500ms das Programm weiterlaufen läßt...

Man kann theoretisch LibTchInit(); etc. verwenden, aber VORSICHT!!!, wenn ihr die HardICONS ("Menu" "Scheduler" und so weiter) aktiviert, dann paßt auf, daß Ihr den Interrupt wieder ausschaltet und zurücksetzt (FastIntOff(); resetintvec() Bevor (!!!) das Menu, der Scheduler etc. aufgerufen werden!!!

Ich bevorzuge aber, PollEvent(...); einfach so zu verwenden, und dann nur die Werte:

tsts.x und tsts.y

zu verwenden, die die Stiftposition zurückliefern... Oder (-1,-1) (funktioniert jedenfalls in PVWar so) wenn der Stift nicht auf dem Touchscreen ist...

Die Verwendung:


Wir benötigen eine Variable vom Typ TCHSTS (ist in stdrom.h definiert):

TCHSTS tsts;

Und rufen dann PollEvent etwa so auf:
PollEvent(&tsts, (EVENT_TCH | EVENT_CRADLE));

Für Ergänzungen wenden Sie sich bitte an: Jürgen Wagner