Trackermodul-Engine (sehr einfach)

Diskussion zum Thema Programmierung unter DOS (Intel x86)
Benutzeravatar
Dosenware
DOS-Gott
Beiträge: 3218
Registriert: Mi 24. Mai 2006, 20:29

Re: Trackermodul-Engine (sehr einfach)

Beitrag von Dosenware »

weil ein Sample Pascal-typisch
nur 65528 Byte belegen kann
Pchar kann glatte 65536bytes, wenn ich mich recht erinnere...
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 520
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Dosenware hat geschrieben: Pchar kann glatte 65536bytes, wenn ich mich recht erinnere...
getmem geht bis 65535... gerade probiert.

Allerdings steht geschrieben:
"Restrictions:
The largest block that can be safely allocated on the heap at one time is
65,528 bytes (64K-$8)."
mov ax, 13h
int 10h

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

Re: Trackermodul-Engine (sehr einfach)

Beitrag von DOSferatu »

zatzen hat geschrieben:Der Hase Jockel :)
Wie die bloß auf den Namen gekommen sind...
Vielleicht arbeitet bei denen jemand der mein Liedchen gehört hat.
Ja, ich hab's mir gestern auch wieder einige Male angehört.
Ich mag u.a. ja diesen plötzlichen Break, mit der Trompete und dem Klavier an der einen Stelle...
zatzen hat geschrieben: [...]
Und wenn ich es richtig sehe ist die "Gefahr" nur da, wenn das WORD, das den
Vorkomma-Anteil enthält, einen Überlauf hätte. Bei dir ist das mit dem AND 31
sowieso verhindert, bei mir wird es nicht auftreten, weil ein Sample Pascal-typisch
nur 65528 Byte belegen kann und es bis zum Überlauf noch 8 Byte wären, diese aber
wahrscheinlich nie überschritten werden weil eine Schrittweite von 8 schon höchst
unwahrscheinlich ist, vor allem bei einer Mixfrequenz von 16kHz. Da müsste schon
jemand mit dekadent hohen Samplerates für Samples kommen (z.B. 44.1 kHz)
und dann noch mehr als eine Oktave höher gehen als die Grundfrequenz.
Das mit dem AND 31 war nur ein Beispiel für die WAVES. Wie Du weißt, supporte ich auch
Samples. Und Samples werden sowieso getestet, ob sie die Größe überschreiten.
Und außerdem werden sie zur Sicherheit auch nach "Überlauf" getestet:

Code: Alles auswählen

add EBP,Addierwert
adc BP,0
jc @Sample_Over
cmp BP,DX
jae @Sample_Over
So ungefähr.
Die zweite Addition erhöht nur das untere Word, so hat man noch die Möglichkeit, hier einen Overrun zu testen -
ABER: Man kann ihn natürlich nicht bei der ersten Addition testen, wo er viel wahrscheinlicher ist.
Aber wie Du selbst bereits erwähntest: Schrittweite wird nicht SO groß werden.
Meine Samples dürfen z.B. maximal 65520 Bytes groß werden. Bei mir hat das aber nichts mit der GETMEM-
Funktion zu tun (auf die Du Dich wahrscheinlich beziehst, weil Du von 65528 Bytes sprichst).

Samples liegen bei mir ja in einem Doppel-4bit-Puffer, der eine "beliebige" Größe haben kann - Hauptsache,
der Heap reicht aus. Die Startposition (20bit!) eines abgelegten Samples wird natürlich irgendwo vermerkt.
Soll dieses Sample gespielt werden, wird seine Position so umgerechnet, daß die oberen 16 Bit einem Segment
zugeordnet werden und die unteren 4 Bit dann zur Startposition addiert werden. Ja, ich weiß - das ist schon
wieder krasser, als es sein müßte.

Naja, aber dafür kann ich Samples hinlegen wo ich will und brauche trotzdem keine zusätzlichen Segmentwechsel.

Und ja, natürlich besteht bei einem Sample, das mit sehr hoher Frequenz abgespielt werden soll, immer die
"Overrun"-Gefahr - in meinem Fall, wenn mehr als 15 Bytes übersprungen werden, d.h. der Addierwert >$000FFFFF
(bzw natürlich gedreht: $FFFF000F) wird. Solche Extremfälle berücksichtige ich aber nicht, zugunsten der
Performance. Wenn man Sound bis zur Unhörbarkeit verzerrt, dann fällt auch nicht mehr auf, wenn da mal ein Bit
glitcht...
zatzen hat geschrieben:Bleibt aber noch die Rotiererei mit CX und AX.
Prinzipiell braucht man INNERHALB der Schleife gar nichts rotieren. Bei mir jedenfalls nicht.
zatzen hat geschrieben:Und ich brauche glaube ich drei weitere Register wenn ich das vermeiden will und
auch auf den Stack verzichte. Du benutzt z.B. BP.
Wenn man in reinem Assembler programmiert und nicht auf lokale (auf dem Stack gesicherte)
Variablen zugreifen will, ist die Benutzung von BP (und EBP) kein Problem. Der "Mißbrauch" von BP
mit den ENTER / LEAVE Befehlen in Hochsprachen (die beim Eintritt in eine Subroutine automatisch
BP auf den derzeitigen Inhalt des Stackpointers (SP) setzen), ist ja in Assembler nicht nötig.
Man braucht nur die Register entsprechend retten.
zatzen hat geschrieben:Wären SS und SP auch zu haben wenn man diese Register vorher im Speicher sichert?
Prinzipiell kann man JEDES Register benutzen, bei SS/SP würde ich aber davon abraten. Der Stack wird ja
gebraucht - z.B. zum Sichern von Rücksprungadressen (bei Aufrufen mit CALL) und natürlich nicht zuletzt auch
zum Sichern von CS, IP und Flagregister, wenn ein Interrupt auftritt.
Verzichetet man auf CALL, PUSH, POP und schaltet zusätzlich alle Interrupts ab, könnte man auch bedenkenlos
SS und SP benutzen, sofern man sie am Ende wieder vernünftig herstellt.
Meines Erachtens überwiegen hier aber eher die Nachteile, deshalb fasse ich den Stack nicht an.
zatzen hat geschrieben:Uff, das erscheint mir gerade sehr kompliziert, DS wird ja auch verändert...
Naja ich versuch mal mich selber schlau zu machen.
Naja, DS zu ändern ist ja kein Problem. Und außerdem hast Du ja auch noch ES, FS und GS.
DS muß eben nur am Ende wiederhergestellt werden, wenn man es ändert. Und: Mit geändertem DS hat man
natürlich dann keinen Zugriff auf globale Variablen - bzw., wenn man darauf mit geänderten DS zugreift, erwischt
man eine ganz andere Stelle als man wollte. Pascal "sieht das ja nicht", wenn man da mit DS rummurkst und generiert
den Code so, als wäre DS noch immer da, wo es war.

In ISM habe ich auf das Ändern von DS verzichtet, da ich oft auch Zugriffe auf globale Variablen brauche.
In anderen Projekten (GameSys2, Arcade01) murks ich auch mit DS rum - man muß es eben nur vernünftig sichern
und wiederherstellen.
zatzen hat geschrieben:Was Source Code angeht, ich bin ja froh dass ich durch meinen eigenen durchblicke.
Deshalb schreibe ich ja auch so betont übersichtlich mit Einrücken und Zeilen frei
lassen. Das macht auch irgendwie Spass.
Naja, das Spaß ist für mich die Programmierung. Sourcen formatieren ist für mich eher Arbeit. Sowas mache ich
im Grunde nie. Meine Sourcen kann man einfach niemandem anbieten: Codeblöcke, spärlich dokumentiert - außer,
wenn ich mal einen Trick mache, der zu freakig ist, um mich später daran zu erinnern, was das sollte.

Es gibt viele Dinge, die ich immer wieder "so und nicht anders" löse, daher reicht mir dann meist ein Blick auf einen
solchen "Code-Block" um zu sehen: "Ahja, das schon wieder..." - Daher keine Dokumentation an der Stelle nötig.

Wenn ich Code zeilenweise schreibe, entgeht mir immer der Überblick über das "große Ganze", weil ich dann immer
hin und her scrollen muß. 20 ASM-Befehle tun nun mal nicht gerade so viel, um eine ganze Funktionsweise darin zu
erkennen (inner loops mal ausgenommen). Aber so hat eben jeder seinen eigenen Stil. Und wenn man dann auch noch
den dazugetanen Byte-Code nimmt, den ich nur dokumentiere, wenn nicht klar ist, was der macht... (ein db $66 vor
einem Befehl dokumentiere ich z.B. nicht mehr...) dann wird das ganze noch kryptischer...

