"Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Diskussion zum Thema Programmierung unter DOS (Intel x86)
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

"Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von zatzen »

Hallo!

Ich habe da eine vielleicht etwas ungewöhnlich Idee.
Es ist vielleicht eher ein Experiment als eine wirklich praktisch sinnvolle Sache - aber das muss man noch überlegen.

Und zwar soll es hier einmal um die Idee gehen, soetwas wie Sprites nicht durch Kopieren von einem Speicherbereich in den anderen letztlich auf den Bildschirm zu bringen, sondern die Bilddaten direkt in ausführbaren Code zu verwandeln, wodurch das einlesen der Daten entfällt. Zudem entfallen jegliche Sprünge innerhalb einer Kopier-Routine, wie auch Vergleiche bzw. Prüfungen, etwa bei Transparenz.

Natürlich würden die Daten somit deutlich aufgebläht.
Wenn ein Pixel einer solchen Grafik nunmehr als Code z.B. durch "mov [di+1234], al" repräsentiert wird, d.h. mit dem Maschinencode " 88 85 d2 04 "dann haben wir hier den 4-fachen Speicherplatzbedarf.
Das ganze relativiert sich allerdings, wenn in der Grafik z.B. vier Pixel der gleichen Farbe nebeneinanderliegen, diese kann man, wenn EAX vorher entsprechend belegt wurde, durch "db 66h; mov [di+1234], ax", d.h. "66 89 85 d2 04" erledigen, somit nur noch der 5/4-fache Speicherplatzbedarf.

Der "Trick" bei der ganzen Angelegenheit wäre, die Grafiken in ihre einzelnen Farben aufzudröseln, und jeweils das AL Register, oder bei 2 oder 4 nebeneinander vorkommenden gleichen Pixeln das AX- resp. EAX-Register entsprechend vorzubereiten.
Als nächstes würden dann entsprechende Pixel per mov ... al, ax, oder eax geschrieben.
Auf diese Weise kann man transparente Bereiche dann auch einfach auslassen.

Ich bin mir gerade nicht sicher, ob man diesen so generierten Code als Daten auf den Heap laden und dort ausführen kann.

Also, das ganze soll eher als Experiment betrachtet werden, da man abwägen muss, ob die vierfache Datengröße wirklich einen überhaupt existierenden Geschwindigkeitsvorteil rechtfertigt.
Möglicherweise bringt diese Vorgehensweise aber auch weitere Ideen mit sich.

Noch etwas zur Positionierung im Puffer oder auf dem Bildschirm: Der generierte Code würde eine feste X-Breite des Puffers oder des Bildschirmspeichers voraussetzen. Dann kann durch Übergabe von DI die x-y Position der linken oberen Ecke festgelegt werden. Hat man etwa eine Breite des Puffers von 320 Pixeln, dann beginnt die erste Zeile der darzustellenden Grafik bei DI+0, die nächste bei DI+320 etc. Ist eigentlich selbsterklärend. Clipping geht so leider erstmal nicht.

Noch zu klären wäre: Sind Speicherzugriffe über Register + Offset wirklich so schnell wie nur über Register?

Und etwas zu mir: Mein bevorzugter Grafikmodus ist MCGA 320x200. Der hat den Nachteil dass er nur eine Seite hat und dass man deshalb im Speicher puffern muss und dann in den Grafikspeicher kopieren. Der echte VGA-Modus ist mir bislang zu kompliziert zu programmieren.
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: "Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von zatzen »

So, update:

Code auf dem Heap aufrufen funktioniert, per FAR CALL über einen Pointer.


Unklar ist weiterhin also nur noch, ob soetwas wie MOV [DI+1234], AL genauso schnell ist wie MOV [DI], AL.
Ich habe bisher aber nichts gefunden was dagegen spricht.


Übrigens an dieser Stelle noch:
Die Art und Weise wie in alten Sierra Adventures die Grafik erzeugt wurde war besonders, zielte aber nicht auf hohe Geschwindigkeit sondern auf Speicher Sparen ab. Und zwar wurde diese so gespeichert, dass sie regelrecht gezeichnet wurde wie in einem Malprogramm. Statt also alles in einzelne Pixel aufzulösen hat man sie vielmehr in Linienbefehle, Füllungen, usw. zerlegt.
mov ax, 13h
int 10h

while vorne_frei do vor;
bboeckmann
Windows 3.11-Benutzer
Beiträge: 8
Registriert: Di 1. Nov 2022, 16:53

Re: "Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von bboeckmann »

zatzen hat geschrieben: So 9. Jul 2023, 15:07 Unklar ist weiterhin also nur noch, ob soetwas wie MOV [DI+1234], AL genauso schnell ist wie MOV [DI], AL.
Ich habe bisher aber nichts gefunden was dagegen spricht.
Hängt von der CPU ab. Das letztere ist auf 8088 definitiv schneller, da das Displacement (+1234) zwei Bytes mehr Maschinencode erzeugt. Auf dem 8088 braucht das laut Intel Handbuch vier Takte mehr (9 gegenüber 5 für die Bestimmung der effektiven Adresse). Insgesamt 22 zu 18 Takte für den gesamten MOV, wenn ich richtig gerechnet habe. Dazu kommt noch, dass die Pixeldaten in AL geladen werden müssen. Das sind auch nochmal vier Takte. Da erscheint mir eine MOV AL,PIXEL : STOSB oder besser MOV AX,ZWEI_PIXEL : STOSW Kombo effektiver, mit vorherigem einmaligen setzen von DI. Dürfte mit vier Byte Maschinencode je 2 Pixel auch dichterer Code sein. Braucht insgesamt 19 Takte pro zwei Pixel auf dem 8088. Das sollte laut Tabelle also tatsächlich etwas schneller sein als REP MOVSW, was mit 25 Takten je zwei Pixel zubuche schlägt (ohne Gewähr). Beim Pentium etc. sieht es wieder anders aus...

Folgende Seite ist hilfreich wegen der aufgelisteten Timings: https://www2.math.uni-wuppertal.de/~fpf ... ode_i.html
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: "Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von zatzen »

Hallo!
Danke Dir, schön dass mal jemand antwortet, und danke für Deine Rechnungen!

Laut meiner bisherigen Recherche ist STOS? langsamer als ein MOV.
Hier geht es ja um Geschwindigkeitsgewinn, daher ziehe ich hier einen MOV Befehl vor, auch wenn dieser mehr Code erzeugt. Konkret soll die ganze Sache nur Anwendung finden in einem Rahmen, wo die Grafikdaten roh vielleicht nur 48 KB brauchen, und so ruhig durch die Code-Wandlung um das vierfache aufgebläht werden dürfen.

Was das beladen des AL oder AX Registers angeht:
Hier bin ich bereits so vorgegangen, dass ich die Grafik Farbe für Farbe durchgehe, und pro Farbe nur einmal AL belade und dann entsprechend oft an entsprechenden Offsets MOV [DI+BX+????], AL ausführe.
AX oder gar EAX (natürlich nur ab 386 - wobei der Sinn sich hier langsam auflöst angesichts der Rechenleistung) habe
ich bisher nur für den Fall genutzt, dass direkt zwei oder eben vier Pixel nebeneinander die gleiche Farbe haben.

Bei näherem Interesse poste ich gerne mal den Code von meinem bisherigen Converter.

Bei einem Test in DosBox lief mein erster Versuch noch mit 50 (!) oder weniger Cycles flüssig mit 50 fps oder mehr.
Ich würde hier stark schätzen, dass das ganze, trotz Tranzparenz zum Hintergrund, schneller läuft als das simple PUT von QuickBASIC, mit dem ich damals ein Spiel programmiert habe, was mir eben zu dieser Überlegung hier Anlass gegeben hat.
mov ax, 13h
int 10h

while vorne_frei do vor;
bboeckmann
Windows 3.11-Benutzer
Beiträge: 8
Registriert: Di 1. Nov 2022, 16:53

Re: "Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von bboeckmann »

Gerne doch :-)

