OK, dann auch hier noch eine Antwort:
zatzen hat geschrieben: Di 20. Okt 2020, 20:23
DOSferatu hat geschrieben:[parsen]Ich kenne keine .NIF Dateien. Wenn Du da etwas konkreter wirst oder mir ein Beispiel schickst,[...]
(22. September):
Es geht eher allgemein ums Parsen, konkret müsste ich bald mal an XMLs ran. Ich werd das schon hinkriegen, aber es könnte ja sein dass Du da gute Tipps hast wo ich so spontan einfach nicht drauf komme.
Dazu hier mal ein Ausschnitt aus einer Renoise Tracker XML, wie dort eine Patternspur codiert ist:
Code: Alles auswählen
<Pattern>
<NumberOfLines>32</NumberOfLines>
<Tracks>
<PatternTrack type="PatternTrack">
<SelectedPresetName>Init</SelectedPresetName>
<SelectedPresetLibrary>Bundled Content</SelectedPresetLibrary>
<SelectedPresetIsModified>true</SelectedPresetIsModified>
<Lines>
<Line index="0">
<NoteColumns>
<NoteColumn/>
<NoteColumn/>
<NoteColumn>
<Note>C-4</Note>
<Instrument>00</Instrument>
</NoteColumn>
</NoteColumns>
<EffectColumns>
<EffectColumn>
<Value>37</Value>
<Number>0A</Number>
</EffectColumn>
<EffectColumn>
<Number>0V</Number>
</EffectColumn>
<EffectColumn>
<Number>0D</Number>
</EffectColumn>
<EffectColumn>
<Number>0G</Number>
</EffectColumn>
</EffectColumns>
</Line>
[...]
</Lines>
<AliasPatternIndex>-1</AliasPatternIndex>
<ColorEnabled>false</ColorEnabled>
<Color>0,0,0</Color>
</PatternTrack>
[...]
</Tracks>
</Pattern>
Achduschande.
Ja, das ist eben XML. Textbasiert, prinzipiell leicht zu parsen, ABER:
Wie Du ja selbst schon siehst, arbeitet das ganze Ding verschachtelt. Bei so neueren Formaten ("wir haben ja genug Speicher und Rechenzeit"...) wird dann leider nicht definiert, WIE TIEF maximal geschachtelt werden darf - erwarte hier also alles oder nichts. Das ganze Ding sieht aus wie die umständlichst-mögliche und platzverschwendendste Möglichkeit, Musikdaten zu speichern. Stumpfsinn in absoluter Vollendung. Umständlichern wäre eigentlich nur noch, wenn dieses Zeug dann auch noch in eine Grafik oder ein PDF eingebettet wäre...
Was kann ich da groß für Tips geben? Man sieht ja, wie die Daten aussehen. Also immer auf < bzw. </ parsen und die Namen aller "Elemente" bzw. "Sub-Elemente" irgendwo in einer Tabelle haben. Sortierte Tabelle wäre gut, weil schnellere Suche (mit der Hälfte-Hälfte-Hälfte-Methode) möglich. Außerdem, was ICH immer mache: Alles interne UPCASE speichern (oder eben alles downcase) und beim Laden auch gleich alles, bevor es verglichen wird erstmal UPCASE (oder downcase) machen. Auf diese Art braucht man nicht Groß-/Kleinschreibung extra checken. Beim SPEICHERN sollte man dann natürlich die entsprechende Groß-/Kleinschreibung wieder so machen, damit das Leseprogramm von Renoise das abkann. (Weiß nicht, wie intelligent die das programmiert haben.)
Ach ja: Und bitte NICHT auf Zeilenenden und dieses Einrücken verlassen oder das als Möglichkeit betrachten, die Dinge zu erleichtern. Diese Leerzeichen und Zeilenenden und Tags oder ähnliches außerhalb der < > zählen nur zur Textformatierung, haben aber keinen Inhalt für HTML oder XML. Prinzipiell mußt Du also davon ausgehen, daß ein HTML oder XML auch komplett in einer einzigen Zeile geschrieben wurde, ohne Leerzeichen oder Zeilenenden. Also kann man nicht erwarten, daß man irgend etwas davon automatisch wie einen max. 255 Zeichen langen Pascal-String einlesen kann.
Bei von einem
Programm erzeugten XML kann man zwar davon ausgehen, daß jedes "geöffnete" Item auch wieder "geschlossen" wird - aber man sollte evtl auch Erkennungsroutinen einbauen, die defekte Strukturen erkennen können und bei einfachen Fehlern diese selbst intern korrigieren und bei schweren Fehlern sich nicht in irgendwelchen Verschachtelungen totlaufen, sondern entsprechend aussteigen.
Ja, das klingt alles Mist - deshalb bin ich ja auch so ein "Riesen-Fan" von diesem Unsinn. (Mal abgesehen davon, wieviel Zeichen/Platz verbraucht werden, um einen Wert zu kennzeichnen, der vielleicht als Daten 1 Byte groß ist.)
Merke:
Beim
LADEN fremder Daten immer so tolerant wie möglich sein - also alles erlauben, auch Zeug, was leicht "neben dem Format" liegt oder defekt ist. Solange es damit lesbar ist, auf plausible Daten anpassen. Falls nicht möglich, Fehler erkennen und ausgeben - keinesfalls darauf verlassen, daß textbasierte Formate immer richtig formatiert sind.
Beim
SPEICHERN von Daten immer so stringent wie möglich sein - also möglichst genau an die Spezifikationen halten, weil man nicht weiß, wie tolerant andere Programme sind).
zatzen hat geschrieben:Es geht natürlich jetzt nicht darum, was da konkret passiert, sondern einfach nur um ein Beispiel, wie die Daten in XML aussehen, die man auch binär halten könnte.
Ich denke mir, ich werde diese XML Daten parsen in binäre Datenfelder hinein, dann modifizieren um anschliessend wieder XML zu generieren. Oder wie würdest Du das machen?
Genau so.
zatzen hat geschrieben:(20. Oktober):
Ich habe zwischenzeitlich meine Tools schon geschrieben, also wieder meine Zeit sinnvoll im Unreal-Life verbracht. Das XML habe ich vielleicht etwas unelegant geparst, modifiziert und wieder generiert, aber es funktioniert. Trotzdem, falls Du da eine quasi optimale Vorgehensweise hast, lass es mich wissen, evtl. muss ich nämlich noch mehr Tools schreiben oder das vorhandene erweitern.
Ich gehe natürlich davon aus, daß Du Dir zum Parsen des XML-Zeugs eine Unit gebaut hast.
zatzen hat geschrieben:[CPU-(64bit)-Abwärtskompatibilität]
Man wollte wohl einen Schnitt machen. Ja, so isses eben, man will neues verkaufen, oder denkt sich einfach, wozu etwas weiter unterstützen wonach 99% der Leute nicht mehr krähen.
Aber es waren meiner Meinung nach zu 0% technische Gründe. Die CPU hätte es immer noch als "Sandbox"-artige Emulation machen können - so fett und überkandidelt, wie die heute sind und dann keine 30 Jahre alten Standards erfüllen können - kann mir keiner erzählen. "Schnitt machen" ist dann immer so ein schönes Totschlag-Argument. So als würden sie den Bedarf ALLER Leute der Welt kennen. Meiner Meinung nach ist es nichts anderes als künstliche Veralterung, um die Leute zu zwingen, diesen neuen Dreck zu benutzen: Dreck, über den Leute kontrolliert werden können, weil sie selbst keine vollen Zugriffe mehr auf ihre eigenen Daten oder ihr System bekommen. Dreck, mit denen man Leuten vorschreiben kann, was sie mit ihren Computern machen dürfen und was nicht - weil das OS denen bestimmte Dinge, die zwar technisch möglich wären, einfach nicht mehr erlaubt.
Es geht einfach darum, willfährige "User" zu züchten - das was Apple schon seit Jahrzehnten macht, will man bei allen anderen jetzt auch durchsetzen. Und das ist nur ein weiterer Schritt in diese Richtung. Und es ist sehr leicht, Laien ohne Maschinen-Verständnis quasi JEDE Schweinerei als technische Notwendigkeit unterzujubeln. Und weil es so leicht ist, wird es auch gemacht.
zatzen hat geschrieben:In der Medienwelt läuft das ja auch so, aber da scheinen die Leute es ja gerne zu haben, mit der Zeit zu gehen.
"Mit der Zeit gehen" ist ein ähnliches Totschlag-Argument. Man stellt damit Leute, die sich für die Technik HINTER der Technik interessieren, als stumpfhirnige Höhlenmenschen dar. Indem man Leute, die bestimmte Dinge wollen / erhalten wollen, diskreditiert, erweckt man den Eindruck: "Dieser Bedarf muß nicht berücksichtigt werden, weil alle die das wollen, dumme Verlierer sind."
Beeinflußbare Leute wollen lieber zur hippen Clique gehören, als dafür, daß sie sich für etwas MEHR interessieren als das, was "Medienwelt" meint, was man "zu wollen hat", als dumme Hinterwäldler hingestellt zu werden. Das ist also alles Ergebnis von Marketing. Den meisten Leuten kann man bekanntermaßen jeden Mist einreden - wird auch seit Jahrzehnten gemacht. So "Werbefachleute" studieren diesen Kram schließlich, als wäre es eine Wissenschaft. Es gibt also Myriaden von Leuten, die in einer Berufsgruppe arbeiten, die den ganzen Tag nichts anderes machen, als (leider sehr erfolgreich) daran zu arbeiten, Leute zu überzeugen, was sie gefälligst "wollen sollen".
zatzen hat geschrieben:Die Vinyl-Schallplatte ist eine Ausnahme und scheint mittlerweile beliebter als die CD zu sein, obwohl sie wissenschaftlich gesehen von letzterer komplett in den Sack gesteckt wird was Klangtreue (und Komfort) angeht. Will heissen: Überspielt man eine Schallplatte mit guten Wandlern auf eine CD klingt die genauso wie die Schallplatte. Macht man von einer CD eine Schallplatte klingt es NICHT genauso.
Naja, und das ist dann eben wieder die berühmte Ausnahme:
Überall, wo es NICHT den EIGENEN Interessenbereich berührt, ist es egal, wenn Interessenbereiche ANDERER Leute beschnitten werden. Berührt es den eigenen Interessenbereich, ist es wichtig und tragisch.
Man könnte dem entgegenhalten, daß eine Schallplatte analog ist, d.h. JEDER denkbare Amplitudenwert möglich ist, bzw. nur noch davon anhängt, wieviele Atome breit die Spurrille einer Schallplatte maximal sein kann - während bei einer CD die Anzahl möglicher Amplitudenwerte auf eine diskrete Anzahl von 65536 begrenzt ist. Ich sage nicht, daß es nicht ausreicht oder mein Gehör so fein wäre, da noch "Treppen" zu hören. Ich sage nur, daß es auch Argumente für Schallplatte und gegen CD gibt.
Der Vorteil digitaler Daten (CD) ist ihre Robustheit. Weil es nur 0 und 1 gibt und NICHT "ein bißchen 1" oder "ein bißchen weniger 0", kann immer eindeutig zu 0 oder 1 zugeordnet werden - und entsprechende Fehlerkorrekturprotokolle tun ihr übriges, um die Daten auf einer CD selbst bei teilweiser Beschädigung noch zu 100% reproduzierbar zu machen.
Hat also alles Vor- und Nachteile und obwohl ich selbst keine Schallplatten habe, habe ich Verständnis dafür, wenn sie jemand den CDs vorzieht.
Denn ich erwarte ja schließlich auch Verständnis dafür (ohne daß es jemand genauso machen muß!), daß mir für Sound oft 8-Bit-Mono 11-16 kHz Daten mehr als ausreichen und ich nicht der Meinung bin, alles in aufgeblasenem 16-Bit-Stereo 44 kHz haben zu müssen - wogegen andere Leute der Meinung sind, daß selbst 384 kHz, 32 Bit immer noch "gerade mal gut genug" sind, um irgendwelche Sounddaten zu speichern.
(Das gleiche erlebe ich mit Grafik: Nein, 16777216 Farben (256 Phasen pro Rot/Grün/Blau-Phase) sind plötzlich ZU WENIG! - Nein, es müssen MINDESTENS 1024 Phasen (30-Bit) Farben sein! 16,7 Mio Farben reichen nicht, diese Leute können über 1 Milliarde verschiedener Farben wahrnehmen und wenn's weniger ist, sehen sie die Abstufungen zwischen den Farben... Daß die INDUSTRIE, die immer wieder was neues herstellen muß, um ihre Existenz zu rechtfertigen, mit solchem Kram kommt, ist mir klar und wundert mich nicht. Aber ich kenne leider Privatpersonen, die diesen Quatsch auch glauben...)
Zu VHS:
VHS hatte den Vorteil, daß es total idiotensicher war, eine Aufnahme zu machen, zu löschen, zu überschreiben, usw.: Einfach Play/Record/FastForward/Rewind/Stop... simpel wie ein Kassettenrekorder! Kein Auskennen mit dem DVD-Format, kein "Finishen" notwendig, damit die abspielbar wird. Tja - aber: Man (also die Film-/Musikindustrie) WILL ja auch gar nicht, daß irgendwer Aufnahmen von irgend etwas machen kann. Und wenn man es genügend erschwert, führt das dazu, daß es irgendwann auch sonst keiner mehr will.
zatzen hat geschrieben:Und kaum habe ich mich damit abgefunden, dass die neueren Computer keine Diskettenlaufwerke mehr haben, sind auch schon CD/DVD Laufwerke wieder aus der Mode.
Disketten: Selbes Ding. Einfach schreiben, löschen, überschreiben. So eine CD/DVD hingegen, die muß man brennen. Braucht ein spezielles Laufwerk UND spezielle Programme.
Die USB-Sticks sind wieder etwas "einfacher" als Wechselmedium: Die kann man endlich wieder wie Disketten/Festplatten beschreiben usw. Allerdings ist USB ein viel zu komplexes Format. Es wird zu viel Hardware und Software benötigt, um es irgendwo zu benutzen. Schon die offiziellen Specs vom ursprünglichsten USB sind ca. 1200 Seiten lang.
Gute Dinge / gute Ideen erkennt man immer daran, daß sie
einfach sind.
Und: Bei allem (Hardware, Software, Programmiersprachen, andere Dinge) was man benutzen soll/will, sollte man immer daran denken, es wie bei HUMOR (bzw einem Witz) zu behandeln: Wenn man es erklären muß, ist es Mist.
zatzen hat geschrieben:DOSferatu hat geschrieben:Als ich auf Pascal kam, gab es erstmal eine MENGE, was ich komisch fand. Aber ich mochte schon auf Anhieb, daß es compiliert und war froh, von diesem Interpreter-Scheiß weg zu sein und seitdem will ich nichts mehr mit Interpreter-Zeug zu tun haben.
QuickBASIC hatte einen Compiler. Allerdings separat - die Testläufe machte ein Interpreter wenn ich nicht irre. Und der konnte meistens mehr Speicher fassen als der Compiler, deswegen war das oft ärgerlich wenn man sein Programm am Ende zurechtstutzen musste. Hatte ich schonmal erwähnt. Weil ich nur QBasic kannte, bei dem in der IDE die "SUBs" separat angezeigt wurden, fand ich erst unübersichlich dass man bei Pascal alle Routinen auf einmal sieht.
Ja, ich kenne noch jemand anders, der damals mit BASIC angefangen hat, jetzt aber immer noch in BASIC (Visual BASIC 6 und PowerBASIC) unterwegs ist und neueren. Aber der kennt das Obengesagte zu diesen alten PC-BASICs auch noch: Daß es interpretiert ODER compiliert sein konnte, der interpretierte (und langsamere) Kram aber mehr Speicher abkonnte...
Naja, selbst das VB6 erzeugt, nach dem, was der so erzählt, nur so 'ne Art Bytecode, der dann interpretiert wird... MIR wär das zu blöde - aber muß jeder selbst wissen. Ich sag mal so: Mein GameSys2 erzeugt auch Bytecode, der dann von einer (ASM-)VM interpretiert wird - aber das Ding ist ja nicht für ganze Programme gedacht, sondern nur zum Steuern von Figuren. Ich käme nicht auf die Idee, damit auch Grafiken zu berechnen/erzeugen.
zatzen hat geschrieben:DOSferatu hat geschrieben:Ach, ich bin das $ so gewöhnt. Außerdem seh ich dann viel deutlicher, daß es Hex ist, weil das Zeichen nirgendwo sonst vorkommt:
bach = Variable Namens bach
0bach = Hex-Wert für dezimal 2988
Da ist mir $bac lieber.
Ich habe wohl verinnerlicht, dass Variablen nicht mit Ziffern beginnen dürfen. Ich hatte bisher keine Probleme. Aber jeder wie er will eben. :)
Aber tatsächlich gibt auch Freepascal bei einem RTE die Adresse mit einem $ vorne aus.
Naja, das ist die Notation, die Pascal von sich aus als primäre benutzt. Das mit dem h wird sekundär supportet für Leute, die's interessiert. Mich hats immer genervt, daß ich "verschieden lange" Hexzahlen haben muß: Damit Hexzahlen, die mit A bis F beginnen, als Zahl erkannt werden, immer diese 0 davor:
45FCh - 5 Zeichen
0FC45h - 6 Zeichen
Das sieht einfach scheiße aus. (Dann muß man wohl ALLE Hexzahlen "nullen", wenn man die untereinander schreibt oder so...) Aber muß eben jeder selbst wissen.
zatzen hat geschrieben:DOSferatu hat geschrieben:Und in C kann man keine Prozedures/Functions ineinander verschachteln. Das ist total arm!
Ich hab das bisher in Pascal auch nur sehr selten gemacht, ich dachte dabei immer das gäbe zu viel Stack-Beanspruchung.
Eigentlich ist das Gegenteil der Fall. Wenn man eine Procedure in eine andere schreibt, kann man die Variablen der "oberen" Procedure in der unteren mitverwenden, ohne sie nochmal extra übergeben zu müssen:
Code: Alles auswählen
procedure Alpha(Eins,Zwei,Drei:word);
var Vier,Fuenf,Sechs:integer;
procedure Beta(Sieben,Acht,Neun:byte);
var Zehn,Elf,Zwoelf:longint;
begin
{Hier bin ich in Procedure Beta -
diese kennt jetzt ihre eigenen Variablen Sieben,Acht,Neun,Zehn,Elf,Zwoelf
und zusätzlich kennt sie auch:
Eins,Zwei,Drei,Vier,Fuenf,Sechs - ohne daß man sie übergeben mußte!}
end;
begin
{hier bin ich in Procedure Alpha}
Beta(120,243,77);{hier rufe ich Beta auf}
end;
D.h. eigentlich braucht man so "Procedures in Procedures" (man kann auch Functions und Procedures oder umgekehrt benutzen und auch mehrfach verschachteln) gar keine Werte übergeben, wenn sie sich aus der darüberliegenden Procedure ergeben. Also: Klar, wenn Alpha nun Beta aufruft, landet die Rücksprungadresse, sowie die Variablen von Beta aufm Stack - aber das würden sie auch, wenn Beta "außen" (außerhalb von Alpha) stehen würden. ABER: Wenn Beta "außen" wäre, würde sie die Variablen von Alpha NICHT kennen und müßte diese alle mit übergeben bekommen, bräuchte also MEHR Stack.
Das heißt: Mit Proceduren in Proceduren braucht man nicht mehr Stack (sondern maximal gleich viel), sondern kann stattdessen damit sogar Stack sparen.
zatzen hat geschrieben:DOSferatu hat geschrieben:Wenn man eine komplexe Packroutine baut, die am Ende größer ist als das, was man beim Packen spart, hat man nichts gewonnen, weil ja alles im gleichen Speicher liegt.
Es ist wahrscheinlich nichts neues wenn ich sage, dass die vielleicht 2K mehr Code (wobei, da ist ja auch noch ne Tabelle dabei, mit 512 Byte aber kaum nennenswert), bei ZSM sich lohnen, sobald man eben mehr als 2K komprimierte Samples hat. Ähnlich denke ich über ZVID2: Ich möchte da vielleicht um die 300K (gepackt) an Grafik reinklatschen, die Routinen werden sicherlich auch ziemlich klein sein.
Ich meinte es nur als allgemeine Aussage, NICHT konkret auf ZSM oder ZVID/ZVID2 bezogen. Es ist einfach so, daß ICH, wenn ich so Dinge baue, schon manchmal extrem kompliziertes Zeug zum Packen gebaut habe, das dann derart viel Rechenzeit UND Code brauchte, daß es die paar Bytes DATEN Einsparung nicht mehr gerechtfertigt haben - und dann habe ich den Kram auch mal wieder ausgebaut.
ABER: Es hängt natürlich IMMER vom konkreten Anwendungsfall ab: Man kann z.B. eine riesige unrolled Loop bauen, die vielleicht 3 kB groß ist und denkt: Achdumeinegüte, 3 kByte! - Aber wenn man damit z.B. die Performance einer Grafikausgabe verdoppelt, verdreifacht oder gar verzehnfacht - was den Unterschied machen kann, ob ein Spiel 3 FPS oder 30 FPS hat - dann sind die 3 kB zusätzlicher Speicher vielleicht DOCH gerechtfertigt.
Aber wenn eine Routine zum PACKEN da ist (und der EINZIGE Grund, warum man Daten packt, ist, benötigten Speicherplatz zu reduzieren - es gibt keinen anderen!), dann sollte diese Routine auch WIRKLICH Speicherplatzverbrauch reduzieren! Wenn die Daten von 10 kB auf 3 kB gepackt werden (7 kB gespart), die Packroutine selbst aber 20 kB groß wäre, hätte man statt 10 kB dann 23 kB Speicherplatzverbrauch UND zusätzlichen Rechenzeitverbrauch (für Entpacken) und hätte quasi gar nichts gewonnen. Wenn das Entpacken (wie bei RAR/ZIP usw. irgendwo extern vorher passiert, ist es ja egal, wie groß die Routine ist. Wenn das Entpacken aber quasi "live" passiert und live im gleichen Speicher liegt wie die Daten (d.h. die Routine nach dem Entpacken nicht weg wäre), dann sollte es eben nicht mehr belegen, als der Packalgorithmus spart. Das ist alles, was ich damit sagen will.
Mitunter kann eine primitive, aber dafür schnelle und kleine Entpackroutine mehr helfen als eine komplexe, aber dafür große und langsame.
Die Engine von GameSys2 belegt z.B. ca. 20 kByte Speicherplatz - hinzu kommt noch der Bytecode, den sie ausführt, um Figuren zu steuern. Da könnte man sagen: Ist ja Speicherverschwendung gegenüber direktem Coden.
Stimmt ja auch! -Nur dient GameSys2
NICHT dazu, Daten zu packen, sondern dazu, Daten auswechselbar zu machen und dazu, Dinge, die sowieso in Spielen gebraucht werden (wie z.B. Figur-Figur- bzw. Figur-Hintergrund-Kollisionen oder auch dynamische Speicherverwaltung von Figurendaten) gleich zur Verfügung zu stellen, so daß man es nicht für jedes Spiel (oder am Ende sogar für jede Figur!) einzeln neu coden zu müssen.
Das Gleiche kann man auch bei Sound sagen: Natürlich wäre es auch möglich, GAR KEIN Soundformat zu haben, sondern einfach nur ein paar Samples in den Speicher zu legen und direkt im Code einzelne Aufrufe einer Subroutine zu machen, die (gepitchte) Samples zum "Sound-Datenstrom" mixt - immer, wenn man meint, daß das nächste Sample gebraucht wird und KOMPLETT SO die Begleitmusik eines Spiels zu machen. - Möglich? Ja. Sinnvoll? Wohl kaum.
zatzen hat geschrieben:Übrigens an dieser Stelle nochmal das brenzlige Thema Hintergrundgrafik-Kompression bei der Blocksache:
Wieso ist das Thema "brenzlig"?
zatzen hat geschrieben:Wenn mein primäres Ziel nicht das Erreichen von möglichst hoher Performance ist, sondern möglichst viel Grafik im Speicher halten im Vordergrund steht: Dann ist es meines Erachtens sogar höchst sinnvoll, diese Kombination gepackter Hintergrund und nur teilweise restaurieren, denn: Wenn ich die Hintergründe faktisch definitiv packen will, dann schlägt der Performanceboost durch nur teilweises Restaurieren umso stärker zu Buche, im Vergleich dazu als wenn ich jedes mal ein Vollbild entpacken würde.
Ja, wie gesagt: Es ist immer ein guter Ansatz - gerade, wenn man im Realmode/VM86 und Heap (<640kB) codet, sich ein wenig zu überlegen, was man eigentlich braucht und was nicht - und
selbstverständlich gibt es hier keine "allgemeingültige" Aussage, sondern hängt immer davon ab, was für eine Art Programm/Spiel man machen will. Und daß man Grafikdaten bei fixen Hintergründen sinnvollerweise komplett anders anordnen/speichern würde als z.B. bei scrollenden Hintergründen, steht ja wohl außer Frage.
zatzen hat geschrieben:DOSferatu hat geschrieben:[vom Heap ins Codesegment kopieren]Klar kann man das. Wie im letzten Abschnitt erwähnt, ist das ja alles der gleiche Speicher. Und weil wir im Real Mode (nicht Protected Mode) sind, gibts hier keinen Speicherschutz/Segmentschutz. Das Codesegment ist nur dadurch definiert, wo wir CS hinlegen. Ginge es nicht, ins Codesegment zu kopieren, ginge ja auch kein selbstmodifizierender Code (was ich so oft mache, wenn ich keine Register mehr habe bzw für die Register anderweitig bessere Verwendung habe.)
Das wäre ja dann selbmodifizierend, im großen Stil sozusagen. Ein kleiner Performance-Haken könnte dann sein, dass das Kopieren den Codes auch eine kleine Weile dauert.
Wie bereits sehr oft erwähnt: Es ist IMMER ein Trade-Off zwischen Speicher und Performance. Alles gleichzeitig geht nicht. Einerseits das beste 3D-Spiel der Welt bauen, andererseits soll es nur 4 kByte groß sein - sowas wird z.B. nicht funktionieren. Wo man MEINT, daß einem selnstmodifizierender Code Vorteile bringt, kann man es ja einsetzen. Es stattdessen nur deswegen einzusetzen, weil "das schonmal jemand anders gemacht hat" oder weil es in einem
völlig anderen Projekt mal einen Vorteil gebracht hat, ist selbstredend sinnlos. Also, wenn man das macht, sollte man auch wissen, warum. Und wenn man es vermeiden kann (z.B. weil man eigentlich auch noch Register übrig hätte), sollte man es auch vermeiden.
Selbstmodifizierender Code ist keinesfalls ein "Allheilmittel" und kann (auch performancemäßig) arg nach hinten losgehen.
zatzen hat geschrieben:DOSferatu hat geschrieben:zatzen hat geschrieben:[KI upscaling]Davon kann man halten was man will, aber auf eine Weise interessant ist es schon. Wobei ich sagen muss dass ich ein Fan von 320x200 (oder x240) Pixelgrafik bin und nicht das Bedürfnis nach "remaster" Editionen habe.
Ja, wie gesagt: Es gibt inzwischen ECHTE Remaster-Versionen, die wirklich ausgehend vom Originalmaterial neu gemacht wurden, NICHT von den Pixelbildern "hochgezogen".
Aber auch hier bin ich so geprägt, dass 320x200 zu einem stimmungsvollen Spielerlebnis beitragen kann. Die Hintergrundgrafiken von Monkey Island II sind kunstvoll mit Filzstift gemalt, durch die niedrige Grafikauflösung werden sie etwas pixelig-unschärfer und wirken dadurch paradoxerweise fotorealistischer. Wenn diese nun hochauflösend zu sehen sind sehen sie wieder mehr nach Zeichnungen aus. Bei der Special Edition wurden die Grafiken scheinbar auch nochmals stilisiert, weiter weg vom Original, das hat auf mich wieder zu sehr den Effekt "Hintergründe nur gemalt, Figuren wie aus Knete gemacht". - Oder vielmehr, alles wie aus Holz geschnitzt und angemalt. Das mag ja vielen gefallen, Monkey Island II als Holzpuppentheater, aber ich bevorzuge die pixelige Version die Raum zur Interpretation lässt und somit letztlich realistischer wirkt.
Geht mir genauso. Vieles so "nachträglich aufpolierte" Zeug sieht im Nachhinein nicht besser aus, sondern nur irgendwie "künstlicher" und verliert dadurch etwas an Reiz. Das ist teilweise auch bei digital remasterten und nachberechneten Filmen so: Manche der ("analogen") Effekte wurden damals gemacht mit dem Wissen, daß Nebel und Auflösung verbergen würden, wie es wirklich gemacht wurde und damit dem Effekt seine Wirkung verliehen hat. Werden solche Filme nachträglich digital "nachgebessert", wird das, was (zur Realisierung des Effekts) eigentlich verborgen bleiben sollte, wieder klarer gemacht und die alten Effekte wirken billig und albern. Muß man nicht wirklich haben - verstehen aber auch sehr viele Leute leider nicht.
zatzen hat geschrieben:DOSferatu hat geschrieben:Vielleicht sollte ich mal, mit meinen neuen Engines ein neues Xpyderz machen, das gleich von Anfang an die ganzen neuen Dinge benutzt: Das 100%-ASM Menüsystem, neue Darstellungsroutinen und natürlich auch ISM und so. Aber Xpyderz war damals eigentlich nur ein Experiment meinerseits. Das Spielprinzip ist ja nicht gerade der Brüller.
4-Wege Scrolling hätte mich zumindest als Kind begeistert, eine große Welt erkunden und so. Deswegen hab ich ja auch Zelda gespielt. Oder Cauldron II auf dem C64 fand ich auch toll. Mein Einstieg in die Computerspielewelt war im Prinzip Super Mario Bros. und da konnte man ja immer nur von links nach rechts laufen, aber niemals zurück.
Ja, "vorwärts immer, rückwärts nimmer"[TM].
Naja, ich gehe davon aus, daß unter anderem auch DAS (dieses 1-Richtung-Scrolling) bei Super Mario dazu beigetragen hat, diesen intern sehr effizient gespeicherten Levelaufbau zu schaffen, der es ermöglicht hat, in dem sehr kleinen Speicher so wahnsinnig viel Inhalt unterzubringen. Wie der Macher da die Levels gespeichert hat, ist 'ne Wissenschaft für sich - das ist nicht einfach ungepackt angeordnetes Blockraster.
zatzen hat geschrieben:Hello World:
DOSferatu hat geschrieben:Der Rest der "Magie" funktioniert, indem man die Anzahl Zeilen, die die Zeichen haben können, in der Grafikkarte auf 2 Zeilen stellt. Der Textmode ist ja quasi 720x400 Pixel
War mir gar nicht so bewusst, diese Auflösung. Also hat ein Buchstabenblock komische 9x16 Pixel. Es ist schon ne Weile her dass ich mal nen eigenen Zeichensatz gemacht habe... Trotzdem messe ich die Breite Deiner Pixel gleichmäßig mit jeweils 4 "echten" Pixeln. Irgendwo stimmt da was in meinem Verständnis nicht. Vielleicht ist der Textmodus ja doch nur 640x400, die Ascii-Zeichen sind jedenfalls 8x16, gerade nochmal nachgesehen.
Ja, die Zeichen sind 8x16, aber die Grafikkarte stellt sie standardmäßig (bei Herc, CGA und bei VGA, nur nicht bei EGA) mit 9 Spalten dar, um die Lesbarkeit zu erhöhen. Das ist hardwaremäßig in die Grafikkarte eingebaut. Die 9. Spalte ist dabei entweder "leer" (Hintergrundfarbe) ODER ene Kopie der 8. Spalte (letzteres nur für Zeichen 192 bis 223 möglich und abschaltbar). Zeichen 192 bis 223 sind die "Grafikzeichen - damit so Linien nicht "unterbrochen" aussehen. Das ist der Grund, wieso man die "Rähmchen" beim 437er Zeichensatz, die nach links gehen, VOR 192 gelegt hat - weil die diese kopierte 9. Spalte nicht brauchen. Man kann die 9. Spalte übrigens auch abschalten, dann stehen die Zeichen wieder enger nebeneinander (werden also 8x16 dargestellt und die Auflösung ist 640x400).
Anmerkung dazu: DOSbox supportet die 9. Spalte standardmäßig nicht - wahrscheinlich aus Performancegründen. (DOSbox macht ja alles als Grafik, stellt also auch den Textmode als Grafik dar.) Es gibt aber einen alten "Hack" von DOSbox 0.72, bei dem die Leute (simulierten) Netzwerkkartensupport eingebaut haben und diese Version supportet im Fenstermode auch die 9. Textmode-Spalte
Eigentlich dachte ich, dieses Wissen über den Textmode und so weiter wäre inzwischen Grundwissen von so DOS-Freaks. Also, ja: Datentechnisch sind es 640x400 - aber die Grafikkarte stellt 720x400 Pixel dar und der 9. Pixel wird von der Grafikkarte selbst generiert (siehe oben). Diese Möglichkeit der 720er Breite ist auch der Grund, wieso der Grafikmodus 360x200 bzw. 360x240 noch zu den "normalen" zählt (die auf jeder VGA funktionieren). Weil das Timing für die 720er sowieso schon eingebaut ist.
zatzen hat geschrieben:DOSferatu hat geschrieben: - und so erhält man quasi einen 160x200 "Pseudo-Grafik" Mode:
Es sind 80 Zeichen, die aber aus zwei Hälften bestehen: Vordergrund- und Hintergrundfarbe sind jeweils der linke und rechte Pixel. Und man sieht nur die ersten 2 Zeilen des Zeichens, dann kommt die nächste Zeichenzeile.
Auch sehr interessant. Hätte dieser Modus gewisse Vorteile gegenüber einem EGA 160x200, oder waren Spiele wie die ersten von Sierra in Wirklichkeit vielleicht sogar in diesem Modus gehalten?
Vorteil gegenüber EGA: In EGA Grafikmodus sind die Pixel in Planes aufgeteilt. D.h. EGA sind eigentlich 4 monochrome Planes, die übereinander liegen und die 4 HINTEREINANDER (also auf die 4 Planes verteilten!) Bits ergeben jeweils eine der 16 Farben. Kannst Dir ja vorstellen, was für'n Spaß das ist, die Farbe EINES EGA-Pixels zu ändern.
Diesen Trick mit den Textmode hat man aber wohl eher bei CGA angewendet:
CGA hatte 16farbigen Textmode und nur 4-farbigen Grafikmode. Außerdem waren die Zeichen des Textmode nicht veränderlich. ABER: Standard CGA hatte nur 16kB Speicher oder glaub später 32kB - damit gehen aber keine 160x200, sondern nur 160x100. Wieso? Naja, 160x200 sind 32000 - aber man braucht für 2 "Pixel" ja trotzdem 2 Bytes, auch wenn eins der Bytes immer das gleiche "Zeichen" ist, also braucht man 64000 Bytes - das war bei CGA nicht eingebaut.
zatzen hat geschrieben:Aber ich sehe gerade, dass die Grafik selbst dort zwar auf 160x200 beschränkt ist, die Textausgaben aber in 320x200 aufgelöst sind. Dann ist es wohl insgesamt 320x200 Grafikmodus, und man wollte bei der Grafik einfach sparen. Oder einfach ein komischer Modus der zwar 320x200 Text aber nur 160x200 Grafik kann?
Kann ich nicht sagen, kommt darauf an, wie es die Programmierer gemacht haben. Wenn es nur 16 Farben sind, wird es kaum der berühmte Mode-X (oder Mode-Y) sein. Wegen seiner komischen Anordnung hat ModeXY die Eigenschaft, auch mehrere gleichfarbige Pixel gleichzeitig (d.h. mit einem einzigen Zugriff) setzen zu können, weil man bis zu 4 nebeneinanderliegende Pixel "chainen" kann. Das gilt aber nur für den
Zugriff, d.h. Darstellung ist trotzdem einzeln. D.h. man kann das Chaining auch immer an- und ausschalten und z.B. für eine Grafik das Chaining anschalten, damit man nur halb so viele Zugriffe braucht und für Text wieder aus, damit man die Pixel einzeln ansprechen kann.
Man sieht das auch gut, wenn man Xpyderz die LowRes-Einstellung benutzt: Da wird nur das Level LowRes gemacht, aber die Sprites und Anzeigen sind weiterhin HiRes - d.h. Auflösung bleibt 320x200, aber im LowRes-Chaining (immer 2 nebeneinanderliegende Pixel gleichzeitig) werden mit einem Zugriff 2 Pixel gleichzeitig gesetzt, um Rechenzeit zu sparen.
zatzen hat geschrieben:DOSferatu hat geschrieben:Mit DOSbox kann man recht gut sehen, was das Ding macht: Wenn man die Zyklen auf einen sehr geringen Wert stellt, sieht man, wie der das ganze Bild aufbaut. Das ist NICHT irgendeine Grafik, die der da irgendwo hin"kopiert". Der "malt" das Bild wirklich.
Und da denk ich immer: Die heutigen Hoch-/Skriptsprachen-Trottel, die für alles nur immer ihre riesigen Frameworks benutzen und noch nie mal Binär-Arithmetik gemacht haben, ob die wohl selber (und in ASM) so 'ne Linienziehroutine bauen könnten...
Jeder hat eben sein Spezialgebiet, und wer objektorientiert an künstlicher Intelligenz bastelt wird nicht viel mit Linienalgorithmen zu tun haben. Wobei eine allgemeine Programmiererfahrung auf jedem Gebiet sicherlich nicht schädlich ist.
Ja, aber wer Frameworks anderer Leute benutzt, ohne zu wissen, wie sie funktionieren, könnte sie auch nicht nachbauen und wäre immer darauf angewiesen, und davon abhängig, daß einem andere Leute erst irgendwelches Grundlagenzeug zusammenbauen, bevor man selbst überhaupt anfangen könnte, zu programmieren. Und GERADE wenn man darauf angewiesen ist, daß es Leute gibt, die sich mit Binärarithmetik und systemischer Programmierung auskennen, sollte man nicht so herablassend über diese urteilen (wie es viele dieser Skripter leider gerne tun, so als wären Systemprogrammierer dumme Höhlenmenschen und sie wären diesen gegenüber so erhaben) weil man ohne die nämlich garnix wäre.
zatzen hat geschrieben:DOSferatu hat geschrieben:[Zufallsgenerator]Kannst ja mal raten, woher der seinen Anfangswert (seinen "Seed") bekommt. Ist ja nicht schwer.
Timer? Ist ja die übliche Methode.
Ja, Timer natürlich.
Hier gleich zum nächsten Posting:
zatzen hat geschrieben:Ich habe mich mal wieder mit ZVID2 beschäftigt. Für andere Mitleser: Ein Grafikformat das die Grafiken in 4x4 Pixel Blöcke unterteilt und dann jeweils nur mit so viel Bit speichert wie nötig, entsprechend der Anzahl der verschiedenen Farben innerhalb eines 4x4 Blocks. Das ganze soll dann in Echtzeit entpackt werden.
Naja und ich rolle das ganze nochmal mehr oder weniger neu auf und habe jetzt schnell gemerkt, dass ich die Assembler-Darstellroutinen zuerst schreiben muss, denn das Format muss sich nach der Darstellungsroutine ausrichten und nicht umgekehrt.
Naja, das kann man so oder so machen. Um irgendwelche Routinen zu schreiben, braucht man ja erst einmal eine Idee, was sie machen sollen, und da kann es nicht schaden, dabei eine Art Format im Hinterkopf zu haben. Wenn aber das Format nicht irgendwelchen allgemeinen Formaten angepaßt sein muß, kann es dabei nicht schaden, es direkt auf die systemischen Möglichkeiten zuzuschneiden, um es sich selbst - und der Maschine - "leichter zu machen".
zatzen hat geschrieben:Jetzt habe ich noch eine Frage zum Thema Speicher an Nulloffset-Adresse anlegen. Du (DOSferatu) sagtest mir, ich muss das so machen (nachdem ich mit getmem 16 Byte mehr reserviert habe):
Zum einen habe ich da eine Verständnisfrage, zum anderen wäre es in meinem Fall vorteilhaft wenn man das Segment immer um genau 1 erhöhen könnte, unabhängig von Inhalt des Offsets, also:
Wenn ich mit getmem genau 16 Bytes mehr reserviere und nicht mehr, dann funktioniert das ganze nur, wenn offset maximal 15 ist - andernfalls würde der reservierte Speicher nicht ausreichen. succ(offset shr 4) ergibt immer 1, egal ob offset 0 ist oder größer, solange < 16. Also müsste eigentlich auch inc(segment); offset := 0 korrekt sein, zumindest ist es im Ergebnis identisch mit inc(segment, succ(offset shr 4)); solange offset < 16 ist.
Ja, WENN Getmem immer einen Offset <16 zurückgeben würde, wäre das so! Hier geht es ja darum, genau den Speicher zu "treffen", den man reserviert hat UND einen Offset von 0 zu haben. Das Ganze kann dadurch erreicht werden, weil man weiß, daß die Segmente sich "überlappen": Alle 16 Bytes fängt ein Segment an. Die Speicheradresse
$1234:$5678
ist also die gleiche wie die Adresse:
$1234+$567:$0008 (also $179B:$0008)
weil beides der physikalischen Adresse $000179B8 entspricht.
Und weil ich nicht WEIß, bzw. mich nicht darauf verlassen kann, in welcher Anordnung mir GetMem einen Pointer zurückliefert, "normalisiere" ich diesen Pointer, indem ich den den *16-Teil des Offsets zum Segmentanteil "rübernehme" und mir nur ein Offset zwischen 0 und 15 bleibt. Genauer geht es eben nicht, weil Segmente immer nur an jeder 16. Speicheradresse anfangen. Wenn ich dann nun einen Segmentanfang mit Offset 0 haben will, muß ich eben ein paar Bytes verschwenden. Ich kann aber nicht einfach den Offset, falls er <>0 ist, "weglassen", denn dann wäre die damit beschriebene Speicherstelle ja VOR dem Pointer - also muß ich das Segment um 1 erhöhen (was die physikalische Speicherstelle um 16 erhöht) und DANN kann ich den Offset =0 setzen, weil er dann mit Sicherheit innerhalb des reservierten Speichers liegt.
Und: Bevor ich jetzt analysiere, ob und in welchem Format mir GetMem da Speicher liefert (was im Endeffekt wahrscheinlich sowieso irgend einen INT-Zugriff macht) und evtl. sogar davon abhängt, ob es in DOSbox oder Windows XP oder echtem DOS gemacht wird, mache ich doch lieber diesen sehr einfachen obengenannten Trick und kann damit sicher sein, daß ich einen Pointer mit Null-Offset habe - egal, in welcher Anordnung ich den von Getmem erhalte. Denn bei GetMem ist nur definiert, daß es einen Pointer bestimmter Größe an einer bestimmten Stelle reserviert, aber nicht, wie der den Pointer zurückgibt.
Bei Zugriff auf einen Pointer macht die CPU intern immer (Segment SHL 4)+Offset, um die physikalische Speicheradresse zu bekommen - ob der "addierte Teil" mehr im Offset oder mehr im Segment liegt, um auf dieselbe Adresse zuzugreifen, ist der CPU egal. Der Programmierer jedoch kann das entscheiden und findet einen Offset 0 vielleicht praktischer, um bei Register-indizierten Zugriffen auf ein Segment nicht immer noch einen zusätzlichen Rest dazuaddieren zu müssen.
zatzen hat geschrieben:Nebenbei noch, welche Register könnte ich noch "missbrauchen" in einer Routine ohne Stackrahmen (d.h. ohne Variablenübergabe sondern einfach "procedure blabla; assembler ...")? BP ist mir geläufig, die ?X Register sind alle belegt, wie auch DS, ES, FS, GS.
Evtl. SP? Wobei mir das abenteuerlich scheint, den Stack-Pointer auf dem Stack zu sichern ist wohl keine gute Idee, allemal wäre ein sichern im Speicher (DS verändere ich nicht) denkbar, aber auch da sehe ich Konflikte wenn Interrupt-Routinen vorkommen.
Also, rein technisch gesehen hat SP die gleichen Eigenschaften wie z.B. DX - man kann auch damit rechnen, shiften, usw. ABER: SP ist, wie der Name
SP schon vermuten läßt, der
Stack
Pointer und zeigt an, wo der Stack gerade als nächstes beschrieben/gelesen wird - sowohl bei jedem Unterprogrammaufruf, als auch bei PUSH/POP von Werten oder Registern und selbstverständlich auch von JEDEM Interrupt!
Wenn man also wirklich SP anderweitig nutzen will, muß JEDER Interrupt abgeschaltet (gesperrt) werden und dürfen zwischendurch weder Unterprogrammaufrufe (CALL), noch PUSH/POP/PUSHF/POPF Befehle erfolgen. Und
ja, das geht - aber hier überwiegen eindeutig die Nachteile: Der Timer ist aus, die Tastatur wird nicht mehr automatisch abgefragt und, übrigens: IRQ5 (oder IRQ7 oder was auch immer man eingestellt hat) vom Soundblaster ist z.B. auch ein solcher Interrupt. Der käme dann natürlich auch nicht mehr.
Hardware-Interrupts sind ja so konzipiert, daß sie die Arbeit der CPU quasi
jederzeit (nach Abarbeitung des aktuellen Opcodes) unterbrechen dürfen, ihr Zeug machen und dann die CPU so hinterlassen "als wäre nichts gewesen". Und das geht nur, weil sie die Flags und die aktuelle Adresse, wo die CPU gerade liest, auf den Stack legen (und innerhalb der Interruptroutine vielleicht auch noch andere Register, falls die Routine die verändert) und dazu muß natürlich ein funktionierender Stack mit korrektem SP da sein.
Und noch zum dritten Posting:
zatzen hat geschrieben:Ich habe beim Programmieren wieder was gelernt.
Wahrscheinlich ist Dir der Sachverhalt schon bewusst, leider haben wir es aber nie thematisiert.
Ich habe meinen ASM Code mal kompiliert und wieder disassembliert. Dabei habe ich mich gewundert, warum bestimmte Sprunginstruktionen nicht so durchkommen, wie ich sie programmiert habe. Kurz gesagt:
Der Pascal-Compiler hat aus "jnz @loop" soetwas gemacht wie
Das handelt einem dann je nach Umständen unnötige Sprünge und Takte ein. Den Grund habe ich auch schnell begriffen - im 286er Befehlssatz gibt es nur 8 Bit bedingte Sprünge (short relative), und das Sprungziel war > 128 Byte entfernt. Der Pascal-Compiler würde die gleichen Workarounds möglicherweise auch für JCXZ basteln, aber hier existiert keine Inversion des Befehls, weshalb das nicht möglich ist.
Ja, das stimmt. Eine Umsetzung durch Parser in teilweisen (inline) 386er-Code kann gar nicht schnell genug kommen, oder? Ja, wie gesagt: Muß man sehen. Ich wollte es ja immer mal machen, aber irgendwie murks ich hier (falls ich überhaupt mal code) mit meinem eigenen Kram rum.
zatzen hat geschrieben:Ein eleganter Lösungsansatz wäre gewesen, entsprechend Inline einen 16 Bit Jump zu definieren. Leider kann man aber nicht einfach schreiben "dw 0850fh; dw offset @loop - offset @jump - 4", obwohl das für den Compiler eigentlich machbar wäre.
Ich weiß, irgendwie will der keine Offsets subtrahieren (hab sowas auch schonmal versucht). In so Inline-Code (also dem 386er-Assembler, den ich bauen würde) würde der dann auch die Befehle, die er nicht "ersetzt", trotzdem analysieren, um zu zählen, wieviele Bytes sie brauchen und diese dementsprechend addieren. So wüßte er jederzeit, wie die entsprechenden Offsets wären. Selbstredend geht das bei Vorwärtssprüngen nur mit 2-Pass-Assemblieren, (also 2 Durchgängen) das macht auch Pascal wohl so.
zatzen hat geschrieben:Also fiel mir vorerst nur selbstmodifizierender Code ein:
Code: Alles auswählen
mov ax, 0850fh; mov cs:[offset @jump], ax
{... zwischendrin kommt ein Sprung vor (Cache wird gelöscht) ...}
@jump: nop; jmp @loop
Aber vielleicht gibt es ja doch noch eine Möglichkeit, sich die zwei modifizierenden Befehle (und die Notwendigkeit den Cache zu leeren) zu sparen. Bei Dingen wie meinem blockweise Hintergrund-Restaurieren wären sonst nämlich massig Selbstmodifikationen nötig.
Momentan sehe ich da auch gerade keine andere Möglichkeit. Es ist nervig, ich weiß. OK, es GIBT natürlich eine andere Möglichkeit: Manuell alle Bytes zählen, die alle Befehle zwischen Sprung und Sprungadresse brauchen. Aber das ist ja wirklich Pain-in-the-Ass.
Und das Selbstmodifizieren müßte übrigens nur 1x gemacht werden, danach wäre der Code ja so wie er soll. D.h. die Routine hätte dann einen Init, der nur 1x ausgeführt werden muß. Aber das ist eben alles nur eine "Krücke" und für "den echten Shit" wäre ein 386er-Assembler, der so Sachen automatisch im Code abändert, echt mal hilfreich.
zatzen hat geschrieben:JCXZ bekommt in diesem Zusammenhang eine besondere Nützlichkeit: Man kann damit testen, ob die Modifizierung eines bedingten Sprungs nötig ist, indem man diesen beim Programmieren probeweise durch JCXZ ersetzt.
Stimmt.
zatzen hat geschrieben:Ich habe nochmal in diesem Thread gestöbert und bin auf Deine Erklärung gestoßen dass man auch Variablen (und wie ich festgestellt habe auch Register) mittels "dw" (oder db, dd) in den Code einlassen kann. Mir ist nur etwas unklar, wie der Compiler das handhabt: Den Wert einer Variablen kann er beim kompilieren nicht wissen, also kann es eigentlich nur der Offset im Datensegment sein.
Offset im Datensegment, wenn es eine Datensegment-Variable (global) ist. Die lokalen Variablen werden als relativer Offset gespeichert. Benutzung einer lokalen Variable (also entweder im Header oder mittels var innerhalb einer Procedure/Function angelegt) führt dazu, daß sie intern durch einen [BP+LokalerOffset] Zugriff ersetzt wird,
Register kann man nicht im Code als Offset einlassen - das ist totaler Blödsinn. Register liegen nicht im Speicher, sondern sind Bestandteil der CPU. Daß dw solche Dinge in SUBROUTINEN (teste es mal im Hauptprogramm, da geht es nicht!) machen kannst, bzw der Pascal-Assembler es zuläßt, liegt daran, daß Pascal es ermöglicht, Registerinhalte vom Hauptprogramm automatisch zu übertragen - z.B. für Procedures, mit Typ "Interrupt". Diese Offsets werden Dir also in Assembler nichts bringen, sondern geben lediglich Werte zurück, die aus Hochsprachen-Fragmenten übrig sind. Pascal legt diese Register also nur als eine Art Variablenname an, damit diese Bezeichner dann nicht ungültig sind.
zatzen hat geschrieben:Demnach müsste er bei Registern auch nicht deren Inhalt speichern, sondern den Wert als Offset interpretieren.
Definitiv nicht. Ob ein Registerwert von der CPU wie ein Offset zu irgend etwas wahrgenommen wird, liegt ausschließlich daran, wo es eingesetzt wird:
Code: Alles auswählen
mov AX,BX {AX erhält den Wert von BX}
mov AX,[BX] {AX erhält den 16bit-Wert, der an Speicherstelle DS:BX steht}
Im zweiten Fall ist BX also ein Offset zum Segmentanfang (ohne Segmentangabe meist DS).
Ein Register hat also keinen "Offset", weil es nicht in irgend etwas "liegt", zu dem es einen Abstand haben kann. Es liegt in der CPU und hat eine Nummer.
zatzen hat geschrieben:Aber beim Kompilieren ist ja der Wert wiederum unklar. Rätselhaft. Gerade ins Kompilat geguckt: Bei Registern kommt einfach 0 raus, jedenfalls bei AX. Wozu kann man dann überhaupt Register in den Code bringen?
Ja, wie gesagt: Erklärung oben.
zatzen hat geschrieben:Jedenfalls
könnte man obiges Problem auch auf diese Weise lösen (wie wir sehen werden tatsächlich NICHT), es wäre aber umständlicher und es bräuchte je nachdem für jeden Fall eine extra globale Word-Variable:
Code: Alles auswählen
mov ax, offset @loop; sub ax, offset @jump + 4; mov jumpamount, ax {vorbereitend irgendwo oben im Code}
dw 0850fh; dw jumpamount {später im Code in einem Teil die oft aufgerufen bzw. angesprungen wird}
Und HALT! Genau das wird so nicht funktionieren, weil ja nicht der Inhalt von "jumpamount" in den Code einfliesst sondern der Offset... Naja ich lass das mal stehen, hab immerhin nochmal etwas draus gelernt.
Ja, ist eben nicht so einfach. Gerade das mit den Sprung-Offsets (oder allgemein mit Offsets) muß man sehr aufpassen.
Erklärung dazu: bei "mov ax, offset @loop" wird der Offset von @loop zum Segmentanfang von CS zurückgegeben. Bei relativen Sprüngen wird aber der Offset (d.h. Abstand) zwischen dem Byte direkt nach dem Sprungbefehl und dem Byte, wohin gesprungen werden soll, gebraucht.
zatzen hat geschrieben:Übrigens, noch eine kleine Sache: Ich habe jetzt bei Segment-Overrides in Zusammenhang mit BP keine Probleme gehabt. Mein Fehler war, dass ich bisher z.B. "db 65h; mov ds:[bp], ..." geschrieben habe. Wenn ich nur "db 65h; mov [bp], ..." schreibe funktioniert das mit dem Override tadellos.
Ja, aus zwei Gründen. Erstens: Im Falle BP (oder irgendwas in Kombination mit BP muß es SS sein, nicht DS.
Und zweitens: Ja, und zwar weil er, wenn Du BP benutzt, er normalerweise GAR KEIN Segment hinschreibt (dann aber von der CPU automatisch SS benutzt wird) und dadurch funktioniert dieser getrickste der Segment Override. Würdest Du aber "db 65h; mov [bp+Beispiel];" schreiben und Beispiel wäre eine globale Variable, wüßte der Assembler: Aha, die liegt an DS, also muß ich DS davorschreiben, weil ja BP normalerweise SS hat (wenn man nix davorschreibt). Und DANN würde das db 65h nur einen WEITEREN Segment Override DAVOR machen und im Speicher hätte man dann quasi sowas:
mov GS:DS:[bp+Beispiel] - und damit würde das GS: ignoriert werden, weil danach noch das DS kommt.
Der Trick, DS oder SS davorzuschreiben, wenn man die $64/$65 Segment-Overrides benutzen will, ist, daß man dan ursprünglichen Befehl dazu "zwingt", GAR KEIN Segment davorzuschreiben, indem man im Quelltext genau das Segment davorschreibt, das er sowieso bei diesem Zugriff (der in den eckigen Klammern steht) benutzen würde - denn dann läßt der Pascal-Assembler es nämlich aus Rationalisierungsgründen weg und somit greift das mit dem Byte getrickste $64 oder $65. Und man kann natürlich Glück haben, daß er auch kein Segment davorschreibt, wenn man selbst keins schreibt - aber dann muß man aufpassen, was in den eckigen Klammern steht, sonst kann man böse in die Falle gehen!
Andersherum geht es auch: Wenn man z.B. einen Befehl hat, der zwecks modifiziertem Code auf verschiedene Segmente zugreifen kann (sowas benutzt GameSys2 bei einigen Befehlen) und das "normale" Segment wäre DS, dann muß man den "Override" für DS als Byte (mit db) davorschreiben, weil z.B. ein einfaches "mov AX,DS:[BX]" (statt "mov AX,[BX]") NICHT dazu führt, daß er das $3E (für DS) davorsetzt, weil er ja weiß, daß das redundant ist.
Es ist also praktisch nur eine List, den Assembler dazu zu bringen, "sein" Segment-Override-Präfix wegzulassen, damit man selbst eins setzen kann, das dann beachtet wird.
zatzen hat geschrieben:Und noch eine Analyse (ja, ich habe den Text hier mittlerweile schon mehrfach editiert):
Du sagtest mir mal, dass ich einfach Glück gehabt hätte wenn ich in einer Assembler-Prozedur RET verwende und mir nicht überlege ob RETF vielleicht richtig wäre. Nun, ich selber kann es ja auch schlecht einschätzen, zumal der Compiler nicht soetwas wie "CALLF" kennt mit dem man das selbst festlegen könnte.
Stimmt, CALLF gibts nicht. Der Befehl dafür heißt:
zatzen hat geschrieben:Dafür scheint er aber selbständig die richtigen Entscheidungen über RETF/RETN zu treffen, wenn man einfach nur RET schreibt: In meinem Code steht einfach nur RET, aber der Compiler hat ein $CB draus gemacht, also ein RETF. Schreibe ich dagegen explizit RETN geht das so auch ins Kompilat ein mit $C3.
Natürlich ist der Compiler nicht total doof. Es ist so: Greift man auf eine Routine innerhalb des gleichen "Codeblocks" zu (also etwas, das zwischen asm und end; liegt), weiß er, daß da NEAR ausreicht, macht NEAR calls und bei RET setzt er IMMER NEAR - niemals FAR!
Greift man auf eine Routine außerhalb des Codeblocks zu (also z.B. irgendeine Pascal-Routine) wird er sie so aufrufen, wie sie definiert ist: Ist im Header $F definiert ODER hinter der Routine steht ;FAR;, wird er sie mit einem FAR CALL aufrufen (weil er weiß, daß die Routine auch ein FAR RET haben wird), anderenfalls hängt es davon ab, wie das Programm strukturiert ist. Routinen innerhalb von
Units sind glaub immer FAR, weil die ja von jeder Stelle aus aufrufbar sein müssen, auch wenn man mal nicht im gleichen Codesegment ist. Das hängt damit zusammen, daß eine Unit ja schon vorcompiliert ist. Routinen im gleichen (Haupt-)Programm können je nach Compilerschalter {$F-}/{$F+} entweder NEAR oder FAR sein.
Der Compiler wird sie natürlich so aufrufen, wie sie definiert ist. Prinzipiell könnte man JEDE Routine, die im GLEICHEN Codeblock ist mit NEAR oder FAR aufrufen. Das Problem ist dann nicht der Aufruf, sondern der Rücksprung! Wenn man eine FAR-Routine mit NEAR-Call aufruft (wenn sie im gleichen Codeblock ist), geht der Aufruf zwar noch glatt - aber beim Rücksprung (wenn sie FAR ist, hat sie einen FAR RET (bzw RETF) im Code stehen) will sie einen Offset UND ein Segment vom Stack holen, der NEAR-Call hat aber nur einen Offset auf den Stack gelegt und sie holt 2 Bytes mehr vom Stack als sie dürfte. Schon haben wir den Salat!
Andersherum; Wenn man eine NEAR-Routine mit einem FAR-Call aufruft, legt der CALL zuerst das aktuelle Segment (CS) auf den Stack und dann den aktuellen Offset (also IP) und springt dann zur Routine. Das geht auch gut. Aber eine NEAR-Routine hat auch nur einen NEAR-RET und der holt NUR den Offset wieder zurück, das vorher gespeicherte CS bleibt auf dem Stack und ist dort "zu viel": Wenn irgend etwas anderes jetzt sein gespeichertes Zeug vom Stack zurückholt, holt es erst stattdessen zuerst den Inhalt von CS, ohne es zu wissen - und das ist dann sicher nicht das, was es haben wollte.
(Anm.: Um eine FAR-Routine mit NEAR aufzurufen, müßte man nach dem Aufruf noch add SP,2 machen - oder irgendwie anderweitig das überflüssige Word vom Stack holen.)
Daß man eine FAR-Routine, die in einem ANDEREN Codeblock (Segment) liegt, natürlich NICHT mit einem NEAR-Call aufgerufen bekommt, dürfte klar sein: Um auf Code in einem anderen Segment zu springen, muß man dieses andere Segment ja angeben - und ein NEAR Call gibt ja nur den Offset an (für das aktuelle Segment).
Und ob man RET oder RETN schreibt, wenn man weiß, daß es ein NEAR-Rücksprung wird, ist egal - der Assembler macht aus beidem den gleichen Code. Aber erstens hab ich's ganz gerne, daß ich auch SEHE, daß das ein NEAR RET wird und zweitens gehe ich damit sicher, daß der Compiler/Assembler keine Scheiße baut.
An dieser Stelle auch mal ein lustiger Trick, wie man eine FAR-Routine (z.B. im Hauptprogramm oder einer Unit) in Assembler starten kann, wenn man nur ihren Pointer hat (z.B. in EAX):
Ja, es belästigt kurz den Stack, aber jede andere Möglichkeit, die ich mir vorstellen kann, ist komplizierter.
Wozu könnte man das brauchen?
Code: Alles auswählen
procedure @Koche;begin end;
procedure @Backe;begin end;
[...]
const TU:array[0..5]of Pointer=(@Koche,@Backe,@Putze,@Wasche,@Singe,@Tanze);
[...]
asm
{in BX steht Nummer der Routine}
shl BX,2;
push ES;db $66;push word ptr [TU+BX];RETF;pop ES;
end;
Achja, und falls den Routinen noch Variablen übergeben werden sollen, muß man die vorher pushen...
Nur eine der vielen komischen Ideen, die ich so habe.
Mein schicker Skriptinterpreter, mit dem ich z.B. (unter anderem) so eine GUI steuern könnte, kann externe Routinen aufrufen und denen Daten aus diesem Skript (oder von woanders hergeholt) übergeben.
Einen ähnlichen Trick benutze ich z.B. auch öfter mal, wenn ich eine Routine aufrufe und nach Ende soll die an verschiedenen Stellen weitermachen, die ich schon vor dem Aufruf weiß: Da pushe ich den offset der Stelle, wohin sie danach soll, auf den Stack und rufe die Routine nicht mit CALL sondern nur mit JMP auf. Die Routine hat aber am Ende ein RET und wird sich daher vom Stack holen, wohin sie "zurück" soll. Das kann man auch benutzen, um eine Routine wahlweise per CALL oder JMP aufzurufen. Denn manchmal ist es so, daß bestimmte Dinge im Programm öfter gebraucht werden, aber jedesmal müßte man dann die Routine mit der gegenteiligen Bedingung überspringen und einen CALL machen. Da kann man doch auch das machen:
Code: Alles auswählen
push CS:offset @dahinter;jc @routine;add sp,2;@dahinter:
Was ist der Witz daran? Naja, wenn Carry gesetzt ist, springt er zur Routine, diese wird beim Beenden "@dahinter" vom Stack holen und dort landen. Wenn Carry gelöscht ist, pusht er nur einen Wert aufn Stack, macht den Sprung nicht und addiert danach den Stack um 2 (was den Wert wieder wegmacht).
Die eigentlich normale Alternative wäre:
Ja, da ist ein Befehl weniger drin - ABER: egal, ob Carry gesetzt ist oder nicht, wird hier gesprungen. Und ja, das ist natürlich nur dann sinnvoll, wenn nicht zusätzlich dieser lustige 286er "ich kann nur 8-Bit-Offsets" Kram passiert.
Auch schonmal erwähnt:
In GameSys2 habe ich viele "Befehle" drin, die einen Rückgabewert haben. Der Befehl selbst wird immer per CALL aufgerufen. Die Routine, die den Rückgabewert wieder zurückschreibt, wird auch per CALL aufgerufen.
Somit wäre das so gewesen:
Code: Alles auswählen
@SchreibeErgebniswert:
{tue, was es muß, um Ergebnis zu schreiben}
RET{a}
{...}
@NaechstenBefehlAufrufen:
call @Befehl42;
{...}
@Befehl42:
{tue, was Befehl 42 so tut}
call @SchreibeErgebniswert;
RET{b}
Klingt plausibel und wie ordentliche Programmierung - ABER:
Wenn das ausgeführt wird, wird nach SchreibeErgebniswert mit RET{a} zurückgesprungen - und wohin? Genau vor RET{b}. D.h. ein RET, nur um danach ein weiteres RET zu machen - das ist doch Schwachsinn... - also hab ich an allen Stellen, wo das so war, das gemacht:
Code: Alles auswählen
@SchreibeErgebniswert:
{tue, was es muß, um Ergebnis zu schreiben}
RET
{...}
@NaechstenBefehlAufrufen:
call @Befehl42;
{...}
@Befehl42:
{tue, was Befehl 42 so tut}
push CS:offset @NaechstenBefehlAufrufen; jmp @SchreibeErgebniswert;
Schon hab ich einen CALL durch ein JMP ersetzt und ein RET eingespart und von der Sache her macht das Programm das gleiche.
zatzen hat geschrieben:Für meine Routine mit den Modifikationen habe ich jetzt eine passable Lösung, beim ersten Durchlauf wird ganz nach unten gesprungen, wo die Modifikationen stattfinden, dabei wird auch der Sprung oben umgewandelt in einen sinnvollen ersten Befehl der Routine, und unten steigt die Routine dann eben vor dem Modifikationspart mit RET aus. D.h. man muss die Routine einmalig aufrufen, damit sie sich verwandelt. Das Prinzip hast Du mir vorgemacht vor nicht langer Zeit, nur etwas komplizierter. Danke dafür!
Ja, ich erinnere mich: Das war bei dem Beispiel, wie man DS zurückkriegen kann.
zatzen hat geschrieben:Ich habe nur gerade gemerkt: Man muss vor dem Programmstart immer dran denken, das Ding neu zu kompilieren, sonst legt die Routine (weil sie noch modifiziert im Speicher liegt) einfach los und will Grafik anzeigen, aber man hat keine Parameter angegeben oder ist nicht noch nicht im Grafikmodus...
Naja, im Gegensatz zu Dir compiliere ich nach jeder Änderung sowieso neu.
zatzen hat geschrieben:Okay bessere Idee: Nach dem Modifizieren wird direkt wieder an den Anfang der Routine gesprungen, die diesmal modifiziert ist. So passiert die Modifikation einfach automatisch und einmalig beim ersten Aufruf der Routine, man muss sie nicht extra nur für den Modifikationszweck einmal aufrufen.
Genau. Dann braucht sie beim allerersten Aufruf eben ein paar Mikrosekunden länger für die Modifikationen, aber macht trotzdem, was sie machen soll.
So, dann ist das auch mal beantwortet.
Die letzten Einträge zu Deinem "Trackermodul-Engine-Thread" (den Du ja durch einen neuen ersetzt hast für die neue Engine) werde ich aber auch noch beantworten.