Das ist nicht dazu gedacht, daß andere meinen Code nicht lesen können - es ist eher so, daß ich keine wertvolle
Programmierzeit mit so "Menschenzeug" verschwenden will wie den Code schick zu machen. Mit anderen Worten:
Ich bin dazu zu faul, weil es für mich selbst nicht nötig ist und sowieso sonst niemand diesen Source sehen wird.
zatzen hat geschrieben:Aber ich stecke da einfach nicht so dauerhaft
drin und wenn ich eine paar Wochen oder Monate nicht programmiere würde ich da gar
keinen Zugang mehr finden ohne übersichtliche Formatierung und ausführliche Kommentare.
Ja, wie gesagt: Bei Sachen, die "zu crazy" sind, dokumentier ich schon mal.
Aber es gibt immer weniger Sachen, die "zu crazy" für mich sind...
zatzen hat geschrieben:Aber so geht's und es funktioniert sogar besser als früher, als ich noch viel mehr programmierte.
Ja, bei mir ist es eher das Problem, daß ich mich konzentrieren können muß, wenn ich gescheite Dinge in
Assembler machen will. Und nach 9h Job und noch Arbeitsweg hin/zurück ist nix mehr mit Konzentrieren. Da bin ich froh, wenn ich noch geradeaus laufen kann und meinen Sessel finde.
zatzen hat geschrieben: [...]... ein bisschen die Vorzüge des Formats demonstriert. Wenn man
da überhaupt Vorzüge sehen will. Klar, die Patternkompression ist gut, macht sich
aber nur bezahlt wenn man wenig Sampledaten hat oder aber sehr viele Patterns.
Ansonsten ist das ganze einfach nur ein sehr abgespeckter 8-Kanal-MOD-Player
der keine Effekte kann, absolut nicht zeitgemäß - sowas programmiert man heute
eher auf einem raspberry pi aber mit vollem Effektumfang - aber ich bin nur ein
kleiner Nebenprogrammierer der ab und an Spass an der Sache hat, und jetzt hab
ich eben meinen eigenen Player, was ich immer schonmal haben wollte.
Ja, bei Selbstgemachtem weiß man wenigstens "was es macht". Deshalb bin ich auch ein
großer Freund davon, möglichst alles selbst zu machen. Klar, man kommt zu fast nichts und so (aber mich
hetzt ja auch keiner). Aber der Vorteil ist, daß man sich nicht auf fremde Formate für Eingangs- und
Ausgangswerte einlassen muß. Fremdsoftware hat oft den Nachteil, daß man, wenn man sie einsetzen will,
so viel Aufwand für die "Schnittstelle" zwischen Fremdsoft und Eigensoft betreiben muß, damit sie
kompatibel wird - daß man am Ende es gleich hätte selber schreiben können. Und daß am Ende die
aufwendige Schnittstelle die ganze Power frißt, die man durch den Einsatz der Fremdsoft gewinnen wollte.

Programmiererisch erfinde ich gerne mal das Rad neu - auch wenn es andere und bessere Sachen, die das
gleiche tun, bereits gibt. Heutzutage, bei der schieren Menge existierender Software, ist es ohnehin ziemlich
selten, etwas zu machen, das wirklich noch nie so oder so ähnlich gemacht wurde.
Jedes Jump'n'run ist quasi eine Variation von Super Mario.
Jedes Shoot'em'up eine Variante von Gradius.
Alle Point&Click Adventures sind eigentlich Maniac Mansion mit anderer Geschichte.
Und bei Anwendungen ist es natürlich das Gleiche: Es ist ja nicht so, als hätte ich den ersten Level-Block-Sprite-Editor der Welt geschrieben (sogar ich selbst hab schon mehrere geschrieben, allein auf PC ist es mein vierter, von C64 ganz zu schweigen). Und es ist ja auch nicht so, als gäbe es nicht schon hunderte Tracker auf der Welt, die irgendwie ja alle eine Variation des ersten 4stimmen-MOD-Trackers vom Amiga sind.

Beim "Selbst-Programmieren" kann man aber die Sachen den eigenen Bedürfnissen anpassen. Nützliches oder neues hinzufügen, nicht genutztes weglassen. Ich bin auch nicht absichtlich jemand, der eigene Formate um der Sache willen "erfindet"/entwickelt. Es geschieht dann eher aus der Notwendigkeit heraus.
Ich bin z.B. mit dem Fileformat für meine Musikdaten (von ISM) schon wieder unzufrieden. Habe lange dran entwickelt, auch einen Saver/Loader dafür geschrieben - und spiele mit dem Gedanken, das alles wieder umzuschmeißen, weil es am Ende schon wieder zu sehr ausartet. In letzter Zeit verfolge ich gern das "Keep-It-Simple" Prinzip (auch wenn es manchmal nicht so aussieht). Zu fitzelige komplizierte Formate können eine fiese Fehlerquelle sein und mein derzeitiges Fileformat gehört schon wieder entfitzelt. An der Stelle, wo der erforderliche Programmcode das Vielfache dessen übersteigt, was durch Komprimieren/Zusammenfassen der lesenden Daten gespart werden soll, fange ich in letzter Zeit an, abzuwägen, ob das denn sein muß.

Mit solchen Sachen (es zu kompliziert zu machen) habe ich mir früher manchmal interessante Ansätze "zerschossen" - alles ist dann einfach ins Uferlose ausgeartet... Ich kann ja mal die Doku zu meinem Format ISDP3 oder 4 schicken (Übertragungsformate für serielle Schnittstelle für Daten (Befehle und Steuerstream) von Spielen). Das ist ein derartiges Bit-Gefrickel, da schnallst Du ab... Aber der Programmcode, der den Kram ausführen mußte, war eben auch ein riesiger Monolith...

Ich habe es vielleicht schonmal erwähnt: Mein GameSys2 müßte eigentlich GameSys5 heißen - so oft hatte ich das Konzept radikal geändert, bis ich endlich zufrieden war. Die früheren Ansätze hätten schon wieder derartige Ausmaße an Komplexität angenommen, das hätte ich vom Programm-Speicherplatz her nie vor mir selbst rechtfertigen können.

Bei allem, was ich so baue, was eigentlich "zum Einbauen in etwas anderes" gedacht ist, habe ich immer im Hinterkopf: Es muß auch noch Platz für etwas anderes bleiben. Ein Spiel darf nicht nur "ein kleines bißchen Code und Grafik um GameSys2 herum" werden - es soll auch nicht "ein bißchen Code und Spiel um eine riesige Grafikunit" sein - und natürlich sollte ein Spiel auch nicht "eine Musik- und Klangeffektmaschine mit nebenher ein bißchen Spiel dran" werden.

Die ganzen "Einheiten" müssen sich den Speicher und die Systemleistung gefälligst brüderlich teilen - und JEDER hat dabei Abstriche zu machen, wenn es nicht reichen sollte. Eine Mentalität wie "kauf Dir n Pentium 3 mit 64 MB, weil ich zu faul bin, sparsam zu arbeiten" käme für mich nie in Frage. Lieber gar nichts programmieren, als ein TicTacToe zu bauen, das eine Quadcore Maschine braucht.

OK, genug Gelaber.

Achja, @Dosenware:
Zatzen bezieht sich hier wohl auf die mittels GetMem maximal zu holende Menge Speicher, 65528 Bytes (64kB-8).
Im Realmode kann man glaube ich in Pascal keinen Datentyp mit genau 64kB deklarieren (obwohl man ihn benutzen kann, wenn man mit offenen Array-Grenzen arbeitet, was ich sowieso immer mache). Das liegt daran, daß das Datensegment (für globale Variablen) in Pascal (Borland Turbo Pascal) auf insgesamt 64kB begrenzt ist und der Stack auch nur max. knapp 64 kB werden kann - was auch die Größe von lokalen Variablen auf diese Gesamtgröße beschränkt.