Ich habe nochmal drüber nachgedacht und verstehe jetzt glaube ich besser, was Du vorhast. Die Krux scheint also zu sein, dass die zu setzenden Pixel keinen zusammenhängenden Speicher belegen, da Du bspw. transparente Pixel etc. aussparen möchtest. In dem Fall kann man es natürlich beliebig optimieren :-) Beispielsweise eine Kombination aus einzelnen MOV [DI+1234],AL etc. und REP MOVSD (ab 386, für zusammenhängende Bereiche).

Was ist denn der Grund für das DI im MOV, um die Grafik auf dem Schirm replatzierbar zu machen? Ansonsten könnte man sich das sparen und einfach MOV [1234],AX benutzen. Das ist auf Opi 8088 schneller. DS wird vorab auf das Videosegment gesetzt, nehme ich an?

Es ist auf jeden Fall eine interessante Idee, und wahrscheinlich insbesondere für kleine Grafiken wie 32x32 Pixel Sprites intressant. Anstelle das auf den Heap zu packen besteht auch die Möglichkeit, per Konverter Assemblercode zu generieren, den zu assemblieren und mit als Code in das Programm zu linken. Dann müssen auch die Bilddateien nicht mit verteilt werden. Oder, wenn Du masochistisch veranlagt bist, Bilddateien per Konverter direkt in Code umwandeln und OMF Objektdateien zwecks linken rausschreiben. Aber das OMF Format ist hinreichend komplex, als dass Assembler wohl einfacher ist.

Bei der Geschwindigkeit kommt es krass drauf an auf welcher Hardware es läuft, ob es CPU oder BUS limitiert ist. Beim Pentium bsp. ist fast immer der BUS (im schlechtesten Fall ISA) der Flaschenhals. Beim 8088 fast immer die CPU. Wie hast Du deine Recherche durchgeführt, wenn ich fragen darf? Wenn man da realistische Werte haben möchte, sollte man das auf Metall ermitteln. DosBox und Konsorten sind da nicht so genau.
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: "Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von zatzen »

Hallo!
Ich hänge hier mal den bisherigen Code meines Converters rein:

Code: Alles auswählen

{$I-}

const
  _32bit: boolean = true;
  xdim: word = 16;
  ydim: word = 16;
  scr_xdim: word = 320;


(* OPCODES *)
  _retf: byte = $cb;
  _push_ds: byte = $1e;
  _pop_ds: byte = $1f;

  _mov_ds_dx: word = $da8e;

  _mov_al: byte = $b0; { + 1 BYTE Daten }
  _mov_ax: byte = $b8; { + 1 WORD Daten }
  _mov_eax: word = $b866; { + 1 DWORD Daten }

  _mov_ds_di_plus_xxxxx_AL: word = $8588; { + 1 WORD Daten (Adresse) }
  _mov_ds_di_plus_xxxxx_AX: word = $8589; { + 1 WORD Daten (Adresse) }
  _mov_ds_di_plus_xxxxx_EAX: array[0..2] of byte =
    ($66, $89, $85); { + 1 WORD Daten (Adresse) }

  _mov_al_es_si_plus_xxxxx: array[0..2] of byte =
    ($26, $8a, $84); { + 1 WORD Daten (Adresse) }
  _mov_ax_es_si_plus_xxxxx: array[0..2] of byte =
    ($26, $8b, $84); { + 1 WORD Daten (Adresse) }
  _mov_eax_es_si_plus_xxxxx: array[0..3] of byte =
    ($26, $66, $8b, $84); { + 1 WORD Daten (Adresse) }

  _add_di_cx: word = $cf01;
  _add_si_cx: word = $ce01;


type byte_array = array[0..32767] of byte;

var gra_poi: pointer;
    code_poi: pointer;
    code_data: ^byte_array absolute code_poi;
    gra_data: ^byte_array absolute gra_poi;
    colstat: array[0..255] of word;
    code_pos: word;
    runlen: array[0..3] of byte;
    spr: word;
    in_name, out_name: string;


procedure load_gr(fn: string);
  var f: file;
begin
  assign(f, fn); reset(f, 1);
  if boolean(ioresult) then
  begin writeln('error reading file "', fn, '"'); halt(1); end;
  blockread(f, gra_poi^, xdim * ydim);
  close(f);
end;

procedure make_colstat;
  var i: word;
begin
  fillchar(colstat, 512, 0);
  for i := 0 to pred(xdim * ydim) do inc(colstat[gra_data^[i]]);
end;


procedure writecode_opcode(p: pointer; len: word);
begin move(p^, code_data^[code_pos], len); inc(code_pos, len); end;

procedure writecode_byte(val: byte);
begin code_data^[code_pos] := val; inc(code_pos); end;

procedure writecode_word(val: word);
begin move(val, code_data^[code_pos], 2); inc(code_pos, 2); end;



function maxlen(col: byte): word;
  var x, y, l: word;
begin
  l := 1;
  for y := 0 to pred(ydim) do
    for x := 0 to pred(xdim) do
      if gra_data^[y * xdim + x] = col then
        if (x < pred(xdim)) and (gra_data^[y * xdim + x + 1] = col) then
        begin
          if l = 1 then inc(l);
          if (x < xdim - 3) and _32bit then
            if (gra_data^[y * xdim + x + 2] = col) and
               (gra_data^[y * xdim + x + 3] = col) then l := 4;
        end;
  maxlen := l;
end;



procedure create_code_opaque(col: byte);
  var y, x: word;
  maxln: word;
begin

  maxln := maxlen(col);

  if (maxln = 4) and _32bit then
  begin
    writecode_opcode(@_mov_eax, 2);
    writecode_byte(col);
    writecode_byte(col);
    writecode_byte(col);
    writecode_byte(col);
  end
  else
  if maxln = 2 then
  begin
    writecode_opcode(@_mov_ax, 1);
    writecode_byte(col);
    writecode_byte(col);
  end
  else
  begin
    writecode_opcode(@_mov_al, 1);
    writecode_byte(col);
  end;

  for y := 0 to pred(ydim) do
  begin
    for x := 0 to pred(xdim) do
    begin

      if gra_data^[y * xdim + x] = col then
      begin

        if _32bit and (x < xdim - 3) then
          if (gra_data^[y * xdim + x + 1] = col) and
             (gra_data^[y * xdim + x + 2] = col) and
             (gra_data^[y * xdim + x + 3] = col) then
             begin
               writecode_opcode(@_mov_ds_di_plus_xxxxx_EAX, 3);
               writecode_word(y * scr_xdim + x);
               inc(x, 3);
             end;
        if (gra_data^[y * xdim + x + 1] = col) and (x < pred(xdim)) then
        begin
            writecode_opcode(@_mov_ds_di_plus_xxxxx_AX, 2);
            writecode_word(y * scr_xdim + x);
            inc(x);
        end
        else begin
          writecode_opcode(@_mov_ds_di_plus_xxxxx_AL, 2);
          writecode_word(y * scr_xdim + x);
        end;

      end;

    end;
  end;

end;



procedure create_code_transp(col: byte); { default col: 0 }
  var y, x: word;
begin

  for y := 0 to pred(ydim) do
  begin
    for x := 0 to pred(xdim) do
    begin

      if gra_data^[y * xdim + x] = col then
      begin

        if _32bit and (x < xdim - 3) then
          if (gra_data^[y * xdim + x + 1] = col) and
             (gra_data^[y * xdim + x + 2] = col) and
             (gra_data^[y * xdim + x + 3] = col) then
             begin
               writecode_opcode(@_mov_eax_es_si_plus_xxxxx, 4);
               writecode_word(y * scr_xdim + x);
               writecode_opcode(@_mov_ds_di_plus_xxxxx_EAX, 3);
               writecode_word(y * scr_xdim + x);
               inc(x, 3);
             end;
        if (gra_data^[y * xdim + x + 1] = col) and (x < pred(xdim)) then
        begin
            writecode_opcode(@_mov_ax_es_si_plus_xxxxx, 3);
            writecode_word(y * scr_xdim + x);
            writecode_opcode(@_mov_ds_di_plus_xxxxx_AX, 2);
            writecode_word(y * scr_xdim + x);
            inc(x);
        end
        else begin
          writecode_opcode(@_mov_al_es_si_plus_xxxxx, 3);
          writecode_word(y * scr_xdim + x);
          writecode_opcode(@_mov_ds_di_plus_xxxxx_AL, 2);
          writecode_word(y * scr_xdim + x);
        end;

      end;

    end;
  end;

