Mode X und EGA Parrallax

  • Die innere schleife zeichnet immer eine Pixel Zeile und da immer nur jeden 4. Pixel, wegen Mode X !!!

    Wenn genug Pixel gesetzt wurden in der Zeile, das sind maximal 32/4 = 8, unterbricht die innere schleife.


    Die mittlere wird jetzt 1x durchlaufen,

    Hier wird die X Position zurück gesetzt und mit >> sa+=80;<< im Grafikspeicher eine Zeile dazu addiert (320/4=80 byte je Zeile >> Mode-X 4 Planes)


    Wenn die letzte Zeile gesetzt ist, durchläuft die äussere Schleife 1x und setzt alles wieder zurück um in das nächste Plane die Pixel zu schreiben. 4 Planes , also durchläuft die äussere Schleife nur 4x insgesamt.

    Plane wechsel sind Leistungsfresser, 4x ist hier das Minimum.




    • DCco = sprite(map1, position) (wahrscheinlich intern mehrere Schritte?)

    map1 ist die Textur Nummer im Array und position ist die Position des Pixels im Textur speicher innerhalb der Textur.


    Die Funktion prüft welches Array für die Textur zuständig ist und gibt dann den angefragten Farbwert zurück.


    Die Funktion hat das direkte auslesen des Arrays ersetzt, weil es jetzt 4 Arrays gibt.

    Leider ändern sich die Texturen sonst würde ich zu Funktionsbeginn einfach mehrere Pointer auf die richtige Textur setzen.


    MXs++; // WEGLASSEN? MXs wird nie gelesen, ist einem früheren Umbau zum opfer gefallen. Kann tatsächlich weg.



    Das ganze hatte ich so oft umgebaut und auch mehrere Funktionen Parallel laufen um zu vergleichen, da übersieht man schnell mal was.

    Ich schaue die Funktionen Regelmässig durch und versuche Abfall zu beseitigen.


    Ich schreibe das dann nochmal um und dann schauen wir weiter.


    die Funktionen zum einlesen von Dateien sind fast alle geändert und die NPC's Sprites sind auch schon aufgeteilt... es gibt jetzt dann 8 50Textur Arrays und 1 Aray in dem unterschiedlich grosse Texturen gespeichert sind für spezielle Effekte und Objekte.


    Es ist jetzt 2 FPS schneller wie vor diesem Umbau.

    2 Mal editiert, zuletzt von Markus ()

  • Die 4 Planewechsel in der äusseren Schleife für 1 komplette Textur von bis zu 1024 Pixel sind schon optimal, ja. Aber du hast Recht, auf maximal 32 Pixel Breite kommen nur maximal 8 Durchläufe der inneren Schleife, extrem viel ist das nicht.


    Manche Compiler würden da wohl Loop Unrolling machen, also quasi die innere Schleife 8x Copy Paste in den Assemblercode schreiben und dann je nach Berechnung, wieviele Durchläufe gebraucht werden, an eine geeignete Stelle der 8 Kopien reinspringen. Aber da müsstest du trotzdem in Vorleistung gehen und die Anzahl der Durchläufe vorher berechnen, statt DCxloop hochzuzählen und mit XM zu vergleichen bzw. im Original "while ( DCx + (X8<<2) < XM);" mit Umweg über X8 und Shift zu verwenden.


    Für die mittlere Schleife (über die Zeilen) zähle ich nochmal ungefähr 30 Schritte pro Durchlauf. Diese MapSp und MapTe Felder haben ziemlich viele Dimensionen, eigentlich vier, aber die innersten 2 Dimensionen berechnest du von Hand, darum sehen sie aus wie eine. Die Abmessungen sind je 2x2x16x16 Einträge. Eigentlich könntest du das sogar komplett selbst berechnen in 1-dimensionalen Feldern mit 1024 Einträgen:

    Code
    index = (rx>>5) & 15;
    index |= (ry>>1) & 240;
    index |= (ry>>1) & 256; // texBuY
    index |= rx & 512; // texBuX
    map1 = MapSp[index];
    map2 = MapTe[index];

    Je nach dem, wie clever dein Compiler ist, könnte das merklich effizienter sein als ein MapSp[texBuX][texBuY][aus rx und ry berechnetes tex].

    Du kannst es sogar noch eine Zeile kürzer machen, 7 Rechenschritte statt 9:

    Code
    index = (rx>>5) & 0x0f; // 15
    index |= (ry>>1) & 01f0; // 240+256 wegen texBuY
    index |= rx & 0x200; // texBuX 512
    map1 = MapSp[index];
    map2 = MapTe[index];

    Es wäre auch möglich, die Einträge umzusortieren, um noch mehr Rechenzeit zu sparen:

    Code
    index = (rx & 0x3e0) | ((ry & 0x3e0) >> 5); // 4 Rechenschritte statt 7
    map1 = MapSp[index];
    map2 = MapTe[index];

    Egal wie du MapSp und MapTe strukturierst, die Adressierung müsste natürlich überall einheitlich sein.


    Wenn du index nicht von Hand berechnest, sondern bei Map...[...][...][...] bleibst, wird der Compiler intern sicher nicht die Lösung mit 7 Schritten finden, sondern eher eine generische Lösung mit 10 oder mehr Schritten und wenn du Pech hast sogar mit Multiplikationen statt Shift, AND und OR arbeiten.

  • PS: Wenn du es auf Schritte pro Pixel umrechnest, sind die mittlere Schleife je nach Sprite-Breite etwa 30/8=4 zusätzliche Schritte pro Pixel. Schmalere Sprites sind im Vergleich entsprechend teurer. Von den etwa 30 Schritten kannst du über die Methode mit dem MapSp und MapTe[index] 6 oder mehr einsparen, also immerhin 20%.


    Die innerste Schleife mit ihren 11 oder mehr Schritten pro Pixel bleibt trotzdem pro Pixel der wichtigste Verbraucher von Rechenzeit. Darum werfe ich sehr gerne mal einen Blick auf die sprite(map..., position...) Funktion. Jeder Schritt innerhalb der Funktion den du einsparen kannst, ist ein kompletter gesparter Schritt pro Pixel.


    Und wie gesagt: Stelle sicher, dass die sprite(..., ...) Funktion vom Compiler als inline optimiert wird. Funktionen wirklich aufrufen (ohne inline) kostet auch jedes Mal mehrere zusätzliche Schritte, um jedes Argument hin zu kopieren, zur Funktion zu springen, wieder zurück zu springen und das Ergebnis weiterzugeben.

  • Das ist der Inhalt der Funktion.


  • Danke! Nachdem du es als sprite(map1, position) aufrufst, sich map1 aber extrem selten ändert, würde es da einen sehr grossen Unterschied machen, mit einem getsprite zu arbeiten und zusätzlich die Modulo-Division zu vermeiden:


    Code
    unsigned char far * getsprite(unsigned int TNr) {
      if (TNr < 50) return &MXspriteT1[TNr][0];
      TNr -= 50;
      if (TNr < 50) return &MXspriteT2[TNr][0];
      TNr -= 50;
      if (TNr < 50) return &MXspriteT3[TNr][0];
      TNr -= 50;
      if (TNr < 50) return &MXspriteT4[TNr][0];
      return 0;
    }

    Und dann in deinen Schleifen:


    Damit sparst du dir für jedes einzelne Pixel mehrere teure Berechnungen, die sich in deiner aktuellen sprite() Funktion verstecken: Bisher musst du für jedes Pixel sprite() 1-2 mal aufrufen und jeder Aufruf kostet dich ein Modulo 50 und eine interne Multiplikation mit 1026, wenn Tdata 0 bis 1025 sein kann. Zusätzlich sparst du dir jedes Mal mehrere Vergleiche mit 50, 100, 150, 200. Du rufst getsprite nur genau 2x auf (oder 4x, wenn posx >= 32 eintritt) und behältst den Pointer dann für bis zu 8 Pixel. Die sprite() Funktion rufst du dagegen für jedes einzelne Pixel 1x oder 2x auf, insgesamt also bis zu 16x. Das spart also bis zu 7/8 der Rechenzeit für Sprite-Feldzugriffe 8)

  • Mir war heute Nacht noch ein etwas hässlicher aber vielleicht effektiver Trick eingefallen: Aktuell sind deine Sprites ja 2 Bytes mit der Höhe/Breite und dann bis zu 32x32 Bytes Pixeldaten, was insgesamt mehr als runde 1024 Bytes wird. Bei 1024 wären die Berechnungen einfacher, weil Shift statt Multiplikation wenn es entweder der Compiler bemerkt oder du sie von Hand machst.


    Das könntest du erreichen, indem 32x32 Sprites KEINE Höhe/Breite Angabe haben. Alle ANDEREN Sprites könnten z.B. als erste 2 Bytes einen magischen Wert haben, den die 32x32 Sprites nicht als erste 2 Pixel haben dürfen, und danach dann die 2 Höhe/Breite Bytes. Die anderen Sprites sind ja alle maximal 31x32 Bytes lang, es ist also genug Platz für Magie. Verkompliziert halt den Sprite-Editor und das Lesen der Sprites.


    Eine ganz andere Idee wäre, jede Sprite-Datei mit 256 Bytes für Höhe, Breite und 16-bit Offset (aus 2 Bytes zusammengesetzt) aller 50 (später auf 63 ausbaubar) Sprites der Datei anfangen zu lassen, dann kann die Sprite-Datei je nach dem, was für Sprites drin sind, auf der Platte kleiner sein. Und über die Offset-Liste findet man jeden Sprite sofort, ohne Shift, Multiplikation oder sonstige Berechnungen zu brauchen.


    Im Speicher könntest du dann statt einer 50x1026 Tabelle eine flache Liste von 51456 bzw. 64768 Bytes (50,25 bzw. 63,25 kB) anlegen, muss ja weniger als 64 kB sein um HUGE zu vermeiden. Und die getsprite Funktion könnte die Offset-Liste verwenden, um einen Pointer für den gewünschten Sprite zu erzeugen.


    Nachtrag: Von der Reihenfolge her noch besser: Erst 64 (wovon maximal 63 genutzt) 16-bit Offsets, dann 64 Breiten, dann 64 Höhen, dann bis zu 63 kB an Pixeldaten, sowohl in der Datei als auch im Speicher.

  • Die Texturen für die Landschaft sind fest 32x32 Pixel.

    Also exakt 1024 byte.


    Die 2 zusätzlichen Byte sind folgende.

    Kollision Eigenschaften vom Objekt >> Keine / Kann nicht betreten werden / unterschiedliche Teilbereiche / werden Geschosse geblockt ja / nein


    Ebene der Textur

    oberhalb der NPC (Decke Dach Baumkrone) / unterhalb der NPC(Boden) / NPC's können davor oder dahinter stehen.



    Texturen für Objekte sind zwischen 1 und 32 Pixel, diese werden anders gespeichert.

    Hier sind die ersten 2 byte HxB ...beim laden wird für jede Textur eine Startadresse hinterlegt, damit nicht unnötig Speicher belegt wird.


    Deshalb gibt es auch 2 Funktionen zum reproduzieren, eine reproduziert immer fest 32x32, die andere reproduziert immer nur einen kleinen Bereich.

  • Ah dann ist es in den Dateien ja schon passend umgesetzt gewesen :) Die eigentlichen Pixel könnten in dem Fall im Speicher auf jeweils exakt 1024 Byte abgelegt werden und die Zusatzeigenschaften wie Höhe, Breite, Kollisions-Eigenschaften und Ebene in getrennten Arrays mit jeweils genug Einträgen für sämtliche Sprites/Texturen, dann spart man für die die Aufteilung auf die 4 einzelnen Pixel-Tabellen, hat immer sofort anhand der Textur-Nummer die Eigenschaften an einem zentralen Ort parat und die Pixel selbst fangen auf besonders leicht berechenbaren Adressen innerhalb der 4 Pixel-Tabellen an, nämlich auf Vielfachen von 1024.


    Zusätzlich kannst du in den Pixel-Tabellen noch die Pixel im Speicher oder gleich in den Texturdateien umsortieren für schnelleres Zeichnen. Momentan sind jeweils alle (bis zu 32, je nach Textur nicht alle genutzt) Pixel einer Spalte hintereinander, dann kommt die nächste Spalte etc. Statt der nächsten Spalte könnte es jeweils die VIERTNÄCHSTE Spalte sein: Spalte 1, 5, 9, ... 29, 2, 6, 10, ... 30, 3, 7, ... 31, 4, 8, ... 32. Dann kann man die Koordinaten und Bankwechsel direkt durchzählen und das "+= 4" an verschiedenen Stellen wird zu "++".

  • Habe die Funktion jetzt Schritt für Schritt geändert, das brachte wieder was...




    Ich habe noch eine Funktion geändert, dann kann ich später eventuell die Texturen anders speichern.


    Hier der alte Code...


    habe ich jetzt so geändert das die Textux informationen genau gleich wie bei der anderen Funktion aufgerufen werden.

    Der Rechenaufwand ist aber fast der selbe momentan.



    ich weiss ja nicht wozu die Funktion >>Jump Optimization<< im C++ Compiler eigentlich gedacht ist, aber der mist hat mir jetzt die komplette Festplatte vom 386 zerlegt, muss den jetzt komplett neu aufsetzen.

    Anscheinend hat der irgendwo mist in den boot sektor geschrieben oder sowas. Scheiss Compiler.

  • Ich glaube du hast vermutlich irgendwo durch den Speicher geschrieben mit deinem Programm. Das ist unter DOS sehr leicht möglich. Erst unter Windows, Linux und Co geht das nimmer so leicht…

    root42 auf YouTube


    80486DX@33 MHz, 16 MiB RAM, Tseng ET4000 1 MiB, GUSar Lite & TNDY & SnarkBarker, PC MIDI Card + SC55 + MT-32, XT CF Lite, OSSC 1.6

  • Ich glaube du hast vermutlich irgendwo durch den Speicher geschrieben mit deinem Programm. Das ist unter DOS sehr leicht möglich. Erst unter Windows, Linux und Co geht das nimmer so leicht…

    ...das wird das Programm ungewollt gemacht haben, weil vermutlich irgendwo was falsch vom compiler erstellt wurde, war halt ein Blöder zufall jetzt.


    Danach den Rechner komplett neu aufgesetzt und das Programm ohne die Einstellung Funktionierte wie immer.


    Der 386 wird zum Glück eigentlich nur für programmier Zwecke verwendet... hat seinen Grund.

  • Palettenanpassung je nach Tageszeit? Schöne Idee :)

    Ist die einfachste Möglichkeit bei der Rechenleistung.

    Am liebsten würde ich gleich die Feuer effekte überall hin projezieren, schöne Lichtkegel mit Schatten.

    Aber der 386 zeigt mir schon den Stinkefinger, wenn ich nur drüber nachdenke.

  • Kann man auch berechnen, wenn die Palette einem festen Muster folgt ziemlich gut sogar.



    Nachts


    sowas zu basteln habe ich ja spaß dran.. 386 vermutlich nicht so.

    Für Menüs schaut sowas aber dennoch immer schick aus. Moderner wie blaue Felder!

  • Bei so was ist es gut, wenn die abgedunkelten Farben durch einfache Bitshifts erreichbar sind. Das geht zB wenn die Palette nach Helligkeit sortiert ist.

    root42 auf YouTube


    80486DX@33 MHz, 16 MiB RAM, Tseng ET4000 1 MiB, GUSar Lite & TNDY & SnarkBarker, PC MIDI Card + SC55 + MT-32, XT CF Lite, OSSC 1.6

  • Ich habe mir mal gedanken gemacht wie man Dialoge aufbauen könnte.


    Ich stelle mir vor die Dialoge für jeden einzelnen NPC in Dateien zu packen.

    diese sind dann ich Blöcke unterteilt.


    Funktion | Block Nummer Funktion | Nummer oder Sprungmarke

    Ich denke das ist eine ganz gute möglichkeit Dialoge auf zu bauen und auch etwas zufallsprinzip darin ein zu bauen.


    Später mussen eventuell noch Sonder befehle rein, wenn ein Dialog im zusammenhang mit einer Quest steht!

    Wiederholbare Aufgaben/quests

    Einmalige Quests oder Quests die Teile einer Questreihe sind und auch erst unter speziellen voraussetzungen freigeschaltet sind / werden.


  • Ans Charakter Rom kommt man im Grafikmodus nicht dann?

    386SX- 20 Mhz "Erster eigener Rechner!2" NoName Komponenten

    486DX -30 "Industrie PC" auf Steckkarte

    Super Sockel 7 Gigabyte GA-5AA 3Dfx Voodoo 3500 TV

    AMD "Geode" ebenfalls Steckkarte für Backplane

    3x IBM Netvista 8364 "ThinRetroSystem" 1-2 von denen würde ich tauschen...


    "und noch so einiges mehr... "

Jetzt mitmachen!

Sie haben noch kein Benutzerkonto auf unserer Seite? Registrieren Sie sich kostenlos und nehmen Sie an unserer Community teil!