Wenn ich aber ein Array mit var A:array [0..1] of byte; deklariere, kann ich (mit offenen Array-Grenzen, also Wegfall der überflüssigen und performancekillenden Prüfungen) auch auf A zugreifen, wenn B=65535 ist. (nur nicht mit Konstanten - das fängt der Compiler ab. - also kein A[65535].

Ich selbst arbeite nicht direkt mit GetMem. D.h. eigentlich schon: Ich hole am Anfang mit mehreren GetMem Befehlen einen zusammenhängenden Speicher. Diesen verwalte ich dann mit einer eigenen dynamischen Speicherverwaltung und kann dann Pointer oder Words (die nur Segment:0 Adressen haben) beliebig zuweisen, löschen, in der Größe ändern - auch für Größen weit über 64kB. Ich kann auch nachträglich noch die Größen von Speicherbereichen ändern oder löschen - der Rest wird dann mitverschoben, die Pointervariablen erhalten dann automatisch die neuen Adressen - alles quasi "im Hintergrund". So brauche ich mich nicht mehr um die Belegung von Speicher zu kümmern und es gibt auch keine "Löcher" im Speicher, wenn welcher freigegeben wird. Und denselben Kram, den ich so für den Heap gebaut habe, habe ich dann außerdem nochmal für XMS gebaut - ebenfalls eine dynamische Speicherverwaltung.

Trotzdem sind auch meine Samples auf knapp 64kB begrenzt. Einmal, weil ich denke, daß so große Samples dann kaum noch etwas mit Tracker-Musik zu tun haben - da kann man ja schon ein ganzes Stück in ein Sample tun - und dann natürlich auch, weil ich sowieso nicht SO VIEL Speicher alleine nur für Samples verschwenden will.
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 520
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Der Player wäre soweit fertig. Ich muss noch ein paar schöne Module finden, die sich gut konvertieren lassen.
Für jetzt mal schnell etwas neues trackern habe ich gerade nicht die Muße.
Ich bin auf 22 kHz Mixfrequenz hochgegangen, neben etwas mehr Höhen hat es den großen Vorteil,
dass deutlich weniger Aliasing auftritt, klingt also jetzt wesentlich sauberer.

Die Assembler Mischroutinen könnten noch verbessert werden. Im Moment werden noch ECX und EAX rotiert
sowie AX auf den Stack gelegt und wieder runtergeholt.

Wenn ich FS und GS benutzen könnte wäre mir sehr geholfen.
Ich habe nur nicht viel Ahnung wie das funktioniert.

Overrides für FS und GS sind 64h und 65h.

Kann ich diese dann nur für Funktionen wie lodsb oder movsw verwenden oder würde auch
"db64h; mov ax, ds:[si+bx]" funktionieren geschweige denn soetwas wie "db64h; inc ds" ?
mov ax, 13h
int 10h

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

Re: Trackermodul-Engine (sehr einfach)

Beitrag von DOSferatu »

zatzen hat geschrieben:Wenn ich FS und GS benutzen könnte wäre mir sehr geholfen.
Ich habe nur nicht viel Ahnung wie das funktioniert.

Overrides für FS und GS sind 64h und 65h.

Kann ich diese dann nur für Funktionen wie lodsb oder movsw verwenden oder würde auch
"db64h; mov ax, ds:[si+bx]" funktionieren geschweige denn soetwas wie "db64h; inc ds" ?
Naja, INC DS gibts nicht...
Aber ALLE Befehle, die sich normalerweise auf DS beziehen (und demzufolge im Pascal-Assembler glücklicherweise kein Segment-Präfix erhalten), kann man mit einem anderen Segment (ES, FS, GS) overriden.
Das heißt:
JA,
db 64h; mov AX,DS:[SI+BX]
wird dann zu
mov AX,FS:[SI+BX]

Hier ist es aber immer wichtig dafür zu sorgen, daß der eigentliche Befehl (hinter dem 64h oder 65h Präfix) kein eigenes Segmentpräfix erhält. MEISTENS ist das Standardsegment DS -
Aber Achtung: Für alle Befehle, die im Index irgendwo BP haben, ist SS (Stacksegment) das Standardsegment!
schreibt man also z.B.:
mov AX,DS:[BP]
mov AX,DS:[SI+BP]
mov AX,DS:[DI+BP]
mov AX,DS:[BP+Wert]
mov AX,DS:[SI+BP+Wert] oder
mov AX,DS:[DI+BP+Wert]

dann erhält der Befehl durch den Assembler bereits ein Präfixbyte - nämlich das Segment-Override-Byte für DS (3Eh), also würde dann ein weiteres davorgesetztes Segment-Override-Byte (wie z.B. 64h) nichts mehr bewirken.
Daher ist, wann immer darauf zu achten, daß, WENN man - um sicherzugehen - hier ebenfalls ein Segment davorschreibt, daß man, sobald BP im Index enthalten ist, SS statt DS nimmt.
Wollte das nur zusätzlich erwähnen. Wenn man nicht gleich beim schreiben daran denkt, findet man das später kaum noch wieder beim Debuggen.
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 520
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Jetzt würde ich gern noch die Mischroutinen mittels FS und GS optimieren.
Das Lesen mittels db 64h; mov ax, ds:[si+bx] funktioniert offenbar, allerdings
habe ich Probleme, den Segmentregistern FS und GS einen Wert zuzuordnen.
db 64h; lds si, ... funktioniert nicht. db 64h; mov ax, ds oder umgekehrt auch nicht...
Auch über den Stack geht's nicht.
mov ax, 13h
int 10h

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

Re: Trackermodul-Engine (sehr einfach)

Beitrag von DOSferatu »

zatzen hat geschrieben:Jetzt würde ich gern noch die Mischroutinen mittels FS und GS optimieren.
Das Lesen mittels db 64h; mov ax, ds:[si+bx] funktioniert offenbar, allerdings
habe ich Probleme, den Segmentregistern FS und GS einen Wert zuzuordnen.
db 64h; lds si, ... funktioniert nicht. db 64h; mov ax, ds oder umgekehrt auch nicht...
Auch über den Stack geht's nicht.
Ja, Zuweisung an Segmentregister (egal welche) gehen über spezielle Befehle. Nicht alles, was FS/GS betrifft, kann
mit db $64 bzw db $65 gemacht werden - diese Präfixbytes ändern nur das Segment bei Addressierung.
Für Laden von SEGMENT-REGISTER in normales Register/Speicher oder von normalem Register/Speicher in Segmentregister gibt es z.B. spezielle Befehle:
MOV Speicher/Register,SEGMENTREG. = db $8C,ModR/M
MOV SEGMENTREG.,Speicher/Register = db $8E,ModR/M

Das Mod-Reg-R/M Byte muß man in dem Fall natürlich selbst zusammenbasteln, so wie gehabt, aus den 3 Teilen:
Die mittleren 3 Bits (3,4,5) bestimmen in dem Fall das Segmentregister (ES,CS,SS,DS,FS,GS,?,?) (die Kombi 6 und 7 sind illegal). Die oberen 2 Bits bestimmen, wie die unteren 3 Bits gelesen werden:
%11 = Register (also AX,CX,DX,BX,SP,BP,SI,DI)
und wenn Mod = %00, %01 oder %10, dann geben sie verschiedene indizierte Adressierungsarten an, und bei %00 ohne Displacement, bei %01 mit 8-Bit-Displacement und bei %10 bit 16-Bit-Displacement - naja und so weiter. Kennst Du sicher selbst.
Dabei muß man natürlich beachten, daß im Falle eines Displacements dieses auch angegeben werden muß!
D.h. wenn Mod = %01 oder %10 ist, folgen natürlich noch ein oder zwei Byte, die den Additionswert, also dem Offset angeben.) Achja. Besonderheit ist, daß BP intern nicht einzeln als Index stehen kann, den Fall hat man für einzelnen 16bit-Offset (ohne Indexregister) vorgesehen. Also ist bei Mod = %00 und R/M = %110 nicht [BP], sondern nur der 16bit-Offset gemeint. Wenn man also im Assembler mov AX,[BP] benutzt, wird daraus intern mov AX,[BP+0] gemacht.
Somit ist es eigentlich keine gute Idee, allein BP als Index zu benutzen, weil ja immer mit Offset (wenn auch Null) gearbeitet wird. Andererseits gehen einem irgendwann die Register aus...