end;



procedure create_code;
  var c: byte;
begin

  for c := 1 to 255 do
    if boolean(colstat[c]) then create_code_opaque(c);

  create_code_transp(0);

end;


procedure create_code_hdr;
begin
  code_pos := 0;

  writecode_opcode(@_push_ds, 1);
  writecode_opcode(@_mov_ds_dx, 2);
  writecode_opcode(@_add_di_cx, 2);
  writecode_opcode(@_add_si_cx, 2);
end;

procedure create_code_end;
begin
  writecode_opcode(@_pop_ds, 1);
  writecode_opcode(@_retf, 1);
end;


procedure save_code(fn: string);
  var f: file;
begin
  assign(f, fn);
  rewrite(f, 1);
  if boolean(ioresult) then
  begin writeln('error opening ', '"', fn, '"'); halt(2); end;
  blockwrite(f, code_poi^, code_pos);
  close(f);
end;


function str2int(str: string): word;
  var i: word;
begin
  if ord(str[0]) = 1 then
    str2int := ord(str[1])-ord('0');

  if ord(str[0]) = 2 then
    str2int := (ord(str[1])-ord('0')) * 10 + (ord(str[2])-ord('0'));

  if ord(str[0]) = 3 then
    str2int := (ord(str[1])-ord('0')) * 100
      + (ord(str[2])-ord('0')) * 10 + (ord(str[3])-ord('0'));
end;

procedure checkparams;
begin
  if not ((paramcount = 6) or (paramcount = 5)) then
  begin
    writeln('please specify parameters.');
    writeln;
    writeln('example:');
    writeln('gra2code [inputfile] [outputfile] [spr-xdim] [spr-ydim] [scr-xdim] [32bit?]');
    writeln('input file has to be raw 1-byte-per-pixel indexed graphics format.');
    halt(5);
  end;

  in_name := paramstr(1);
  out_name := paramstr(2);
  xdim := str2int(paramstr(3));
  ydim := str2int(paramstr(4));
  scr_xdim := str2int(paramstr(5));
  if paramstr(6) <> '' then _32bit := true else _32bit := false;

end;


begin

  checkparams;

  getmem(code_poi, 32768); { grosszuegig reservieren }
  getmem(gra_poi, xdim * ydim);


  create_code_hdr;

  load_gr(in_name);

  make_colstat;
  create_code;

  create_code_end;

  save_code(out_name);

end.
Der generierte Code wird auf den Heap geladen und per FAR Call aufgerufen.
Vorher wird in DX das Segment des Grafikpuffers übergeben (wird dann in DS kopiert)
aus dem der Hintergrund bei Transparenz restauriert werden soll.
ES wird direkt vor dem Aufruf bestimmt, und ist in diesem Fall $A000. Das könnte man
auch noch im Converter festlegen, ich habe es aber offen gelassen.

Um das ganze zu erklären, wie ich überhaupt darauf gekommen bin:
Als 15jähriger habe ich eine Art "Dungeon-Jump&Run" realisiert, allerdings mit den Bordmitteln
von QuickBasic, und das Grafik-PUT kann keine Transparenz.
Nun wäre es eine Idee, nach all diesen Jahren das Spiel nochmal neu aufzulegen, aber
diesmal eben mit Transparenz und Hintergrundgrafik.
Dabei trifft es sich ganz gut, dass ich damals einen lahmen Trick angewendet habe, um
die Grafik überhaupt schnell zu halten: Alle Sprites hatten einen schwarzen Rahmen,
1 Pixel breit. So haben sie sich ganz von selbst ausradiert...

Und auf diese Weise lässt sich mit dieser Methode hier auch etwas sehr schnelles realisieren,
wenn eben einfach der Hintergrund die Grafik ausradiert. Dabei funktioniert zwar ein einfacher
schwarzer Rahmen nicht, sondern es müsste vielmehr ein "Schatten" in alle acht Richtungen sein,
also links, rechts, hoch, runter, und dann nochmal alles 45 Grad. Das müsste ich noch ausarbeiten,
dürfte aber einfach sein.

Hier kannst Du Dir mein damaliges Spiel mal kurz ansehen: https://youtu.be/wzOFciAlYcY

Was die "Zielplattform" also den Prozessortyp angeht, würde ich auf 286er abzielen.
Daher ist eigentlich die 32 Bit Option in dem Code hinfällig.
386 oder 486 sind wohl fähig genug, dass man da auf diese Weise nicht tricksen muss.
Ein XT könnte dagegen nicht schnell genug sein, um ein komplexeres Spiel nach meinem
Geschmack überhaupt stemmen zu können.
mov ax, 13h
int 10h

while vorne_frei do vor;
DOSferatu
DOS-Übermensch
Beiträge: 1220
Registriert: Di 25. Sep 2007, 12:05
Kontaktdaten:

Re: "Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von DOSferatu »

Hallo, Zatzen!

Ich habe ja schon eine ganze Weile nichts im DOS-Forum geschrieben - einfach, weil es bei mir kaum Neuigkeiten gibt. Das eine meiner Sub-Projekte, das ich als Ausgangsbasis brauche, um ein anderes Sub-Projekt weiterzuführen, ist Anfang Dezember diesen Jahres (2023) endlich fertig geworden. Das andere Sub-Projekt harrt nun seiner Ausführung. (Es geht um die teilweise Automatisierung für Erstellung von Editoren.) - Ein neues "richtiges" Spiel von mir (also nicht sowas Simples wie dieses NumberKiller von neulich) wird wohl noch eine Weile auf sich warten lassen müssen.

Aber nun mal On-Topic:

Ein Hinweis: Die zusätzlichen Zyklen für sog. "Berechnung der effektiven Adresse" mit indirekter Register-Adressierung und/oder Displacements treten nur bei 8086/8088/80186 auf. Ab 286ern hatte man das Problem offenbar gelöst, d.h. ab da fallen keine zusätzlichen Zyklen mehr an, auch wenn man z.B. mov AL,ES:[SI+BX+1234] macht. In der ASM86FAQ.TXT ist das alles aufgelistet, inklusive der Zyklen.

Das ist das, was in der Spalte für 8086 mit "+EA" gemeint ist. Wo +EA steht, bedeutet das: Plus die zusätzlichen Zyklen für die Berechnung der effektiven Adresse (die sich aus den Indexregistern und/oder festem Addierwert ergibt). Diese Zyklenanzahlen-Werte stehen am Anfang (vor den ganzen Befehlen) für die jeweiligen Kombinationen aufgelistet. Aber wie gesagt: Gilt nur für CPUs unterhalb 80286er (also 8086, 8088 und 80186).

Weiter im Text, betreffend die durchsichtigen Sprite-Pixel:
Zum Problem mit dem Weglassen von Pixeln (z.B. für die "durchsichtigen" Bereiche bei Sprites) hatte ich schonmal die Idee, 15 "Farben" zu reservieren:
Das Ganze wäre dann so in der Art wie

Code: Alles auswählen

mov EBX,[wo_die_Spritedaten_stehen]
cmp BL,15
jb @Spezial
Die Daten wären immer 32bit-basiert - aber wenn das erste Byte <15 wäre, würden die anderen 3 Bytes angeben, wohin diese zu setzen sind, bzw. wo die "Löcher" sind. Man braucht ja dann nur max. 3 andere Bytes, weil mindestens EIN Loch ja immer da wäre. Die entsprechende Operation wäre dann mit einem Sprung zu einer von 15 Sub-Routinen zu lösen, die die entsprechenden Bytes an die Stellen setzen, also sowas wie

Code: Alles auswählen

jmp CS:[BX+@JumpTable]
Da müßte man dann BH kurzzeitig woanders hin sichern, damit man das auch noch hat (weil man für den BX-basierten Spring BH auf einem festen Wert braucht, z.B. 0 oder einen Wert, den man dann wieder von @JumpTable abzieht). Und BL muß ja dann *2 sein (also z.B. add BL,BL oder shl BL,1 - wobei ersteres schneller ist) .. - ODER man schreibt BL direkt in den Sprungbefehl, als selbstmodifizierenden Code... In dem Fall wäre es auch egal, ob man EBX oder irgend ein anderes indizier-fähiges Register benutzt (aber bei selbstmodifzierendem Code immer aufpassen wegen des Prefetch-Queue).

Für diese genannten Varianten könnte man dann zwar, wenn KEINE Löcher auftreten, den schnellen 32-Bit-Schreibzugriff nutzen, aber WENN Löcher auftreten, wäre es durch die mehrfachen Sprünge/Rücksprünge an dieser Stelle entsprechend langsamer. (Die 15 "Signal-Farben" könnte man dann übrigens trotzdem in den anderen 3 Bytes nutzen.)

Eine andere Variante - bei der man keine Farbnummer "verschwenden" würde - wäre, alle 8 Bytes ein Masken-Byte einzuschieben, das jeweils für 2 aufeinanderfolgende DWords bitweise angibt, ob Löcher auftreten (hier am besten die Löcher mit 1 markieren, so könnte man besser auf "keine Löcher" (alles 0) reagieren, wenn man will - obwohl es ja durch den Sprung abgedeckt wäre.

Dann eine Sprungtabelle für die 256 Möglichkeiten machen. Die "ohne Löcher" Variante dann am besten gleich direkt hinter den Sprung setzen (oder an den Schleifenanfang!) so spart man sich für diese Variante den Rücksprung.

Allerdings würden an den "leeren" Stellen dann trotzdem Bytes stehen (wäre wieder Verschwendung), aber man könnte diese natürlich "zusammenschieben" und beide Varianten kombinieren: Nur wenn das Byte =0 ist, liest/schreibt man mit 32bit, ansonsten liest man so viele Bytes/Words ein, wie man braucht und schreibt sie entsprechend und liest dann auch gleich wieder das neue Maskenbyte mit ein.

Es gäbe noch eine andere Variante, bei der immer ein Byte z.B. so genutzt werden würde: Untere 4 Bit geben an, wie viele Pixel folgen, obere 4 Bit geben an, wie viel "Abstand" (Loch) danach folgt. Für beides wäre auch 0 gültig, d.h. wenn 23 Pixel folgen wäre das erste Byte $0F, dann folgen die Pixel, dann kommt $x8 (also beim ersten Byte folgt KEIN Loch, beim zweiten kommen dann noch 8 Pixel, (15+8=23) bevor das Loch kommt. Andersrum, wenn das Loch größer als 15 Pixel ist, (z.B. 21 Pixel), würde man beim ersten $Fx setzen (x Pixel, danach das Loch) und danach $60, weil zusätzlich 6er Loch (15+6=21) ohne Pixel davor.

Vielleicht auch zeilenweise am Anfang angeben, wo die "Grenze" ist (also z.B. untere 5 Bit geben das Loch an, obere 3 Bit die Anzahl Pixel, damit man nicht auf 4:4 festgelegt ist) je nachdem, was am Sinnvollsten ist. Auf diese Art würde man wohl einer Methode nahekommen, mit möglichst effektiven Zugriffen zu arbeiten. Wenn bei der wenn (Anzahl_Pixel and 3)<>0 wäre (d.h. nicht durch 4 teilbar) würde man am Anfang Byte-Zugriffe (oder Word, oder 1x Byte, 1x Word, je nachdem wie komplex man es haben will) machen und danach Anzahl_Pixel shr 2 machen und wenn <>0, dann zu der 32bit-Variante springen, die dann die restlichen Pixel als 32bit-Lese/Schreib-Zugriffe ausführt.

Andere Variante: Ebenfalls ein Byte vor die Daten setzen - wenn oberes Bit=0, folgen 1 bis 128 Pixel, wenn oberes Bit=1, folgt Loch mit 1 bis 128 Pixel Breite (oder umgekehrt). Das wäre allerdings wieder ziemliche Verschwendung und eher nur für sehr große Sprites brauchbar, bei kleinen Sprites würde dieses Byte kaum richtig ausgenutzt.

Zur Hardcoded-Idee:
Es gäbe natürlich auch die Variante, feste Sprites "hardcoded" zu pixeln, direkt mit Maschinencode - das wäre zwar schnell, aber eben maximal unflexibel und die Code-Erzeugung dafür auch etwas aufwendiger. Clipping an den Bildrändern wäre so gar nicht möglich - oder wenn, dann mit komplizierterem Code, der den Geschwindigkeitsgewinn wahrscheinlich wieder auffrißt. Außerdem wäre der Speicherverbrauch natürlich entsprechend hoch - denn Code braucht ja auch Speicher - und wenn jedes Sprite durch festen Code erzeugt wird, könnte es sehr viel werden.

Man könnte dabei natürlich auch "hybrid" arbeiten: Nur bestimmte Sprites - z.B. das Spielersprite, das ständig zu sehen ist - hardcoden. Oder kleine Sprites (Schüsse, Projektile) und häufige Sprites (Explosionen) hardcoden und den Rest mit anderen Methoden.

Zwischenbemerkung: Eine Hardcoded-Variante - weiß nicht, ob durch selbstmodifzierendem Code oder festem Code für jedes Sprite - wurde übrigens durch id-Software im Spiel Wolfenstein-3D benutzt. Das hat einer der Johns (Carmack oder Romero) mal irgendwann erwähnt. Soweit ich weiß, soll sogar der entsprechende Programmcode freigegeben worden sein - bekanntlich gibt id-Software immer nach einer gewissen Zeit ihre Programmcodes frei.

Zu bedenken ist aber:
1.) Alle von mir obengenannten Vorgehensweisen erfordern natürlich, daß die Spritedaten entsprechend vorbereitet werden (aber das kann ja ein externes Tool erledigen).

2.) Einfaches X/Y-Spiegeln wäre hier vielleicht noch möglich (obwohl bei X-Spiegeln für den 32-Bit-Schreibzugriff auch noch 3 vorherige Befehle (z.B. sowas wie rol AX,8;rol EAX,16;rol AX,8) nötig wären), aber jede andere Modifikation (skalieren oder drehen - selbst wenn es nur glatte 90 Grad wären) wäre dann sehr viel komplizierter oder würde entweder zusätzliche Spritedaten (für jede Variante) und/oder zusätzlichen Programmcode erfordern.

3.) Außerdem arbeite ich z.B. gern mit verringerter Farbanzahl pro Sprite, weil kaum ein Sprite jemals alle 256 Farben einer Palette gleichzeitig nutzt, also z.B. Sprites mit maximal 15 Farben (+1 Transparenz), für die nur 4 Bit pro Pixel plus eine 16er-LoopUp-Table nötig wäre bzw. eine "Sub-Tabellen-Nummer", die dann auf ein "Array" verweist, wo die 16 wirklichen Farben stehen. Das Sprite kann dann trotzdem alle möglichen Farben haben, aber eben nur 15 gleichzeitig. Solche nützliche und speichersparende Meta-Paletten-Spielerei wäre bei diesen 32bit-Schreibzugriffen zwar auch möglich, aber ebenfalls eher umständlich - denn die Vorbereitung zum Schreiben würde erfordern, daß man aus den von Tabellen gewonnenen Bytes den Wert im 32bit-Register erst wieder aus diesen Bytes "zusammenbaut", nur damit man es dann mit 32bit-Zugriff schreiben kann - und ob DAS dann wirklich noch schneller wäre, glaube ich eher weniger.

4.) Wenn man dann auch noch mit Direktzugriff auf Mode-X (oder anderen Mode-Y-Varianten) arbeiten will, um andere Bildauflösungen und/oder mehr Bildschirmseiten (für Double-/Triple-Buffering) haben will, ginge das wegen der komischen Mode-X-Zugriffe sowieso alles nicht, weil die Pixel bekanntlich nicht direkt nebeneinander liegen - hier müßte man dann definitiv mit einem Puffer arbeiten und den dann nachher in's Bild kopieren.

Aus all diesen Gründen hatte ich mich damals schlußendlich gegen die 32bit-Zugriffe entschieden, weil - zumindest für mich - die vielen Nachteile die wenigen Vorteile überwogen haben.