Aber OK. Wenn man gerade damit anfängt, FS und GS Werte zuweisen zu wollen, wäre am einfachsten in dem Fall der Weg über ein Register, also sowas wie
mov FS,AX - das entspräche dann db $8E,$E0
Dieses $E0 setzt sich dann bitweise so zusammen 11 100 000
11 = untere 3 Bit geben ein "normales" Register
100 = Segmentregister ist FS (also Nummer 4 in der Reihe, von 0 an gezählt)
000 = Das "normale" Register ist AX (also Nummer 0 in der Reihe).
(Zu beachten ist, daß $8C bzw $8E in dem Fall festlegt, ob es mov AX,FS oder mov FS,AX ist - in beiden Fällen ist das Mod-Reg-R/M Byte das gleiche!)
Man weist also vorher z.B. an AX das zu, was in FS stehen soll und danach führt man db $8E,$E0 aus.
Und, falls diese Frage kommt: Nein, es gibt leider keinen x86-Befehl für direkte Zuweisung von einem an ein anderes Segmentregister - hier muß immer der Umweg über ein anderes Register (oder über Speicher oder Stack) gegangen werden.

Für PUSH Segmentregister und POP Segmentregister gibt es IMMER spezielle Befehle, die direkt 1 Byte groß sind. Für FS und GS sind es 2 Byte (mit dem $0F Präfix) - hier war bei den 1-Byte-Befehlen kein Platz mehr, daher wurden diese in die zweite Befehlstabelle (eben mit dem $0F-Befehlspräfix) aufgenommen. FS und GS sind ja bekanntlich später "nachgerüstet" worden:
push ES = $06
push CS = $0E
push SS = $16
push DS = $1E
push FS = $0F,$A0
push GS = $0F,$A8
Man merkt schon, daß das Ganze einer gewissen "Gesetzmäßigkeit" folgt (außer bei FS und GS).
POP kann man sich leicht merken - einfach jeweils das untere Bit des Befehls setzen:
pop ES = $07
[pop CS = $0F] GIBT ES NICHT! $0F wurde daher später als Befehlspräfix für die zweite Befehlstabelle benutzt.
pop SS = $17
pop DS = $1F
pop FS = $0F,$A1
pop GS = $0F,$A9
(Daß pop CS keinen Sinn macht, dürfte einleuchten, das brauche ich wohl nicht zu erklären...)