Anmerkung_1: In meinen Mode-X Routinen werden sowohl Level (kann bis zu vier unabhängig scrollende Ebenen haben) als auch Sprites senkrecht gepixelt und dann nur die um 4 Pixel versetzten Reihen und dann die dazwischenliegenden um den Ebenenwechsel so selten wie möglich zu haben. Das macht aber auch nur Sinn mit Einzelbyte-Zugriffen.)

Anmerkung_2: In der Unit, die ich für das "486 Power" Demo geschrieben habe, nutze ich für die Levelblocks sowohl 32bit-Lese-/Schreib-Zugriffe als auch eine große Unrolled Loop. Hierbei konnte ich aber von mehreren Dingen profitieren:
(a) VESA-Mode mit direkt hintereinanderliegenden Pixeln - und trotzdem 2 Bildschirmseiten für Pageflipping (also Double-Buffering)
(b) Scanline auf 1024 erweitert, somit kein 64kB-Bank-Wechsel innerhalb der Pixelzeile (in RealMode macht nur Banked-Mode Sinn) - eine 64kB-Bank ist hier also genau 64 Zeilen hoch und endet nicht inmitten einer Bildzeile
(c) wegen der erweiterten Scanline auch keine nötige Prüfung auf Bildschirmränder - der Rest landet einfach außerhalb des Sichtfelds
(d) "Scrollen" des möglich, da Bildschirmposition verschiebbar - somit können die Blocks immer innerhalb einer 64kB-VESA-Bank liegen
(e) Ich nutze nur eine Level-Ebene (also keine "Löcher" im Level, durch die anders-scrollender Hintergrund durchscheint), also keine Prüfung der Pixel nötig
Anmerkung_3: Also: Für 1-Ebenen-Levels (also Blocks ohne "Löcher") finde ich 32-Bit-Zugriffe durchaus sinnvoll - selbst für Mode-X wäre das hier möglich, dazu müßte man nur die Pixel-Anordnung der Blocks entsprechend anders vorbereiten. Aber für Sprites mit ihren nicht-rechteckigen Formen (und "Löchern") ist es meiner Meinung nach nur begrenzt sinnvoll.


Schlußbemerkung:
Ich will damit nicht sagen, daß ich die Idee der 32bit-Zugriffe für Sprites generell schlecht fände. Ich bin ja bekanntlich immer dafür, so viel wie möglich Performance aus den Systemen rauszuholen. Und wenn es eine Methode gibt, bei der ich entweder Rechenzeit oder Speicher oder bestenfalls sogar beides sparen kann (auch wenn es dafür etwas mehr Mühe beim Programmieren erfordert), bin ich fast immer bereit, diesen Weg zu gehen.

Aber durch persönliche Erfahrungen, die ich über Jahrzehnte bei der Spieleprogrammierung bzw. Grafikprogrammierung allgemein gesammelt habe, ist mir auch aufgefallen, daß ich mitunter durch einfachere Methoden am Ende meßbar bessere Ergebnisse erzielt habe als durch kompliziertere. Wenn die komplizierteren Methoden am Ende gleichzeitig mehr Speicher und mehr Rechenzeit erfordern, finde ich es zwar schade um den aufwendigen Programmcode, aber letztendlich nutze ich ihn dann nicht, wenn er mir keine Vorteile bringt.

Ach ja, noch etwas:
Ich wünsche Dir und auch den anderen Forenmitgliedern, die hier noch mitlesen, ein schönes und erfolgreiches Neues Jahr 2024! - (Und mir selbst, daß ich im Neuen Jahr vielleicht mal wieder etwas Interessantes oder Nützliches zum Forum beizutragen habe.)
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: "Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von zatzen »

Hallo DOSferatu! Schön, wieder von Dir zu Lesen! Ich wünsche Dir auch ein frohes neues Jahr!

Ich war jetzt drauf und dran meinen "Hardcoder" zu erweitern um eine Assembler-Code Text-Ausgabe, so zum Überprüfen und hier als Veranschaulichung was genau passiert, aber irgendwie ist mir da ein Fehler unterlaufen und ich habe gerade nicht die Nerven, um das zu debuggen...
Also etwas später...

Bis dann
Zatzen
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: "Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von zatzen »

So, getestet in Form von Aufrufen des Codes und somit erzeugen des Bildes hab ichs jetzt noch nicht, aber logisch nachvollzogen müsste es stimmen. Es hatte allerdings bereits tadellos funktioniert, bevor ich die ASM-Textausgabe reingebracht habe.

Ich habe diese 16x16 Pixel Grafik:
km2_0.png
km2_0.png (1.09 KiB) 1808 mal betrachtet
Im Hex-Editor stellt sich die Grafik so dar:

Code: Alles auswählen

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000010  00 00 00 00 00 00 00 9C 97 94 9E 9B 97 95 00 00  .......œ—”ž›—•..
00000020  00 00 00 00 9D 98 95 95 95 96 94 97 9B 9E 00 00  .....˜•••–”—›ž..
00000030  00 00 00 9C 95 96 96 94 95 97 98 97 98 96 95 00  ...œ•––”•—˜—˜–•.
00000040  00 00 9C 98 98 97 95 95 95 94 94 95 97 9B 00 00  ..œ˜˜—•••””•—›..
00000050  00 9D 98 95 95 94 94 95 95 95 96 97 98 98 01 00  ..˜••””•••–—˜˜..
00000060  00 9B 95 70 93 95 94 94 94 94 95 96 96 98 9C 00  .›•p“•””””•––˜œ.
00000070  00 98 93 00 70 93 92 92 93 93 93 95 97 98 9A 00  .˜“.p“’’“““•—˜š.
00000080  00 98 97 93 94 92 91 91 92 92 93 95 96 98 99 00  .˜—“”’‘‘’’“•–˜™.
00000090  00 98 97 95 94 92 91 90 91 93 94 95 96 98 99 00  .˜—•”’‘.‘“”•–˜™.
000000A0  00 98 96 96 94 93 92 91 91 92 94 95 96 98 9A 00  .˜––”“’‘‘’”•–˜š.
000000B0  00 9A 98 98 96 94 92 92 93 94 95 96 96 98 9B 00  .š˜˜–”’’“”•––˜›.
000000C0  00 9D 97 97 95 95 95 94 94 94 95 97 98 99 9E 00  ..——•••”””•—˜™ž.
000000D0  00 00 9B F2 97 96 96 96 96 96 97 98 99 9C 00 00  ..›ò—–––––—˜™œ..
000000E0  00 00 01 9B 9A 99 99 98 99 99 9A 9A 9C 00 00 00  ...›š™™˜™™ššœ...
000000F0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
Der Converter erzeugt diesen Code daraus (und den natürlich auch als Maschinencode):

Code: Alles auswählen

{ setup: }
push ds
mov ds, dx
add di, cx
add si, cx

{ opaque pixels: }
mov al, 001h
mov ds:[di + 01614], al
mov ds:[di + 04482], al
mov al, 070h
mov ds:[di + 01923], al
mov ds:[di + 02244], al
mov al, 090h
mov ds:[di + 02887], al
mov ax, 09191h
mov ds:[di + 02566], ax
mov ds:[di + 02886], al
mov ds:[di + 02888], al
mov ds:[di + 03207], ax
mov ax, 09292h
mov ds:[di + 02246], ax
mov ds:[di + 02565], al
mov ds:[di + 02568], ax
mov ds:[di + 02885], al
mov ds:[di + 03206], al
mov ds:[di + 03209], al
mov ds:[di + 03526], ax
mov ax, 09393h
mov ds:[di + 01924], al
mov ds:[di + 02242], al
mov ds:[di + 02245], al
mov ds:[di + 02248], ax
mov ds:[di + 02250], al
mov ds:[di + 02563], al
mov ds:[di + 02570], al
mov ds:[di + 02889], al
mov ds:[di + 03205], al
mov ds:[di + 03528], al
mov eax, 094949494h
mov ds:[di + 00329], al
mov ds:[di + 00650], al
mov ds:[di + 00967], al
mov ds:[di + 01289], ax
mov ds:[di + 01605], ax
mov ds:[di + 01926], eax
mov ds:[di + 02564], al
mov ds:[di + 02884], al
mov ds:[di + 02890], al
mov ds:[di + 03204], al
mov ds:[di + 03210], al
mov ds:[di + 03525], al
mov ds:[di + 03529], al
mov ds:[di + 03847], ax
mov ds:[di + 03849], al
mov ax, 09595h
mov ds:[di + 00333], al
mov ds:[di + 00646], ax
mov ds:[di + 00648], al
mov ds:[di + 00964], al
mov ds:[di + 00968], al
mov ds:[di + 00974], al
mov ds:[di + 01286], ax
mov ds:[di + 01288], al
mov ds:[di + 01291], al
mov ds:[di + 01603], ax
mov ds:[di + 01607], ax
mov ds:[di + 01609], al
mov ds:[di + 01922], al
mov ds:[di + 01925], al
mov ds:[di + 01930], al
mov ds:[di + 02251], al
mov ds:[di + 02571], al
mov ds:[di + 02883], al
mov ds:[di + 02891], al
mov ds:[di + 03211], al
mov ds:[di + 03530], al
mov ds:[di + 03844], ax
mov ds:[di + 03846], al
mov ds:[di + 03850], al
mov eax, 096969696h
mov ds:[di + 00649], al
mov ds:[di + 00965], ax
mov ds:[di + 00973], al
mov ds:[di + 01610], al
mov ds:[di + 01931], ax
mov ds:[di + 02572], al
mov ds:[di + 02892], al
mov ds:[di + 03202], ax
mov ds:[di + 03212], al
mov ds:[di + 03524], al
mov ds:[di + 03531], ax
mov ds:[di + 04165], eax
mov ds:[di + 04169], al
mov ax, 09797h
mov ds:[di + 00328], al
mov ds:[di + 00332], al
mov ds:[di + 00651], al
mov ds:[di + 00969], al
mov ds:[di + 00971], al
mov ds:[di + 01285], al
mov ds:[di + 01292], al
mov ds:[di + 01611], al
mov ds:[di + 02252], al
mov ds:[di + 02562], al
mov ds:[di + 02882], al
mov ds:[di + 03842], ax
mov ds:[di + 03851], al
mov ds:[di + 04164], al
mov ds:[di + 04170], al
mov ax, 09898h
mov ds:[di + 00645], al
mov ds:[di + 00970], al
mov ds:[di + 00972], al
mov ds:[di + 01283], ax
mov ds:[di + 01602], al
mov ds:[di + 01612], ax
mov ds:[di + 01933], al
mov ds:[di + 02241], al
mov ds:[di + 02253], al
mov ds:[di + 02561], al
mov ds:[di + 02573], al
mov ds:[di + 02881], al
mov ds:[di + 02893], al
mov ds:[di + 03201], al
mov ds:[di + 03213], al
mov ds:[di + 03522], ax
mov ds:[di + 03533], al
mov ds:[di + 03852], al
mov ds:[di + 04171], al
mov ds:[di + 04487], al
mov ax, 09999h
mov ds:[di + 02574], al
mov ds:[di + 02894], al
mov ds:[di + 03853], al
mov ds:[di + 04172], al
mov ds:[di + 04485], ax
mov ds:[di + 04488], ax
mov ax, 09A9Ah
mov ds:[di + 02254], al
mov ds:[di + 03214], al
mov ds:[di + 03521], al
mov ds:[di + 04484], al
mov ds:[di + 04490], ax
mov al, 09Bh
mov ds:[di + 00331], al
mov ds:[di + 00652], al
mov ds:[di + 01293], al
mov ds:[di + 01921], al
mov ds:[di + 03534], al
mov ds:[di + 04162], al
mov ds:[di + 04483], al
mov al, 09Ch
mov ds:[di + 00327], al
mov ds:[di + 00963], al
mov ds:[di + 01282], al
mov ds:[di + 01934], al
mov ds:[di + 04173], al
mov ds:[di + 04492], al
mov al, 09Dh
mov ds:[di + 00644], al
mov ds:[di + 01601], al
mov ds:[di + 03841], al
mov al, 09Eh
mov ds:[di + 00330], al
mov ds:[di + 00653], al
mov ds:[di + 03854], al
mov al, 0F2h
mov ds:[di + 04163], al

{ transparent pixels (copy from background buffer): }
mov eax, es:[si + 00000]
mov ds:[di + 00000], eax
mov eax, es:[si + 00004]
mov ds:[di + 00004], eax
mov eax, es:[si + 00008]
mov ds:[di + 00008], eax
mov eax, es:[si + 00012]
mov ds:[di + 00012], eax
mov eax, es:[si + 00320]
mov ds:[di + 00320], eax
mov ax, es:[si + 00324]
mov ds:[di + 00324], ax
mov al, es:[si + 00326]
mov ds:[di + 00326], al
mov ax, es:[si + 00334]
mov ds:[di + 00334], ax
mov eax, es:[si + 00640]
mov ds:[di + 00640], eax
mov ax, es:[si + 00654]
mov ds:[di + 00654], ax
mov ax, es:[si + 00960]
mov ds:[di + 00960], ax
mov al, es:[si + 00962]
mov ds:[di + 00962], al
mov al, es:[si + 00975]
mov ds:[di + 00975], al
mov ax, es:[si + 01280]
mov ds:[di + 01280], ax
mov ax, es:[si + 01294]
mov ds:[di + 01294], ax
mov al, es:[si + 01600]
mov ds:[di + 01600], al
mov al, es:[si + 01615]
mov ds:[di + 01615], al
mov al, es:[si + 01920]
mov ds:[di + 01920], al
mov al, es:[si + 01935]
mov ds:[di + 01935], al
mov al, es:[si + 02240]
mov ds:[di + 02240], al
mov al, es:[si + 02243]
mov ds:[di + 02243], al
mov al, es:[si + 02255]
mov ds:[di + 02255], al
mov al, es:[si + 02560]
mov ds:[di + 02560], al
mov al, es:[si + 02575]
mov ds:[di + 02575], al
mov al, es:[si + 02880]
mov ds:[di + 02880], al
mov al, es:[si + 02895]
mov ds:[di + 02895], al
mov al, es:[si + 03200]
mov ds:[di + 03200], al
mov al, es:[si + 03215]
mov ds:[di + 03215], al
mov al, es:[si + 03520]
mov ds:[di + 03520], al
mov al, es:[si + 03535]
mov ds:[di + 03535], al
mov al, es:[si + 03840]
mov ds:[di + 03840], al
mov al, es:[si + 03855]
mov ds:[di + 03855], al
mov ax, es:[si + 04160]
mov ds:[di + 04160], ax
mov ax, es:[si + 04174]
mov ds:[di + 04174], ax
mov ax, es:[si + 04480]
mov ds:[di + 04480], ax
mov ax, es:[si + 04493]
mov ds:[di + 04493], ax
mov al, es:[si + 04495]
mov ds:[di + 04495], al
mov eax, es:[si + 04800]
mov ds:[di + 04800], eax
mov eax, es:[si + 04804]
mov ds:[di + 04804], eax
mov eax, es:[si + 04808]
mov ds:[di + 04808], eax
mov eax, es:[si + 04812]
mov ds:[di + 04812], eax

{ exit: }
pop ds
retf
Man könnte das noch darum erweitern, statt nur einfarbigen Bytes, Words oder Dwords auch z.B. Words mit zwei verschiedenen Bytes zu finden, und die am besten mehrfach vorkommen. Letzteres dürfte aber eher selten sein, und bislang kommt mir meine Methode hier schon recht zeit- und speichersparend vor. Der Maschinencode ist etwa 4x so groß wie die rohen Pixeldaten - dazu trägt auch der Teil bei, die transparenten Pixel vom Hintergrund her zu kopieren.
Diesen Teil könnte man auch weglassen und das irgendwie anders handhaben.
Seinen Grund hat die ganze Idee basierend auf meinem Spiel Kotzman II, wo sich die Sprites wegen einem schwarzen Rand selber "ausradieren", so dass ich keinen Grafikpuffer verwenden musste.
Damals musste deshalb aber auch der Hintergrund einfach ein Schwarz sein - das könnte ich heute dann eben anders lösen.
Das Grafik-PUT von QuickBasic kann weder Clipping noch Spiegeln - von daher wäre dieses Hardcoding hier ein gleichwertiger Ersatz, nur dass noch Hintergrundgrafik realisiert werden kann, und dass es nebenbei noch schneller ist.