Ich kann Dir gerne mal die ASM64FAQ.TXT zusenden,
dann noch ein cooles zeilenweise Textfile, das die Befehle nacheinander enthält, danach die Bytes für die Mod-Reg-R/M, dann für S-I-B (Scale-Index-Base), falls Du mal mit dem db $67-Präfix arbeiten willst (32-Bit-Adressierung...)
Das Ganze gibts aber auch nochmal als Textfile, wo es besser erklärt ist.
Dann hab ich auch noch eine (mit Rähmchengrafik gebaute) Tabelle, wo die Befehle tabellarisch in einer "Matrix" gelistet sind - ideal zum Ausdrucken.
Das ganze Zeug habe ich nicht selbst erstellt - nur über die Jahre so gesammelt.
Achja: Die ASM86FAQ.TXT kann man ja auch von meiner Seite herunterladen.
Und wenn man will, auch mein FAQREAD.EXE, was das Suchen in dem (ca. 1 MB großen) Textfile erheblich erleichtert. (Man kann dann aus Pulldowns die Kategorien und Subkategorien suchen. Es speichert auch Einstellungen ab. Und die Such-Eingabe ist recht einfach gelöst.
Einfach Buchstaben/Zahlentasten drücken und das Such-Eingabefeld geht auf. Wenn man beim ersten Zeichen zusätzlich Shift drückt (z.B. Shift+A), wird das Suchfeld gleich geöffnet, mit | A, wobei | dann ASCII 179 ist, also senkrechte Rähmchenlinie. Damit kann man dann einfacher gleich zu Assemblerbefehlen springen, weil die in so Kästchen stehen.

Selbst heute noch benutze ich gern und öfters diese Nachschlagewerke - ich habe schließlich nicht alle Befehle und Bytes ständig im Kopf.

Achja, diese "komplexeren" Befehle, sowas wie
LFS DI,Adresse
Könnt ich auch noch kurz erklären:
LFS Register,Adresse = $0F,$B4,Mod-Reg-R/M
LGS Register,Adresse = $0F,$B5,Mod-Reg-R/M
Das "Reg"-Feld (Bits 3,4,5) enthält das Register (z.B. wie oben DI wäre %111), Adresse würde sich wieder aus dem Mod und den R/M Feldern zusammensetzen. Ich weiß gerade nicht, ob Mod = %11 hier überhaupt Sinn macht, bzw. was dann herauskommt - vielleicht auch nur direkter Offset - ist ja auch egal.

Achso, WICHTIG! Immer, wenn man natürlich das Mod-Reg-R/M Byte benutzt und NICHT Register, also (indizierter) Speicheroffset, sollte man natürlich auch hier beachten, daß dieser OFFSET entsprechend dem angegebenen SEGMENT gilt!
D.h. normalerweise ist das DS. (Wieder beachten! Jede Indizierung, die irgendwo BP enthält, macht SS zum Standardsegment!) Will man also z.B. eine Adresse im Code-Bereich (dem Code-Segment) benutzen, muß hier natürlich das Segment-Präfix für CS: angegeben werden!
Segment Präfixe sind:
ES: = $26
CS: = $2E
SS: = $36
DS: = $3E
FS: = $64
GS: = $65
Auch hier sieht man wieder: Diese entsprechen (bis auf die "nachgerüsteten" FS und GS) entsprechenden "Regeln" - es ist wie die PUSH-Befehle + $20 (also mit gesetztem Bit 5).

Kurze Erklärung zwischendurch - falls kein Interesse, einfach überlesen bis zur nächsten Linie:
Daß der ganze x86 Assembler teilweise geordnet und teilweise wie Chaos aussieht, liegt daran, daß zu Anfang (4040, 8080, 8086) CPUs noch "festverdrahtet" waren und man die Befehle so angeordnet hat, wie es am einfachsten zu "verdrahten" war. So entstanden auch "Lücken", also Bytes, wo die Kombination keinen Sinn machte - (so erklären sich z.B. die "illegalen" Befehle beim C64-Chip (MOS 6502 bzw MOS 6510), die dann auch eine gewisse Funktionalität haben, die sich quasi "von allein ergeben" hat). Beim x86 gelten solche "Stellen" bis auf weiteres als undefinierte Befehle und erzeugen einen INT $06 (illegaler Opcode). "Bis auf weiteres" bedeutet hier: Bis es eine neuere CPU-Generation gibt.

Bei intel hatte man schnell erkannt, daß Abwärtskompatibilität der CPU eine gute Idee und verkaufsfördernd für Hardware war, da man nicht immer komplett neue Software kaufen mußte, wenn man seinen Computer aufrüstete. Also behielt man die existierenden Befehle bei und füllte in nachfolgenden CPU-Generationen nach und nach die "Lücken" mit brauchbaren neuen Befehlen. Inzwischen sind CPUs intern nicht mehr "festverdrahtet", sondern die Zuordnung der Befehle erfolgt über interne Tabellen - somit könnte jeder Befehl an jeder Stelle stehen. Aus Gründen der Abwärtskompatibilität hat man aber bei den alten Befehlen ihre Stelle weiter beibehalten.

Mit dem 32bit-Mode (Protected-Mode) und später mit dem 64bit-Mode gab es zwei "Brüche" zur Abwärtskompatibilität.
Der PM ist immerhin noch in der Lage, auch 16bit-Programme auszuführen, da quasi der RM (16bit)-Mode (der ursprüngliche) als Sub-Mode des PM konstruiert wurde. Er funktioniert ab 386er (eigentlich ab 286er) wie der PM mit abgeschalteten/auf Standard gesetzten Features.

Der 64bit-Mode macht das gleiche für den 32bit-Mode, grenzt dabei aber den 16bit-Mode aus. D.h. im 64bit-Mode können auch 32bit-Programme ausgeführt werden, aber keine 16bit-Programme mehr. Aus Gründen der Steigerung der Performance (oder meiner Meinung nach der Steigerung der Verkaufszahlen neuerer Betriebssysteme, die dann auch neuere Rechner brauchen) hat man im 64bit-Mode die Abwärtskompatibilität zu 16bit weggelassen. (Technisch wäre weiterer Support von 16bit meiner Meinung nach möglich gewesen - in CPUs, die selbst schon 3D Grafik berechnen, sollte die "Emulation" eines 386ers keine Schwierigkeit darstellen...)

Dieses "Auffüllen" (Nutzen vorher ungenutzter Opcode-Bytes) durch neue Befehle macht also das teilweise "Chaos" aus. Will sagen: In manchen bereichen, wie dem Mod-Reg-R/M Byte bei den "MOV Register,Register" Befehlen, konnte man einfach die freien Bits noch weiter nutzen. Statt 4 gab es nun 6 Segmentregister. Weil aber im Mod-Reg-R/M Byte an der Stelle ohnehin 3 Bits vorgesehen sind, konnte man nun einfach weiternumerieren, und FS und GS die Nummern 4 und 5 geben.
Bei den PUSH/POP/LxS Befehlen oder den Segmentpräfixen war aber alles "nachfolgende" schon belegt gewesen, bzw für gar nicht mehr als 4 verschiedene Kombinationen vorgesehen gewesen. Hier mußte dann "getrickst" werden.
------------------------------------------------------------------------------------------------------------------------------------

So ist es ja auch quasi leicht ersichtlich, daß hier ab 386er diese schöne Lücke von $64 bis $67 einfach vollständig "aufgefüllt" wurde, allesamt mit Präfix-Bytes:
$64 = FS:
$65 = GS:
$66 = Präfix für 32bit-Rechenbreite (Registerbreite)
$67 = Präfix für 32bit-Adreßbreite (4 GB Adressierung) - ermöglicht übrigens das S-I-B Byte zusätzlich zum Mod-Reg-R/M Byte. Außerdem kann dann hier jedes Register als Indexregister benutzt werden und bei Kombination kann dann auch automatisch eins der beteiligten Register *1, *2, *4 oder *8 gerechnet werden.
Und ja - das ist auch im 16bit (Real-Mode) möglich - ABER VORSICHT! Die Register werden hier alle 32bit gelesen und es wird NICHT wrapraoundet! Wenn man aber nicht den Flat-4G-Trick angewendet hat und der Offset ergibt zusammengenommen einen Wert größer als 65535 (also $FFFF), dann gibt's einen Absturz (selbst getestet und danach auch nachgelesen, daß es so ist). D.h. hier werden NICHT die oberen 16bit ausgeblendet, wie es normalerweise der Fall ist (wie erwähnt: der RM ist seit 286er ein Sub-Mode des PM). D.h. um das zu nutzen, müssen die oberen 16bit der an der Adressierung beteiligten Register immer auf $0000 gesetzt sein - oder genauer gesagt: Das Gesamtergebnis darf keinen Offset über 65535 ergeben.
An sich fände ich das ja praktisch, vor allem wegen der automatischen Skalierung. Aber dafür dann bei den Registern die oberen 16bit nicht zu nutzen, oder immer vorher irgendwohin retten zu müssen - das fand ich dann unpraktisch.
Aber vielleicht werde ich da "später" nochmal ganz andere Sachen machen. Für einen neuen C64-Emulator (mein alter ist schon etwas angestaubt) wäre das - vor allem für die Emulation der Grafikmodes - sehr praktisch.

Hier noch, falls Interesse, ein kleiner Exkurs meinerseits zum Thema: Wie hält man bei größeren Projekten überhaupt "Ordnung" in seinem Assembler-Code?
OK, ich sage mal, wie ICH es mache:
Ich mache vorher einen Plan.
(Ja, Planwirtschaft in der DDR war Mist, weil ein 5-Jahres-Plan davon ausgeht, daß sich Bedarf, Bedürfnisse, Gesellschaft, Entwicklung usw in den nächsten 5 Jahren nicht - oder nur planbar - ändern werden. So war das ganze Denken der Obrigkeit der DDR ja ausgelegt: Veränderungen existieren nicht. Außer, wenn von oben befohlen.)
Beim Programmieren sind Pläne aber gut.

Dieser Plan sieht vor allem vor, welche Register ich (hauptsächlich) für welchen Zweck einsetze, das betrifft sowohl die "Standardregister", als auch die Segmentregister. Warum? Naja - in Hochsprachen definiert man einfach neue Variablen, wenn man welche braucht - die Grenze ist nur der RAM. In Assembler kann man zwar auch alles in den Speicher legen, aber man merkt natürlich schnell, daß Register praktischer sind und die Abarbeitung schneller. Aber natürlich ist die Anzahl Register begrenzt und man kann nicht einfach neue "dazudefinieren".

Bei der Programmierung könnte man natürlich auch "immer ein Register nehmen, was gerade frei ist". Bei kleineren Subroutinen von Hochsprachen ist das auch weitestgehend egal.

Bei größeren Projekten kann man sich dabei aber manchmal etwas verzetteln - das führt dann dazu, daß man ständig aufwendig Register sichern muß (zeitfressende PUSH/POP-Orgien... - nein, nicht Popp-Orgien!), weil die gerade wieder anderweitig benötigt werden.
Gerade im RM (16bit), wo nicht jedes Register ein Indexregister sein kann und wo außerdem einige Register Spezialfähigkeiten haben, die nicht auf ein anderes Register übertragen werden können, macht es Sinn, wenn man sich vorher überlegt, welche Register man wofür einsetzt.
Ich liste die mal auf, inklusive Fähigkeiten. Das E (EAX) lasse ich mal weg. Die 32bit-Erweiterung haben ja alle.

AX
Teilbar in 8bit AH/AL. Multiplikationen, Divisionen. AL nutzbar für XLAT Befehl. Enthält den Wert für Port-Ein/Ausgaben (IN und OUT Befehl). AH kann mit den Flags geladen, Flags von AH geladen werden. AL, AX und EAX werden auch für den Wert benutzt, wenn es z.B. um die MOVSB, MOVSW, MOVSD Befehle geht, die dann, in Kombination mit SI und DI zum Kopieren/Testen/Füllen und bei Nutzung von REP auch für ganze Bereiche benutzt werden können.

CX
Teilbar in 8bit CH/CL. CL nutzbar als variabler Parameter für Bit-Shift/Roll-Befehle (SHL, ROR usw.). LOOP-Befehl. Abwärtscounter für REP (Wiederholungen). Spezialbefehl vorhanden für Testen auf CX=0 mit anschließendem Sprung (JCXZ)

DX
Teilbar in 8bit DH/DL. Erweitertes Register für Multiplikationen, Divisionen, sobald mehr als 8bit. Außerdem für mehr als 8bit-Port einziges Register, das die Portadresse enthalten kann (IN / OUT Befehle).

BX
Teilbar in 8bit BH/BL. Als Indexregister nutzbar. Für XLAT Befehl.
(Irgendwie mein "Lieblingsregister". Gleichzeitig 8bit-teilbar und als Indexregister nutzbar, das hat nur BX.)

SP
Stackpointer - das an wenigsten "frei nutzbare" Register. ES IST NICHT ZU EMPFEHLEN, SP ZU ÄNDERN/NUTZEN, AUßER MAN WEIß, WAS MAN TUT! Will man mit SP herummanipulieren und es braucht mehr als EINEN Befehl, damit es wieder einen gültigen Wert enthält, so müssen davor IMMER die Interrupts abgeschaltet werden!
Schade, daß SP NICHT als Indexregister nutzbar ist...

BP
(Längere Erklärung. Aber diese ganzen Erklärungen für BP sind wichtig.)
Als Indexregister nutzbar. (Wichtig: SS wird dann Standardsegment! Ich kann das nur immer wieder betonen!)
Wenn ohne Kombination, wird aber immer intern [BP+0] daraus, da der Fall [BP] ersetzt wurde durch [Offset], damit auch Zugriff auf einen Offset ohne Indizierung möglich ist.
Wird durch ENTER/LEAVE beeinflußt. Hochsprachen nutzen das gern, um einen "Stackframe" zu erschaffen beim Eintritt in eine Subroutine. BP wird dazu auf den Stack gerettet und erhält danach den momentanen Wert des Stackpointers.
So ist es leichter, auf die lokalen Variablen, die das Unterprogramm angelegt hat (und auf die im Header übergebenen Variablen/Werte) zuzugreifen.
Also, WICHTIG: Wenn man ein Pascal-Unterprogramm erzeugt, das außer Assembler auch Hochsprache enthält (also NICHT als

Code: Alles auswählen

procedure Beispiel; assembler;
asm
{...}
end;
definiert ist, erzeugt das den ENTER/LEAVE Rahmen.
Man kann (ohne "assembler;") dann auch auf lokale oder übergebene Variablen auch im asm-Teil zugreifen, intern werden daraus Adressierungen mit [BP+Offset] gemacht. (Das merkt man z.B., wenn man mit [Lokale_Variable+BX] indizieren will. Geht nicht, weil Kombi [BP+BX+Offset] als Index nicht vorgesehen!, hier muß dann SI oder DI herhalten.)
Wichtig ist: WENN man BP dann selbst nutzen oder ändern will, sollte man entweder schon alle lokalen Variablen "in Sicherheit gebracht" haben oder BP vorher sichern - da sonst natürlich spätere Zugriffe auf lokale Variablen (zu denen auch die im Procedure-Header übergegebenen gehören) nicht mehr funktionieren.
In reinem Assembler kann man BP natürlich wie jedes andere Register nutzen.

SI
Als Indexregister nutzbar. Ist "Source-Index" (daher SI) für die speziellen MOV/Test-Kopier-Befehle, die auch mit REP kombinierbar sind (mit CX als Counter).

DI
Als Indexregister nutzbar. Ist "Destination-Index" (daher DI) für die speziellen MOV/Test-Kopier-Befehle, die auch mit REP kombinierbar sind (mit CX als Counter). Wichtig: Bei den speziellen Befehlen, wo DI als Ziel gilt, ist das Zielsegment IMMER ES, das kann nicht "präfixt" werden. Das Quellsegment (für SI) ist standardmäßig DS und kann "präfixt" werden (in jedes andere Segment).

Wieso dieser ganze Kram, den ich hier schreibe? Naja , diese ganzen Sachen (habe sicher noch ein paar vergessen) sind so Spezialzwecke, die die verschiedenen Register so haben und die ich immer so im Kopf behalte, um die Register optimal ihrem Zweck entsprechend auszubeuten...

Das Ganze schließt natürlich nicht aus, Register zu "zweckentfremden" - das kann und wird oft passieren, gerade wenn man seinen Code optimiert. Und natürlich können, um Werte zu halten, laden/speichern, zu rotieren, addieren, subtrahieren auf dem x86 erstmal ALLE diese Register gleichermaßen genutzt werden (bis auf SP vielleicht). Und selbst wenn man sich einen Registernutzungs-Plan gemacht hat, wird man von diesem auch intern oft abweichen.

So ein Plan ist (auch für mich) nicht dazu gedacht, mir selbst vorzuschreiben, wann ich welches Register nutzen soll, sondern eher dazu, daß ich, wenn ich gerade mehrere Register zur Auswahl hätte, mir die Entscheidung zu erleichtern, indem ich das "geplante" den anderen vorziehe. Meiner Erfahrung nach kann man so das dauernde Sichern/Laden/Zwischenspeichern oder Tauschen von Registerinhalten erheblich minimieren.

Anderes Beispiel: im RM benutzt man sowieso nur 16bit-Indizes. Das muß aber nicht heißen, daß man deshalb die oberen 16bit der Indexregister brachliegen lassen muß. Sie eignen sich ebenfalls zum Speichern von Werten. Speicheroperationen sind auch immer langsamer als direkte Registeroperationen.

Ich gebe mal ein Beispiel:
Ich will DI für eine Schleife nutzen, von 0 angefangen. Die anderen Register werden gleich noch gebraucht, ich kann also keins der Register ändern außer DI. Aber der Wert, den DI enthält, müßte ich vorher auch noch sichern, der wird danach wieder gebraucht.
Die offensichtliche, aber dümmere Methode:

Code: Alles auswählen

push DI    {DI auf den Stack sichern}
xor DI,DI  {DI Nullsetzen}
.... {die Schleife abarbeiten}
pop DI     {DI vom Stack zurückholen}
Die weniger offensichtliche, aber schlauere Methode:

Code: Alles auswählen

shl EDI,16  {alten Wert von DI sichern, DI gleichzeitig nullsetzen}
.... {die Schleife abarbeiten}
shr EDI,16  {alten Wert von DI zurückholen} 
Ein Befehl weniger - und zusätzlich keine Stackbefehle nötig, alles schnelle Registerbefehle. Das Nullsetzen und Sichern passiert gleichzeitig. Man kann so auch zwei verschiedene 16-Bit-Indexwerte tauschen, indem man immer wieder mit ROL DI,16 (oder ROR DI,16) arbeitet.
(Dieses Tauschen der Indexwerte benutze ich z.B. in meinem GameSys2. Es gibt 128 Befehle, das obere Bit löst den Indextausch aus. Der Sinn ist, daß man so im gleichen Unterprogramm mit den gleichen Befehlen die "Variablen" zwei verschiedener "Figuren" ansprechen kann. Der Index gibt den Offset der Variablen einer Figur im Figurenstack an.)

Die Idee, mit Shiftbefehlen gleichzeitig Werte zu sichern und Teilregister (wie AL oder AX) eines Registers nullzusetzen, ist zwar einfach - aber ich bin da auch nicht gleich am Anfang meiner ASM-Laufbahn drauf gekommen.
Das Ganze macht schon ab 286ern Sinn - ab da hat die Anzahl der Schiebungen keinen Einfluß mehr auf die benötigte Ausführungszeit. D.h. shl DI,5 dauert genausolange wie shl DI,13

OK, das soll erstmal genug dazu sein. Nur noch soviel: Sowohl in GameSys2, als auch in ISM habe ich so einen "Registernutzungsplan", d.h., außer an Stellen, wo es nicht anders geht (d.h. wo entweder keine Register mehr frei sind oder wo ein bestimmtes Register zwingend erforderlich ist, weil es als einziges das benötigte Feature hat) werden die Register entsprechend ihrem Plan eingesetzt.

Die ganze Idee wie ich in den letzten Jahren so programmiere, basiert im Grunde darauf, daß ich größere Projekte, vor allem solche Dinge wie GameSys2 und ISM, so konstruiere, als würde ich eine Maschine zusammenschrauben. Möglichst wenig Teile verbauen - Teile mehrfach und für typgleichen Zweck verwenden, um die Maschine klein und schnell zu bekommen.

Hochsprachen haben hier manchmal gewisse Nachteile. Sie erlauben dem Programmierer, wild Variablen zu deklarieren, bis der Speicher voll ist. Weil man ja "ordentlich" sein will, werden Variablen nur ihrem Namen entsprechend benutzt, für jeden Kram wird eine neue definiert, selbst wenn sie nur einmal kurz benutzt wird. Der "Ich-habe-Programmieren-in-einem-Kurs/Lehrgang-gelernt"-Mensch wird keine "Hilfsvariablen" nutzen, die auch mal mehrfach verwendet werden können. Das macht den Source zwar schick, aber erhöht den Speicherverbrauch.

Das Schöne an Assembler ist, daß man hier gut lernen kann, sich auf Wesentliches zu beschränken, EINFACH zu denken, keinen Platz und keine Register zu verschwenden.

Meiner (persönlichen) Meinung nach wird man erst dann ein guter und effizienter Hochsprachenprogrammierer, wenn man sich auch mal etwas eingehender mit Assembler beschäftigt hat. Erst dann wird einem klar, was man mit vielen Dingen, die man früher in Hochsprachen "mal einfach so" gemacht hat, wahrscheinlich im Speicher und für die CPU so angerichtet hat. - Meistens ein Disaster oder einen Worst Case.
Erst dann wird einem klar, wieso erfahrene Programmierer dem Hochsprachen-Einsteiger immer Sachen sagen wie "Keep it simple". Oder wieso Entscheidungstabellen (oder überhaupt Tabellen) oft besser sind als Code.
Riesige komplizierte Formeln, IF-Entscheidungs-Kaskaden und ähnliches im Quellcode sehen für einen Laien so aus wie: "Wahnsinn, das ist ein totaler Experte."
Tja, und ein Experte, der sich denselben Quelltext anschaut, würde denken: "Wahnsinn, das ist ein totaler Laie."

Ich weiß natülich, daß eine solche Meinung unpopulär erscheint, in Zeiten, in denen der unbedarfteste Copy-&-Paste-Skripter einen Assembler-Coder auslachen darf (nur weil er schneller fertig ist, egal wie ineffizient das Ergebnis ist), in denen so mancher angebliche Programmierer noch nie einen einzigen Assembler-Opcode gesehen oder im Binärcode gerechnet hat (also immer *32 statt SHL 5 rechnen würde), in denen Hochsprachen ganze 1 MB-Blöcke an Modulen zu einer *.EXE zusammenfügen (selbst wenn nur 1 Subroutine daraus gebraucht wird), in denen die Qualität eines "Programmierers" proportional zu den Abstraktionsleveln ermessen wird, die er über der Maschinenebene steht (obwohl, wie jeder wissen sollte, jedes Abstraktionslevel das Programm größer und langsamer macht und mögliche Fehlerquellen hinzufügt) - und eben in Zeiten, in denen zwar "Retro" so hochgeschätzt wird und wieder beliebt ist, aber es kein Problem ist, daß ein 2D-Tetris eine VM benötigt, die auf einer 2 GHz-Maschine laufen müß...

Aber wem sage ich das? Wir sind ja hier im DOSforum. Der Eine oder Andere wird wahrscheinlich wissen oder nachvollziehen können, was ich meine.

@zatzen:
Ich Bitte um Entschuldigung Wenn das zuviel Kram auf einmal war. Sag Bescheid, wenn Dich das eine oder andere davon besonders interessiert.
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 520
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Hier schliesslich mein erster Release des Players.
http://www.zatzen.net/ZSMP001B.zip

Ich bedanke mich bei allen die mir geholfen haben und den Thread soweit verfolgt haben.

Ich habe das Format und den Player auf 16 Kanäle erweitert. So kann ich besser auch eigene Musikstücke
konvertieren. Trotzdem bleibt die CPU Auslastung bei Musikdateien mit weniger Kanälen niedrig.

Bei der Auswahl der Musikdateien habe ich mich bei den meisten daran orientiert, dass sie kleiner als
128 KB sind. Das fände ich noch eine gesunde Größe für ein Spiel. Es sind aber auch ein paar dabei die
kleiner als 64 KB sind und auch eins mit gerade mal 2 KB.
Axel F ist in der uralten MOD-Version dabei die überall kursiert... Turrican habe ich auch dazu getan.
Es ist zwar durch das fehlen einiger Effekte stellenweise etwas entstellt, aber wir hatten es ja in der
Diskussion, und so ist vielleicht die Größe der Patterndaten interessant.

Der Player zeigt u.a. ein Lautstärke-Meter an. Darunter wird die "Attenuation" angezeigt, die man mit
der Taste V ändern kann zwischen 0, -6 und -12 dB. -6 dB ist für die meisten Module der beste Trade-Off
zwischen Übersteuerung und Rauschen.
Man kann hier gut ausprobieren, wie sich Clipping anhört. Gerade wenn es sich hier um eher mäßige
Klangqualität handelt finde ich, dass gelegentliches Clipping noch absolut vertretbar ist. Ein Überlauf
wäre fatal, dass kracht und knackst, aber kontrolliertes Clipping hört sich eher wie ein übersteuerter
Transistorverstärker an.
Was man vielleicht nicht direkt denkt ist, dass sich Samples nicht zwangsläufig so aufaddieren dass
man bei 16 Kanälen die Gesamtlautstärke durch 16 teilen muss. Tatsächlich macht meine "Volume
Attenuation" bei -6 dB gerade mal eine Halbierung, bei -12 dB eine Viertelung - und bei 0 dB füllt
bereits ein einziges Sample in voller Lautstärke den gesamten Headroom, sprich es würde lautstärke-
mäßig 1:1 in den Puffer gelangen. Einge Module haben leise Samples oder verwenden Lautstärke-
Kommandos, wodurch sie insgesamt leiser sind. Dort passt manchmal sogar die 0 dB Attenuation.
Ich mische zuerst in einen 16 Bit Puffer. Eine volle Samplelautstärke setze ich dabei mit einem
links-Shift von 7 um, halbe Sample-Lautstärke mit Shift von 6 usw. So vermeide ich den Verlust von
Bit-Tiefe in diesem Stadium, und somit Rauschen. Ich muss lediglich wieder um 2 rechts-shiften,
sonst gibt es bei besonders lauten Modulen bereits im 16-Bit Puffer einen Überlauf.
So verliere ich erst bei Samples an Bit-Tiefe, die mit einer 64tel Lautstärke oder weniger
abgespielt werden. Natürlich kommt am Ende alles in einen 8 Bit Puffer. Aber es würde deutlich
mehr rauschen wenn bereits die Samples mit verminderter Bit-Tiefe reingemischt würden.

Ich wünsch euch jedenfalls viel Spass beim Anzeigen-gucken und rumspielen,
nicht zu vergessen beim Hören.

Und ich bin gespannt ob und wie das Ding auf euren Maschinen läuft.


Jetzt habe ich ersteinmal das "schlimmste" geschafft. Ich denke ich werde demnächst den
Code noch optimieren und mehr Musikstücke konvertieren oder speziell neue machen.
Ich habe auch schon einen Test gemacht gemeinsam mit ZVID - funktioniert.
Zu jedem Sample speichere ich ein Info-Byte, welches man verwenden könnte um
Grafik mit der Musik zu synchronisieren. So könnte man z.B. entsprechende
Schlagzeug Samples so markieren, dass man eine Animation eines Schlagzeugers
synchron zur Musik macht. Ich denke soetwas in der Art werde ich demnächst mal
machen.
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
Dosenware
DOS-Gott
Beiträge: 3218
Registriert: Mi 24. Mai 2006, 20:29

Re: Trackermodul-Engine (sehr einfach)

Beitrag von Dosenware »

Läuft supi, und vor allem schnell :like:

Die Bytes/Sec Anzeige finde ich cool 8-)

PS. 386DX33
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 520
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Dosenware hat geschrieben: PS. 386DX33
Das freut mich! Dann sind ja die 32 Bit Befehle im Code gerade gerechtfertigt, wenn es auch
schon auf einem 386er läuft. Funktionieren denn auch Module wie sandman3.zsm (Sequencer
Position 24-29) flüssig, also welche mit zeitweise allen 16 Kanälen?

Die Puffergröße ist 512 Byte, also rechnet der Player ca. 43 mal in der Sekunde die Anzeigen aus
und stellt sie dar. Dafür geht natürlich auch etwas Rechenzeit drauf.
Auch muss ich die Pattern-Nummer jeweils beim Sequencer-Wechsel ermitteln, indem ich die
Offsets vergleiche, denn ich habe nur im Sequencer direkt offset Words in die Patterndaten
hinterlegt, den Patterns aber keine Nummer zugewiesen, d.h. nicht noch zusätzlich eine Offset-
Liste angelegt für jedes Pattern - oder vielmehr, nicht im Sequencer Words anlegen mit der Pattern-
nummer, und dann nochmal den Umweg über eine Tabelle gehen. Ausser in solch einem Player
interessieren ja auch niemanden die Pattern-Nummern, in einem Spiel würde ich einfach zu diesem
oder jenem Sequencer-Punkt springen.
Die Player-Unit ist derzeit noch eher provisorisch auf ZSMPLAY abgestimmt, damit es seine Anzeigen
machen kann. Viele dieser Variablen brauche ich hinterher in einem Spiel auch nicht mehr.
Für das Lautstärke-Meter scanne ich nochmal den 16Bit-Puffer durch, allerdings mit einer
Schrittweite von 4, also effektiv 5512,5 Hz - wird nicht so viel CPU Power fressen, aber
auch das brauche ich in einer anderen Anwendung nicht mehr.
mov ax, 13h
int 10h

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

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Ich habe meine Mischroutinen nochmal "verschnellert". Hier die Routine für Samples ohne Schleife:

Code: Alles auswählen

procedure mix_in_channel(length: word);assembler;
asm

  db 66h; mov bx, word ptr s_pos
  db 66h; mov dx, word ptr s_speed

  mov cx, ds; lds si, smp_pointer; mov ax, ds; mov ds, cx
  db 8eh; db 0e0h { mov fs, ax }
  les di, soundbuffer16bit_pointer
  mov ax, mix_position
  add ax, ax { shl ax, 1 }
  add di, ax
  (* es:[di] zeigt nun auf die richtige Mixposition *)

  mov ax, actual_smplen; db 8eh; db 0e8h; { mov gs, ax }
  mov cx, length

  mov al, actual_chan_vol
  cmp al, 7; je @loopvol7
  cmp al, 6; je @loopvol6
  cmp al, 5; je @loopvol5
  cmp al, 4; je @loopvol4
  cmp al, 3; je @loopvol3
  cmp al, 2; je @loopvol2
  cmp al, 1; je @loopvol1

  @loopvol0:
  db 8ch; db 0e8h; { mov ax, gs }; cmp bx, ax; jnb @end
  db 64h; mov al, ds:[si+bx]; cbw
  sar ax, 2; add es:[di], ax
  add di, 2; db 66h; add bx, dx; adc bx, 0
  dec cx; jnz @loopvol0; jmp @skip

  @loopvol1:
  db 8ch; db 0e8h; { mov ax, gs }; cmp bx, ax; jnb @end
  db 64h; mov al, ds:[si+bx]; cbw
  sar ax, 1; add es:[di], ax
  add di, 2; db 66h; add bx, dx; adc bx, 0
  dec cx; jnz @loopvol1; jmp @skip

  @loopvol2:
  db 8ch; db 0e8h; { mov ax, gs }; cmp bx, ax; jnb @end
  db 64h; mov al, ds:[si+bx]; cbw
  add es:[di], ax
  add di, 2; db 66h; add bx, dx; adc bx, 0
  dec cx; jnz @loopvol2; jmp @skip

  @loopvol3:
  db 8ch; db 0e8h; { mov ax, gs }; cmp bx, ax; jnb @end
  db 64h; mov al, ds:[si+bx]; cbw
  sal ax, 1; add es:[di], ax
  add di, 2; db 66h; add bx, dx; adc bx, 0
  dec cx; jnz @loopvol3; jmp @skip

  @loopvol4:
  db 8ch; db 0e8h; { mov ax, gs }; cmp bx, ax; jnb @end
  db 64h; mov al, ds:[si+bx]; cbw
  sal ax, 2; add es:[di], ax
  add di, 2; db 66h; add bx, dx; adc bx, 0
  dec cx; jnz @loopvol4; jmp @skip

  @loopvol5:
  db 8ch; db 0e8h; { mov ax, gs }; cmp bx, ax; jnb @end
  db 64h; mov al, ds:[si+bx]; cbw
  sal ax, 3; add es:[di], ax
  add di, 2; db 66h; add bx, dx; adc bx, 0
  dec cx; jnz @loopvol5; jmp @skip

  @loopvol6:
  db 8ch; db 0e8h; { mov ax, gs }; cmp bx, ax; jnb @end
  db 64h; mov al, ds:[si+bx]; cbw
  sal ax, 4; add es:[di], ax
  add di, 2; db 66h; add bx, dx; adc bx, 0
  dec cx; jnz @loopvol6; jmp @skip

  @loopvol7:
  db 8ch; db 0e8h; { mov ax, gs }; cmp bx, ax; jnb @end
  db 64h; mov al, ds:[si+bx]; cbw
  sal ax, 5; add es:[di], ax
  add di, 2; db 66h; add bx, dx; adc bx, 0
  dec cx; jnz @loopvol7; jmp @skip

  @end:
  mov play_sample_actual, 0
  @skip:
  db 66h; mov word ptr s_pos, bx

end;
Eine Sprungtabelle ist für die 7 CMPS wahrscheinlich nicht unbedingt nötig.
Ansonsten würde ich gerne lernen, wie man das veranstaltet.

Der Code ist nun natürlich ziemlich redundant, aber da ich mit festen Shift-Werten
arbeiten kann spare ich mir zum einen die Rotation von ECX und brauche auch nur
einmal zu Shiften (statt SAL AX, CL und SAR AX, 2), einmal sogar gar nicht.
Eine Verbesserungsmöglichkeit sehe ich noch:
Weil mir das Wissen fehlt gehe ich beim Vergleich von GS mit BX über AX.
Vielleicht geht auch direkt CMP BX, GS.
Daher, @DOSferatu, wäre es toll wenn ich das Textfile mit den Befehlen und danach
den ensprechenden Bytes hätte.
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
Dosenware
DOS-Gott
Beiträge: 3218
Registriert: Mi 24. Mai 2006, 20:29

Re: Trackermodul-Engine (sehr einfach)

Beitrag von Dosenware »

Also bei Benutzung aller 16 Kanäle merkt man was (geht glaube schon bei 13/14 Kanäle los - ist bei den Sprachsamples aber schwer zu sagen)

Time stottert bei Sequenz 29 - 33.
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 520
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Danke, dann werde ich mal weiter optimieren.
Die Mischroutinen werden schon den Löwenanteil ausmachen, aber auch im Pascal-Teil
lässt sich noch einiges verbessern.
mov ax, 13h
int 10h

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

Re: Trackermodul-Engine (sehr einfach)

Beitrag von DOSferatu »

@zatzen: Habe es mir mal runtergeladen und alles angehört. Ja, gefällt mir ganz gut. Macht auch irgendwie "mehr Sound" als mein Kram. Es fetzt.
(Liegt wohl u.a. auch daran, daß alles, womit ich meinen Kram bisher getestet habe, mein eigenes uninspiriertes Enten-Ding da ist.)
Jedenfalls: Auch schon beeindruckend, wie Du die Patterndaten eindampfen kannst auf eine geradezu lächerlich geringe Größe. Ja, es macht so richtig Bock, einen engagierten Programmierer bei der Arbeit zu sehen. Respekt, Mann!

So einen MOD2ISM Konverter (für meinen Kram) habe ich ja auch mal irgendwann angedacht. Aber erstmal soll mein eigener Kram 100% fertig sein. Die Bedienung meines "Trackers" gefällt mir noch nicht so.

Achso ja. DIe Files (für Assembler und so) werd ich demnächst mal auf meinen Webspace hochladen.
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 520
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Kleines Update:
Ich wollte ja beim Sample-Ende Vergleich den Weg über AX vermeiden - ich nutze
jetzt BP bzw. EBP. Für euch vielleicht trivial, aber für mich was neues.

Hier eine der Schleifen für Samples mit Schleifen. Es hat sich gezeigt dass sehr
kurze Schleifen gerader klingen wenn ich direkt nach Zurücksetzen von BX nochmal
eine Schrittweite draufaddiere. Vielleicht muss ich da nochmal dran werkeln.

Code: Alles auswählen

  @loopvol6:
  cmp bx, bp; jb @skipvol6
  db 66h; ror bp, 16; mov bx, bp; db 66h; ror bp, 16
  db 66h; add bx, dx; adc bx, 0
  @skipvol6:; db 64h; mov al, ds:[si+bx]; cbw
  sal ax, 4; add es:[di], ax; add di, 2; db 66h; add bx, dx; adc bx, 0
  dec cx; jnz @loopvol6; jmp @end
Und hier noch, nur damit ihr mir sagen könnt ob ichs verstanden habe,
die Wertezuweisung in die Register - gehe ich richtig in der Annahme
dass nach Verändern von BP keine Variablen mehr direkt adressiert werden
können?

Code: Alles auswählen

procedure mix_in_channel_sloop(length: word);assembler;
asm

  db 66h; mov bx, word ptr s_pos
  db 66h; mov dx, word ptr s_speed
  mov cx, ds; lds si, smp_pointer; mov ax, ds; mov ds, cx
  db 8eh; db 0e0h { mov fs, ax }
  les di, soundbuffer16bit_pointer
  mov ax, mix_position
  add ax, ax { SHL AX, 1 }
  add di, ax
  (* es:[di] zeigt nun auf die richtige Mixposition *)

  mov cx, length  { <--- wird length hier nicht schon direkt in AX übergeben? }
  db 66h; shl cx, 16
  mov cx, actual_smplen
  mov al, actual_chan_vol
  db 66h; shl ax, 16
  mov ax, actual_sample_loopstart

  push bp
  mov bp, ax { loopstart, oberes word }
  db 66h; shl bp, 16
  mov bp, cx { smplen, unteres word }

  { CX: actual_smplen }
  { oberer Teil von AX: smp_loopstart }
  { unterer Teil von AX: actual_chan_vol }

  { unteres word von EBP: smplen }
  { oberes word von EBP: smp_loopstart }

  db 66h; shr cx, 16
  db 66h; shr ax, 16

  cmp al, 7; je @loopvol7
  cmp al, 6; je @loopvol6
  cmp al, 5; je @loopvol5
  ...
mov ax, 13h
int 10h

while vorne_frei do vor;
Antworten