Bei kleinen Sprites kann man evtl. auch verschmerzen wenn es kein Clipping gibt. Wenn es sich hier um 16x16 Sprites handelt, könnte man auch ohne zu große Einschränkungen den Bildschirm runherum um jeweils 15 Pixel kleiner machen, hätte dann noch 290x170, oder etwas mehr, denn man hat ja mehr als 64000 Byte - wie auch immer, da kann man erfinderisch werden, bräuchte dann aber eben wieder einen Grafikpuffer...
Bei einer X-Breite von 350 Pixeln bleiben vertikal 187 Pixel bei 65528 Byte Puffer. Das ist mehr als ich bei Kotzman II vertikal verwende, und oben und unten ist ab Level 2 "zu". Da könnte ich also sogar noch Clipping reinbringen - aber eben dann nur mit Puffer...


Wie man im generierten Code sehen kann, ist der Witz an der Sache also, dass man nur einmal für direkt mehrere Pixel den Akkumulator mit einem Direktwert belädt, und dann "unrolled" diese "Farbe" überall dort hinschreibt, wo sie sein soll. Und dann die nächste Farbe usw. So spart man sich lesende Speicherzugriffe, und auch Vergleiche, Sprünge oder "weiterdrehen" von Registern.
Was hier je nach CPU allerdings sein könnte: AGI Interlocks, da in jedem Befehl das gleiche Register verwendet wird.
Hier müsste ich vielleicht noch DX mit ins Boot holen, ebenfalls mit einem Wert beladen (bzw. "mov dx, ax"), und dann im Wechsel mit AX verwenden.

Ich habe hier mal den 32 Bit Modus aktiviert, auch wenn ich erst sagte, ich ziele auf 286er ab. Aber je nach Grafik-Aufkommen und gewünschter "Framerate" kann es auch auf 386+ von Vorteil sein, eine derartig schnelle Grafik zu haben.

Okay und - natürlich habe ich hier wieder ein Extrem verwirklicht. Diesmal, ungewöhnlich für mich, Performance statt Speicher sparen. In Anbetracht dessen, dass die Level-Grafiken bei Kotzman II nur ca. 20 KB beanspruchten, ist das aber doch völlig legitim.

Im Hinterkopf habe ich dann noch, dass der Grafikspeicher ($A000) sehr langsam sein soll. Oder war das nur bei Lesezugriffen? Sollte er auch beim Schreiben meinetwegen 10x so langsam sein wie ein RAM-Lesezugriff, dann wird diese ganze Sache hier natürlich ziemlich relativiert.

Noch kurz zur Verwendung:
Ich lade den Maschinencode auf den Heap und rufe ihn dann per CALL über einen Pointer auf.
Vorher muss ES:SI auf die Hintergrundgrafik gesetzt werden, und in DX das Segment des Zielspeichers angegeben werden (in meinem Fall $A000), und DI entsprechend auf den Anfang des Zielspeichers. CX enthält die Koordinaten, in Form von, je nach Bildbreite z.B. 320 x Y + X.

Somit sollte nun alles genau geklärt sein, auch wenn Ihr es vielleicht schon längst verstanden hattet.
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: "Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von zatzen »

@DOSferatu:

Ich finde Deine Ideen immer interessant, und sie haben mir in der Vergangenheit auch oft weitergeholfen.

Ich muss im Moment nur sehen, was ich überhaupt machen möchte.
Ich möchte eher dort anschliessen, wo ich 1995 mit Kotzman II aufgehört habe.
Mit mehr Know-How vielleicht erstmal etwas ähnliches machen, nur eben dann alles realisieren was damals noch nicht ging (Hintergrundgrafik, AdLib Musik während des Levels, One-Shot Digitalsounds, die keine Rechenleistung einfordern.).
Ein "Wahnsinn" Spiel mit Günter und Jupp, und das ganze so als "Dungeon", wäre vielleicht eine nicht allzu aufwendig zu realisierende Sache, die dann gleichzeitig vielleicht auch den ein oder anderen Fan des Songs tatsächlich zum Spielen veranlassen könnte.
Nach meinen letzten Jahren in denen ich mich durch Experimentiererei ausgetobt habe, d.h. z.B. irgendwelche gepackten Grafikformate, würde ich wohl jetzt eher auf Pragmatik und Effizienz bauen, wenn ich ein konkretes Ziel/Spiel vor Augen habe - dann muss es eben nur am Ende funktionieren, egal ob man tolle Formate da drin hat oder meinetwegen nur simple Bitmaps.

Einen AdLib-Tracker wollte ich noch programmieren, der von vornherein die Operatoren z.B. 50x in der Sekunde ändern und so andere Klänge erzeugen kann, als man so von AdLib gewöhnt ist. Ob das auch funktioniert muss ich sehen.
Jedenfalls: Den Tracker kann ich auch nur mit dem Ziel umsetzen, dass ich ihn selber für ein Spiel brauche.
Es wird sich nämlich wohl kaum jemand für einen noch-und-drölften AdLib Tracker interessieren...

Trotzdem darf man wohl heute in seinem Spiel einen flotten 486er voraussetzen. Daher guck ich nochmal wie das so mit der Grafik werden soll. Ich hätte aber schon gerne so 50 fps die niemals einbrechen und somit auch nicht dynamisch gehalten werden müssen.
Darüber haben wir schon ausführlich debattiert. Lass mich erstmal machen...
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: "Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von zatzen »

Für mich wird das nächste wohl mein AdLib Tracker sein, und evtl. parallel schon was mit einem Spiel.

So viele AdLib Tracker gibt es doch nicht, sondern im Wesentlichen "AdLib Tracker II" und "Reality AdLib Tracker".

Das sind natürlich DIE Standards und die können auch einiges.
Wenn ich mir Mühe gebe, könnte meiner aber vielleicht doch für den ein oder anderen eine Alternative sein, zumindest wenn ich ihn einfach zu bedienen gestalte und möglicherweise eben doch einen ungewöhnlichen Sound ermögliche, der direkt in den Instrumenten definiert wird, ohne dass man sich extra Makros basteln muss.

Ich habe angefangen und gehe ganz langsam und und wohlüberlegt voran.
mov ax, 13h
int 10h

while vorne_frei do vor;
wobo
DOS-Guru
Beiträge: 614
Registriert: So 17. Okt 2010, 14:40

Re: "Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von wobo »

Huhu Zatzen&Dosferatu!
Schön, dass Ihr immer noch am Code-Basteln seid. Ich bin da ja leider seit rund 10 Jahren nicht mehr aktiv am Code-Rumbasteln.

Einen eigenen AdLib-Tracker finde ich eine gute Idee, auch wenn es zumindest Mittelklasse-Tracker wie Sand am Meer gibt. Hast Du mal überlegt, einen OPL3-Tracker zu machen, Dich also nicht auf OPL2-Kompatibilität zu beschränken? Karten mit OPL3 sind ja wesentlich häufiger anzufinden, so dass die Beschränkung auf OPL3 kein echter Nachteil wäre (imho).

Außerdem hättest Du wahrscheinlich ein Alleinstellungsmerkmal. Außer AdLib Tracker II bietet das m.W. kein anderer Tracker. Und wenn Du von vornherein auf OPL3 setzt, wären Deine Aspielroutinen auch schneller, weil Du weniger Waitstates als für den OPL2 einbauen müsstest.

Bei OPL3 hättest Du außerdem 8 Waveforms und könntest für 6 Channels auch vier Operatoren benutzen (https://en.wikipedia.org/wiki/Yamaha_OPL#OPL3). Ich weiß zwar nicht, wie FM-Stimmen mit vier Operatoren klingen, wäre aber sehr neugierig :-).
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: "Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von zatzen »

Hallo Wobo!

Puh...

Mein Anliegen war ja eigentlich, wie oben beschrieben, einen AdLib Tracker zu machen, bei dem eine kontinuierliche Veränderung der Operatoren (Standard: 50 Hz) direkt per Instrumentendefiniion festgelegt wird. Ich habe das bereits geprüft, das ist möglich, so also auch etwa einen Sound zu genenieren der soetwas wie "oooaaapp!" macht, indem der Modulator von leise nach laut verändert wird, was die AdLib so direkt nicht hergibt.

Das mit den Waitstates kann natürlich ein Argument sein, da müsste man sehen wie "schlimm" sich das bei Adlib auswirkt, mit 6x + 35x Register lesen für eine Veränderung eines Registers.
Hast Du hier Erfahrungen? Etwa, wieviele Takte so ein Lesezugriff auf 0388h verschlingt?
Moment: 35 Lesezugriffe dürften etwa 23 Mikrosekunden dauern.
Man schafft also ganz grob 40 Interaktionen mit der AdLib in einer Millisekunde.
Könnte evtl. funktionieren, was ich da vorhabe. Dass ich der AdLib nur "Relatives" mitteile, d.h. nur immer wenn sich was wirklich geändert hat, sollte klar sein.
Oder man versucht, die Waitstate-Zeit zu optimieren, indem man das quasi "im Hintergrund" macht und mit einem Zähler nachhält, wann der nächste AdLib Zugriff wieder stattfinden darf. Kompliziert, aber vielleicht möglich, wenn nötig.
Aber nein - das ist Quatsch. Am besten wird es sein, die AdLib-Zugriffe für jeden Zeit-Frame auf einen "Punkt" zu sammeln, und dann entsprechend alle nacheinander mit den Wait States auszuführen. Da kommt man nicht drumherum.

Ansonsten wäre mir Stereo ersteinmal egal, für Spiele in meinem Format reicht mono vollkommen.
Auch 9 Kanäle sind mehr als ausreichend.
Oft inspirieren Beschränkungen die Kreativität, zu viel Freiheit kann lähmen.

Mein Alleinstellungsmerkmal wäre eher die ungewöhnliche Instrumentendefinition, die für fast jeden Operator-Parameter eine Veränderung über die Zeit ermöglichen würde, und das eben für alle Instrumente als Standard, natürlich auch deaktivierbar.

Was die Wellenformen von OPL3 angeht: Rechteck wäre verlockend, um über die Kombination zweier Kanäle und detuning so ein richtig klassisches PWM Signal zu erzeugen. Aber vielleicht geht das auch irgendwie mit OPL2 Wellenformen.
Ich habe vor 27 Jahren schon einmal einen OPL3 Tracker geschrieben, irgendwie war das aber enttäuschend. Auch wegen meiner damaligen Beschränkten Fähigkeiten, aber auch weil OPL3 für mich irgendwie weniger Integrität hat als normales AdLib.
Hier mal ein Video von dem Tracker von damals, einfach nur so, leider zeigt es nicht die möglichen Klangfarben.
https://youtu.be/tn3VHysfGkM

Noch ein Faktor den man beachten muss, wäre die Komplexität der Daten und der Abspielroutinen. Standard AdLib gestaltet sich da ziemlich einfach, OPL3 dagegen ist schon etwas komplizierter.

Also Wobo, danke erstmal für Deine Anregungen!
Ich hatte mich jetzt einfach nur auf AdLib festgelegt, obwohl ich sehr wohl weiss, dass OPL3 überlegen ist!
Das mit den Waitstates wäre für mich gerade noch ein schwerwiegendes Argument.
Weisst Du gerade mal, wie das bei OPL3 wäre?
mov ax, 13h
int 10h

while vorne_frei do vor;
DOSferatu
DOS-Übermensch
Beiträge: 1220
Registriert: Di 25. Sep 2007, 12:05
Kontaktdaten:

Re: "Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von DOSferatu »

Der "OPL2", der in AdLib verbaut ist, braucht mehr Zeit, bis die Register wieder "ansprechbar" sind als der OPL3. Damals hatten manche Spiele der alten Machart das (statt mit Timern) mit Warteschleifen gelöst. Wie toll Warteschleifen auf (zu) schnellen Computern sich auswirken, kennen wir ja alle von den Borland-Produkten: der berühmte RTE200 (Borland Pascal) bzw. E6001 (Borland C). Und wenn man diesen Warteschleifen-Init weg-patcht, hat man aber immer noch das Problem, wenn diese Delay-Befehle oder Ähnliches wirklich irgendwo benutzt wurden und dann zeitlich zu kurz sind: Hängende Töne, vermurkste Sounds, teilweise "Absturz" der AdLib, weil zu schnell neue Daten ohne daß sie sich "erholen" konnte (d.h. da hatte man bis zum Reboot des Rechners gar keinen Sound mehr).

Den "Turbo-Button" auf AUS zu schalten, hat irgendwann nicht mehr ausgereicht. Deswegen hatte man ja dann auch irgendwann diese Slowdown-TSRs, die absichtlich den Computer runtergebremst haben, wenn man auf einem neueren Computer Monkey Island spielen wollte oder sowas.

Teilweise wurden in ganz alten Programmen nichtmal Warteschleifen eingesetzt, weil die CPUs so langsam und die Taktrate so niedrig waren, daß zwischen zwei Opcodes genug Zeit verging, damit die Portzugriffe langsam genug stattfanden. Auf schnelleren Computern gab es dann die o.g. Probleme.

Übrigens: Ich habe hier "das große Soundblaster Buch" (oder wie es hieß), und da wird außer der Soundbuffer/Frame+DMA Variante der SB auch die ganze AdLib-Ansteuerung beschrieben (SB hat ja die AdLib-Fähigkeiten integriert) - inklusive der Zugriffs-/bzw. Wartezeiten für die einzelnen AdLib- (d.h. OPL2) Register.

OK, das war mein Senf zu dem Thema.
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: "Kompilierung" von Grafik zu Assembler-Code, für maximale Geschwindigkeit?

Beitrag von zatzen »

Danke, DOSferatu.

Ich nutze übrigens vorerst als Timer eine BIOS-Funktion, die nach einstellbarer Zeit eine Flag-Variable setzt.
Das ist für mich erstmal hinreichend genau und ich hab das Gedöns mit Int08 umbiegen, Uhrzeit mitzählen (was meist nicht genau ist) nicht, außerdem brauche ich das genau so, d.h. alles "erledigen" (NICHT in einer Interrupt Routine), und dann warten bis das Flag umspringt.
Sollte es so zu sehr "eiern" kann ich das immer noch "richtig" machen, aber vorerst sehe ich keinen Grund dazu.

Die Wartezeiten bei der AdLib werden ja durch Lesezugriffe realisiert, sind also, zumindest in Assembler geschrieben, ziemlich genau nur so lang wie sie wirklich sein müssen.


Ich habe eben auch noch recherchiert:

OPL3 braucht nur 0,28 Mikrosekunden Wartezeit nach einem Schreibzugriff, das ist alles.
Vielleicht ist OPL3 Hardware aber auch als AdLib konfiguriert entsprechend schnell, so dass ich das per Konfiguration einstellen würde, wie lang gewartet werden muss.

Ein Vorteil von AdLib könnte sein, wenn ich es recht verstanden habe, dass es direkt von Windows bis XP unterstützt wird. Somit könnte man dann ein Spiel ohne DosBox in halbwegs echtem DOS spielen, bzw. meinen Tracker dann so nutzen.

Ich habe "Das Soundblaster Profi Buch". Zumindest damals, als 14jähriger oder so, musste ich mich da aber so richtig durchbeissen, irgendwie empfand ich es als umständlich geschrieben. Da kommt mir jetzt so eine übersichtliche knappe Anleitung wie https://bochs.sourceforge.io/techspec/adlib_sb.txt gelegener.


Übrigens, vielleicht was für die Admins:
Während ich hier schrieb, schrieb auch DOSferatu was, und als ich meine Nachricht abgesendet hatte war sie einfach weg.
mov ax, 13h
int 10h

while vorne_frei do vor;
Antworten