Trackermodul-Engine (sehr einfach)

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

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

@DOSferatu: Jetzt wo es was mit Musik bzw. Sound zu tun hat bin ich auch richtig bei der Sache und
auch schon ein Stück mehr motiviert, ein Spiel oder wenigstens so eine Art interaktive Demo zu machen.

Übliche Tracker haben halt einen dicken Nachteil, die Redundanz der Patterndaten, wenn es sich
nicht unbedingt um komplexe klassische Musik handelt, die man da getrackert hat, was aber meistens
sowieso nicht so gut rüberkommt. Es gibt ein paar MODs, da wird jeder Befehl und alle Tricks ausgereizt,
um mit einem einzigen Pattern auf eine Länge von einer halben Minute zu kommen. Aber dann kann
man wirklich fast ein komplexeres Format wählen mit Programmcode-ähnlichen Befehlen.

Ein Speicherfresser bei meiner Patternkompression ist noch das "Gerüst": Die Flags, die ansagen,
in welcher Zeile und auf welchem Track ein Befehl steht. Während der Programmierung habe ich
festgestellt dass simples Zeilenweises flagging mit 1 Bit jeweils effektiver ist als wenn man da
optional oder permanent Lauflängen mit reinbringt. Bei einem (1) Track mit 64 Zeilen sind das
8 Byte, zumindest wenn es keine ganzen Leerzeilen gibt, die als solche geflagt würden.
Jedenfalls kann sich das ganze so zusammenläppern, dass ich alleine für das Gerüst 32 Byte oder
bei 16 Kanälen sogar 128 Byte habe, im ungünstigsten Fall.

Aber mir kommt gerade eine neue Idee, und zwar dass ich diese Gerüste, oder nennen wir sie mal
Masken, tabellarisch ablege und dann einfach per Byte (oder so viel Bit wie es braucht für die Anzahl
aller vorkommenden verschiedenen Masken) jedem Track zuordne. Dabei könnte man diese dann
auch noch je nach Typ entsprechend komprimieren - je nach Pattern kommt es auch mal vor, dass
jede Zeile auf einem Track belegt ist. Solch ein Muster braucht ja nichts ausser der Information
dass alles voll ist.

Es könnte sich so noch eine wesentlich höhere Kompressionsrate ergeben, denn ich habe das
Format so angelegt, dass jeder Track eine Tabelle hat (es sei denn sie braucht mehr Speicher
als wenn die direkten Werte im Track gespeichert stehen plus der Indizes im Track). Es gibt drei
Arten von Tabellen: Samplenummer und Volume, und es gibt eine dritte, die nicht für jeden Track
erzeugt wird, sondern generell für jedes Sample: Die Pitch-Tabelle. Worauf ich hinaus will:
Für den Ausnahmefall, dass wir es z.B. mit einer Hihat zu tun haben, die auf einem Track
gespielt wird auf einer festen Lautstärke, dann hat dieser Track nur eine Samplenummer, nur
eine Lautstärke und nur eine Note. Folglich werden gar keine Bits gelesen, weil für diesen Track
schon alles fest definiert ist - nur das Gerüst gibt an, WANN die Hihat gespielt werden soll.
Naja und wenn ich diese Gerüste nochmal zusammenstreiche, dann wird es öfter Tracks oder
im Extremfall ganze Patterns geben, die mehr oder weniger 0 Byte belegen, von den (kleinen)
Tabellen und dem Hinweis für jeden Track, welches Gerüst er haben muss, abgesehen.

EDIT: Nochmal drüber nachgedacht... So wie beschrieben werde ich es nicht umsetzen. Der Overhead
würde den Gewinn nahezu kompensieren und die Leseroutinen würden unverhältnismäßig komplex.
Nur irgendwie meine ich, müsste da noch was gehen. Wobei der Zweck schon erfüllt ist, ich bekäme
rund 500 Patterns in 64K, und das erlaubt rund 1 Stunde nichtredundante Musik. Ob man 4 oder
16 Kanäle hat macht dabei gar nicht so viel aus, da man automatisch bei mehr Kanälen alles
nicht so dicht bepackt.


Ich halte es übrigens nicht für unmöglich, redundante MOD Patterns wirklich effizient und intelligent
zu komprimieren. Aber das würde schon etwas mehr Programmieraufwand erfordern, und
da kommen mir auch solche Dinge wie Brute Force in den Sinn...
Aber mitunter winkt eine größere Effizienz als wenn man einfach ZIP drüberjagt.
Ähnlich wie bei meinem ZVID, welches zwar an sich Grafiken nicht ganz so klein bekommt
wie GIF, dann aber wiederum, ergeben sich deutlich kleinere Daten, wenn man ein ZVID mit ZIP
komprimiert, als wenn man Grafik komprimiert, die nicht durch ZVID gelaufen ist.
mov ax, 13h
int 10h

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

Re: Trackermodul-Engine (sehr einfach)

Beitrag von DOSferatu »

zatzen hat geschrieben: 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?
Das betrifft nur LOKALE Variablen, also solche, die entweder im Header der Procedure/Function übergeben werden und solche, die in der Procedure/Function im Dklarationsbereich mit VAR oder CONST deklariert werden.
Denn diese werden (temporär) auf dem Stack angelegt. Pascal (und auch andere Hochsprachen) nutzen dann ENTER (und am Ende LEAVE), um BP die aktuelle Position von SP zuzuweisen. Somit wird dann ein Zugriff auf eine lokale Variable vom Pascal-Assembler (und auch im Pascal-Teil) immer mit SS:[BP+x] erfolgen, wobei x der definierte Offset im Stack ist, auf dem die Variable landen wird (SS: wird natürlich weggelassen, da, wie bekannt, für Indizierungen mit BP ja sowieso immer SS das Standardsegment ist).
D.h. wenn man natürlich BP ändert, "weiß" Pascal ja nichts davon und nachfolgende Ausdrücke wie
MOV AX,Variable
werden dann immer noch mit
mov AX,SS:[BP+Offset_von_Variable]
übersetzt. Der Offset ist ja der gleiche, der wird ja bei der Deklaration der Varaiblen im Header berechnet. Aber BP stimmt ja nicht mehr - also wird auf eine ganz andere Stelle im Stack zugegriffen als gewollt.
ABER man kann natürlich auch jederzeit BP wiederherstellen, dann funktioniert es wieder.

Hat man einen "Pascal-Rahmen" um eine Procedure/Function (also ohne assembler; dahinter und mit begin end; statt asm end;) und ändert BP innerhalb der Procedure/Function, so MUß man BP am Ende wieder zurückstellen! Am Ende ist nämlich ein LEAVE (das Gegenstück zum ENTER am Anfang) und da wird dann der SP auf den Inhalt von BP gesetzt und danach BP vom Stack geholt. Enthält nun BP einfach "irgendwas", so wird SP auf "irgendwas" gesetzt, was natürlich beim Rücksprung der Procedure/Function ins Hauptprogramm "erheblichen Schaden" anrichten wird, denn als nächstes liegt auf dem Stack die Rücksprungadresse (eigentlich die Variablen, dann die Rücksprungadresse, die Variablen werden, wenn vorhanden, übersprungen, weil der entsprechende RET mit einem Parameter abgelegt wird, der die zu überspringende Anzahl Bytes angibt - egal. Na jedenfalls: Wenn der Stack am Ende nicht wieder da steht, wo er am Anfang stand, geht der Rücksprung sonstwohin.
zatzen hat geschrieben:@DOSferatu: Jetzt wo es was mit Musik bzw. Sound zu tun hat bin ich auch richtig bei der Sache und
auch schon ein Stück mehr motiviert, ein Spiel oder wenigstens so eine Art interaktive Demo zu machen.
Das klingt cool.
zatzen hat geschrieben:Übliche Tracker haben halt einen dicken Nachteil, die Redundanz der Patterndaten, wenn es sich
nicht unbedingt um komplexe klassische Musik handelt, die man da getrackert hat, was aber meistens
sowieso nicht so gut rüberkommt. Es gibt ein paar MODs, da wird jeder Befehl und alle Tricks ausgereizt,
um mit einem einzigen Pattern auf eine Länge von einer halben Minute zu kommen. Aber dann kann
man wirklich fast ein komplexeres Format wählen mit Programmcode-ähnlichen Befehlen.
Tja, wie ich schon immer sagte: Kann nicht verstehen, wie jemand auf ein Format (MOD) kommen konnte, das im Regelfall zu 95% aus Nullen besteht.
zatzen hat geschrieben:Ein Speicherfresser bei meiner Patternkompression ist noch das "Gerüst": ...
Ach naja, Dein Format packt eigentlich schon recht ordentlich. Zu MOD überhaupt kein Vergleich.
zatzen hat geschrieben:Aber mir kommt gerade eine neue Idee, [...] Masken [...]
Beim Packen von Daten kann man natürlich beliebig tief abgehen.
Gerade bei Daten, die so redundant sind wie MOD-artige Files bieten sich hier viele Möglichkeiten.
zatzen hat geschrieben:Ich halte es übrigens nicht für unmöglich, redundante MOD Patterns wirklich effizient und intelligent
zu komprimieren.
Ich auch nicht. Gerade MOD Patterns sind ja ein Paradebeispiel für Redundanz. Und je größer die Redundanz, umso stärker die Möglichkeit zur Kompression.
Ich persönlich finde nur, es gibt da 2 Arten von Kompression. 1.) Eine Kompression zum absolut kleinstmöglichen Speichern, vielleicht um sie auf winzigen Datenträgern zu lagern oder über ein Nadelöhr von DFÜ-Medien zu versenden o.ä.
Diese ist geeignet, wenn die Größe absolute Priorität hat und die Performance beim Entpacken nebensächlich ist.
2.) Eine Kompression, die nicht absolut kleinstmögliche Ergebnisse erzeugt, sondern solche, die auch noch mit gescheiter Performance "In Time" gelesen und verarbeitet werden können - also nicht vor lauter Verweisen oder komplizierter Berechnungen, um den entpackten Zustand wiederherzustellen zuviel Leistung=Zeit erfordert.
zatzen hat geschrieben:Aber mitunter winkt eine größere Effizienz als wenn man einfach ZIP drüberjagt.
Ja, nur würde ich ZIP dann nicht mehr "live" entpacken+abspielen wollen.
zatzen hat geschrieben:Ähnlich wie bei meinem ZVID, welches zwar an sich Grafiken nicht ganz so klein bekommt
wie GIF, dann aber wiederum, ergeben sich deutlich kleinere Daten, wenn man ein ZVID mit ZIP
komprimiert, als wenn man Grafik komprimiert, die nicht durch ZVID gelaufen ist.
Ja, wie gesagt - selbst auf DOS-Rechnern mit ihren kleineren Festplatten ist es für mich heutzutage nicht mehr SOOO wichtig, ob ein File auf HDD nun 50kB oder 53kB groß wird - weil es bei großen Platten aufgrund der Sektorgrößen sowieso keinen Unterschied mehr macht. Wo ich "auf einfache Art" packen kann (Lauflängencodierung ist ja immer irgendwie drin, das ist ja quasi das kleine 1x1 des Packens), mach ich das natürlich auch.

Mir geht es bei der Größe von Daten wirklich eher darum, wie groß bzw. klein diese sich dann im RAM breitmachen - denn Texturen/Grafiken oder Samples sind wahre Speicherfresser und wenn ich bei solchen großen Daten den Speicherbedarf erheblich reduzieren kann, ist es mir auch mal z.B. eine 256 Bytes große Tabelle im Codebereich wert.

Ich führe da ständig intern mit mir solche Verhandlungen/Trade-Offs, ob ich manche Dinge besser in Code oder besser in Ergebnis-/Entscheidungs-Tabellen realisiere. Tabellen (Look-Up-Tables) liefern natürlich meist schnellere Ergebnisse (mehr Performance), fressen aber auch mehr (Code-)Speicher. Ohne Tabellen, sondern mit Code kann man diesen Speicher natürlich sparen, aber wenn der Code aus massenhaften Divisionen besteht, geht die Performance natürlich schnell in den Keller.

Hier versuche ich immer, einen guten Mittelweg zu finden, schmeiße eine an sich gut aussehende Idee auch schon mal wieder weg, wenn sie zwar Mühe gemacht hat, aber doch nicht das erhoffte Ergebnis bringt. Meist besteht der Mittelweg aus einer Kombination von beidem. Hier muß man auch etwas "abstrakt" im Kopf behalten, wie die zu erwartenden Daten im Durchschnitt so zur Laufzeit aussehen werden, die durch diesen Code/Tabelle gehen werden und danach abzuschätzen, wo Platz zugunsten Performance verschwendet werden kann und wo Performance zugunsten von Platz.

Das ist es auch, was ich meine, wenn ich immer sage, daß Compiler noch so gut sein können, aber natürlich trotzdem nicht wirklich "wissen" können, was das Programm machen wird oder welche Art Daten der Normalfall sein werden, die ein Programm verarbeiten wird. Daher kann eine compilerseitige Optimierung nur eher allgemein erfolgen - aber nicht an das gewünschte "Problem" angepaßt, denn dieses kennt nur der Programmierer.

Es gibt ja (gerade heutzutage) viele Leute, die meinen, Assemblerprogrammierung sei völlig obsolet und die ganzen Interpreter- und Skriptsprachen, die heutzutage so genutzt werden, hätten alle Compiler, die gut genug sind, um selbst zu optimieren. Demgegenüber stehen die ganzen riesigen und langsamen Programme, die heutzutage auf Rechnern laufen, die selbst wahnsinnig schnelle CPUs und wahnsinnig großen RAM haben - und die trotzdem nicht performen und ständig den kompletten RAM für sich selbst haben wollen. Und selbst WENN man auch in Hochsprache oder ausschließlich in Hochsprache programmiert, kann ein Verständnis um die Interna der Maschine, für die man programmiert, hier in allen Bereichen (Speicherverbrauch, Performance) immer entscheidende Vorteile bringen.

Wer zu faul ist, etwas anderes zu lernen als sein Lieblingsskript, ist doch völlig OK. Aber diese Scheinargumente, daß wer sich heute noch um Maschineninterna kümmert, dumm sein muß, weil die Skripte und systemferne Hochsprachen ja angeblich besser als Menschen optimieren - nur weil man in Wahrheit keine Lust hat, sich damit zu beschäftigen, die kann ich schon nicht mehr hören... - aber das nur am Rande.

Und ich finde es heute, wo Gigabyte-weise Speicher verschwendet wird, immer wieder erfrischend, daß es auch noch Leute wie Zatzen gibt, denen es nicht scheißegal ist, wenn der ganze Speicher voll mit Nullen ist, so daß kein Platz mehr für valide Daten vorhanden ist, sondern die sich die Mühe machen und auch über Lösungen nachdenken, um nicht nur "Hauptsache fertig", sondern auch "es kann auch ruhig mal viel besser sein als der Durchschnitt" als Maßstab anlegen.
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Das betrifft nur LOKALE Variablen
Ich habe hier aber eine assembler-deklarierte Procedure, die trotzdem eine Variable im Header hat.
Macht Pascal daraus nun eine mit Pascal-Rahmen und ignoriert die Deklaration "assembler"?
Hier fragte ich ja auch, ob die Variable "length" nicht direkt im Register AX zu finden sein wird.

Code: Alles auswählen

procedure mix_in_channel(length: word); assembler;
Kann nicht verstehen, wie jemand auf ein Format (MOD) kommen konnte, das im Regelfall zu 95% aus Nullen besteht.
Das wird wohl dran gelegen haben dass 4 Kanal Patterns mit ihren 1024 Bytes wenigstens noch in 64 KB
reinpassen selbst bei ziemlich langen Musikstücken. Gleichzeitig hat sich aber wohl auch niemand Gedanken drüber
gemacht wie man bei Formaten wie mit 16 Kanälen die Daten schrumpfen könnte, weil bis dahin die Computer
potenter wurden und sowas wie 128 KB allein für Patterns niemandem weh getan haben. Abgespeichert werden
seit Trackern wie Fasttracker II die Patterns mit einer gewissen simplen Kompression, ich glaube aber, im Speicher
werden diese Patterns aus Performancegründen ungepackt gehalten.

Das eher redundante Gerüst bei meinem Format ist mir schon noch ein Dorn im Auge. Konkret sehe
ich nur ein Problem darin, Verweise auf die ganzen Track-Gerüste zu machen, die müssten nämlich
wahrscheinlich mit mehr als 8 Bit erfolgen, ich würde wieder direkte Offsets in die Gerüst-Daten
machen - aber 16 Bit verbrauchen ja schon wieder so viel wie 16 Zeilen...
Okay naja der Gedanke war, dass ich mir eine extra-Offset-Tabelle sparen möchte für die Gerüstdaten.
Siehe Overhead zu der ganzen Sache, muss ich mir überlegen, sobald Gerüste mehrfach vorkommen ist
ja schon viel gespart, trotz Overhead.
Ich würde eine Assembler Routine schreiben, welche die Track-Gerüste ausliest - die Leseroutinen
selbst wären weiterhin simpel.
Die Gerüstdaten würden komprimiert (aber on-the-fly auslesbar durch die asm-Routine), und
die Anzahl der Gerüste auf 256 beschränkt (theoretisch gibts es so viele Kombinationsmöglichkeiten:
Anzahl Tracks * Anzahl Patterns, bei 16 Kanälen wäre da bei 16 Patterns Schluss), wenn die
Gerüsttabellen alle belegt sind gäbe es für abweichende Tracks die Deklaration, dass der dann doch
wieder ein individuelles Gerüst bekommt durch Bits einlesen, oder wenn es ein Track ist für den sich
das Anlegen des Gerüsts in der Datenbank gar nicht lohnt.
Viel Arbeit für den Converter - aber wenig für den Reader.

Und ja, ich bin da noch weiter am Überlegen, weil ich eben nicht nach dem Prinzip "Hauptsache fertig" denke,
sondern in diesem Fall die Motivation auch die Herausforderung ist, Patterndaten so gut zu schrumpfen wie
möglich.

Aber ok - wieder drüber nachgedacht - es würde wohl kaum was einbringen aber außerdem weitere Beschränkungen
mit sich bringen. Dann vielleicht doch lieber noch ergänzen, dass die Notenwerte über eine Zwischentabelle laufen,
wenn es sich denn lohnt und auf dem Kanal nur ein Sample ist (weil sonst mehrere Tabellen nötig wären).


Über C++ wird immer gesagt, es optimiert den Code besser, als man es per Hand und Assembler selber könnte.
Wahrscheinlich stimmt das sogar für die heutige Rechnerarchitektur. Aber die ist ja auch zugunsten der
Performance Speicher-verschwendend ausgelegt. Alles wird 32-Bit-weise Strukturiert, weil es sonst langsamer
wäre.
mov ax, 13h
int 10h

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

Re: Trackermodul-Engine (sehr einfach)

Beitrag von DOSferatu »

zatzen hat geschrieben:
Das betrifft nur LOKALE Variablen
Ich habe hier aber eine assembler-deklarierte Procedure, die trotzdem eine Variable im Header hat.
Macht Pascal daraus nun eine mit Pascal-Rahmen und ignoriert die Deklaration "assembler"?
Ich habe es getestet.
(Ja, ich erstelle manchmal so Test-EXE Files und sehe sie mir dann im Hexeditor an.)
Für eine reine Assembler-Routine wie

Code: Alles auswählen

procedure Beispiel; assembler
asm
....
end;
wird kein "Stack-Frame" erzeugt.

Für eine Assembler-Routine wie

Code: Alles auswählen

procedure Beispiel2(value:word); assembler
asm
....
end;
Wird ein Stackframe erzeugt, d.h. am Anfang fügt Pascal ein:

Code: Alles auswählen

push BP;
mov BP,SP;
und am Ende (vor dem Return)

Code: Alles auswählen

leave;
(der LEAVE Befehl macht bekanntlich mov SP,BP; pop BP;)
D.h. es wird ein Stackframe angelegt weil die Parameter-Übergabe ja schon eine Variablen-Deklaration ist.

Für reine Pascal-Procedures/Functions (d.h. welche, die mti begin/end;) wird IMMER das Stackframe angelegt - egal, ob Variablen im Header übergeben werden oder nicht und egal, ob zusätzlich noch lokale Variablen mit var/const deklariert werden oder nicht!

Interessant ist auch irgendwie, daß Pascal hier zwar am Ende den LEAVE-Befehl benutzt, aber nicht am Anfang den ENTER-Befehl, der ja eigentlich der "Bruder" des LEAVE ist, wenn zusätzlich keine lokalen Variablen deklariert werden!
Werden zusätzlich lokale Variablen deklariert, wird ein

Code: Alles auswählen

enter GROESSE,0
eingefügt, wobei GROESSE dann der Anzahl in Bytes entspricht, die die lokalen Variablen belegen.

Achja, kleiner Tip: Pascal legt Variablen ab Größe 2 (außerhalb von RECORDS) immer auf gerade Offsets. Deshalb ist es keine gute Idee, sowas zu machen:

Code: Alles auswählen

var a:byte;
var b:word;
var c:byte;
Dann wird nämlich nach a eine Stelle im Speicher "freigelassen", danach kommt erst das b und dann c. (Und nach c wird wieder eine Stelle freigelassen. Hier ist besser:

Code: Alles auswählen

var b:word;
var a:byte;
var c:byte;
beziehungsweise:

Code: Alles auswählen

var b:word;
var a,c:byte;
In dem Fall werden nur 4 Bytes im Stack belegt statt wie oben 6.
Tja - ein reiner Hochsprachen-Programmierer, der sich um so etwas nicht kümmert, wüßte das nicht...
Und ja, das dient dazu, weil die x86-CPUs an geraden (bzw überhaupt "aligned") Offsets schneller arbeiten. Ich weiß aber nicht, ob das überhaupt noch ab 386er zutrifft.
Achja: Im Procedure/Function Header übergebene Bytes werden intern übrigens immer als Words übergeben. Es können keine ungeraden Anzahlen auf dem Stack übergeben werden. Das liegt an der CPU.
zatzen hat geschrieben:Hier fragte ich ja auch, ob die Variable "length" nicht direkt im Register AX zu finden sein wird.

Code: Alles auswählen

procedure mix_in_channel(length: word); assembler;
Das verwechselst Du mit der Ausgabe.
Bei FUNCTIONS ist es so, daß der Ergebniswert einer Pascal-FUNCTION so zurückgegeben wird:
byte/boolean/shortint: in AL
word/integer: in AX
longint/pointer: in DX:AX (ja, Pascal kann nur 286er, daher kennt es EAX nicht und gibt Longints nicht in EAX zurück.
real: in DX:BX:AX (real ist 6 Byte groß)
alle anderen: im Stack

Wenn man natürlich reine ASM-Procedures/Functions schreibt, kann man das zurückgeben, wie man will. Wenn man aber eine ASM-Function schreibt, kann man den Kram natürlich zurückgeben, wie man will. Wenn man aber die Function so schreiben will, daß sie in Pascal wie eine Pascal-Function verwendet werden kann, gibt man die Werte am besten auch wie obengenannt zurück. Dann kann man nämlich auch eine reine ASM-Function in Pascal ganz normal benutzen z.B. mit:

Code: Alles auswählen

Ergebnis:=Beispielfunc(Wert);
selbst wenn Beispielfunc eine reine Assembler-Function ist.

Das gehört mit zu den Gründen, wieso ich Pascal so mag - es macht alles quasi genauso, wie es erwartet werden würde. Das macht es viel leichter, Assembler in Pascal einzufügen, ohne daß der Pascal-Code ringsherum irgendwie "durcheinanderkommt". Natürlich optimiert Pascal selbst sehr wenig. Aber wenn man Pascal sowieso nur als "Rahmen" benutzt, um nicht-Echtzeit-relevante Sachen zu machen und um Assembler drin einzubinden, ist das kein Problem.

Kleiner Exkurs meinerseits zu C vs. Pascal:
In Borland C kann man innerhalb von Assembler nichtmal Labels benutzen. Da muß man den ASM-Block beenden, ein Label setzen und dann den ASM-Block weiterführen. Ich hab das mal versucht, so zu arbeiten (bzw. allgemein mit Borland-C zu arbeiten). Da wird man ja wahnsinnig. C z.B. kann auch keine Procedures/Functions verschachteln. Und wieviel Bits ein Typ hat, ist in C nicht eindeutig definiert (OK, in Borland-C ist es an x86-Konventionen gebunden. Aber damit natürlich nicht 100% portierbar, weil ein WORD (also unsigned integer) in C nicht auf jedem System z.B. 16bit entsprechen muß.
Ich persönlich verstehe bei solchen Sachen nicht, wieso dann viele Leute C so verherrlichen (mal abgesehen von der Syntax). Eine so "unordentliche" Sprache wie C ist mir noch nirgends begegnet, da ist selbst BASIC "aufgeräumter".
Aber das muß natürlich jeder selbst wissen.
zatzen hat geschrieben:
Kann nicht verstehen, wie jemand auf ein Format (MOD) kommen konnte, das im Regelfall zu 95% aus Nullen besteht.
Das wird wohl dran gelegen haben dass 4 Kanal Patterns mit ihren 1024 Bytes wenigstens noch in 64 KB reinpassen selbst bei ziemlich langen Musikstücken. Gleichzeitig hat sich aber wohl auch niemand Gedanken drüber gemacht wie man bei Formaten wie mit 16 Kanälen die Daten schrumpfen könnte, weil bis dahin die Computer
potenter wurden und sowas wie 128 KB allein für Patterns niemandem weh getan haben. Abgespeichert werden
seit Trackern wie Fasttracker II die Patterns mit einer gewissen simplen Kompression, ich glaube aber, im Speicher
werden diese Patterns aus Performancegründen ungepackt gehalten.
Ja, daß "sich niemand Gedanken gemacht hat", ist auch so ziemlich der einzige Grund, den ich mir dazu vorstellen kann.

Weil ich selbst aber eher "vom C64 komme" und da diesen Tracker (MVM = MegaVisionMusic von Stefan Konrath) benutzt habe, habe ich mich daran etwas orientiert. Und dieser verschwendet keine Bytes, indem er mehrstimmige nullengefüllte linien/takt-basierte Patterns nutzt, sondern er hat quasi eine ART "Sequenzer" für jede der 3 Stimmen extra und dann kann man "Blocks" definieren, die man in jeder der Stimmen benutzen kann. Ein "Block" hat da bis zu 255 (nicht 256) "Befehle" (kann auch weniger haben!), die natürlich meist Noten sind, zusammen mit Längen. Die Sequenzer geben dann die Blocks an, haben aber auch (Rück-)Sprungbefehle und Befehle für Speed und glaub Transponieren.
Somit wird auf dem C64, der ja immer eher "knapp an Speicher" ist, kein Speicher mit Nullen verschwendet, aber es ist trotzdem noch vernünftig bearbeitbar, d.h. die CPU muß bei diesen Daten trotzdem keine extrem komplexen Berechnungen anstellen, um sich die Daten "aus dem Stück zu ziehen".
In den Blocks gibt es auch so "Instrument" Befehle, d.h. man kann innerhalb des Blocks die Instumente wechseln. Und diese sind extra definiert, eben mit den Registerwerten, die der SID so hat und optional auch mit einer ganzen Sequenz von Registern (damit wird dann z.B. die Percussion abgedeckt). Diese kann man natürlich auch selbst definieren.
An sich ist das eine feine Art, Musik für SID zu speichern/erstellen.

Ich selbst habe es mir da noch einfacher gemacht und einfach alles an Befehlen "auf dieselbe Ebene" verfrachtet. Das gibt einerseits mehr Handlungsspielraum - andererseits hätte man aber ebenfalls auch die Möglichkeit, das Ganze mit Sequenzern und Patterns darzustellen und eine "tracker-artige" Struktur zu simulieren (intern würde das Ganze dann mit Sprungbefehlen bzw mit "Unterprogrammaufrufen" dargestellt).

Nutzt man aber das komplette System so wie es ist, kann man auch wiederholende Sequenzen einfach mit Schleifen abbilden, die gleiche Sequenz in verschiedenen Tonhöhen oder mit anderen Instrumenten, Lautstärken, Geschwindigkeiten abspielen - und somit könnte man, obwohl mein Format nicht ganz so "bitbasiert" ist wie ZVM, trotzdem noch Speicher sparen. Musik besteht ja oft aus sich wiederholenden Teilen - gerade bei den Begleitstimmen. Hat man z.B. eine Percussion-Sequenz aus 4 Tönen die sich für lange Zeit nicht ändern, so kann man einfach eine Schleife darumlegen und die Sequenz bis zu 256 mal wiederholen lassen. Und da es ja 2 "Register" gibt, kann man auch zwei Schleifen verschachteln und eine Sequenz auch bis zu 65536 mal wiederholen. Und da es ja auch noch den Stack gibt, kann man darüber weitere "Instanzen" zwischenspeichern...

Natürlich ist mir klar, daß ein Musikstück dadurch etwas "unübersichtlicher" wird - da so die Töne nicht mehr so schön "nebeneinander stehen" wie in Trackern üblich. Aber eine Ausgabe eines Stücks als solche Sequencen (also mit nebeneinanderliegenden Stimmen, quasi "decompiliert", wäre natürlich trotzdem möglich. Darin zu editieren wäre dann natürlich aufwendiger, könnte aber trotzdem bewerkstelligt werden. (Ich hatte ja mal irgendwann angedacht, eine "Tracker-Ansicht" einzubauen.)
zatzen hat geschrieben:Das eher redundante Gerüst bei meinem Format ist mir schon noch ein Dorn im Auge. Konkret sehe
ich nur ein Problem darin, Verweise auf die ganzen Track-Gerüste zu machen, die müssten nämlich
wahrscheinlich mit mehr als 8 Bit erfolgen, ich würde wieder direkte Offsets in die Gerüst-Daten
machen - aber 16 Bit verbrauchen ja schon wieder so viel wie 16 Zeilen...
Okay naja der Gedanke war, dass ich mir eine extra-Offset-Tabelle sparen möchte für die Gerüstdaten.
Siehe Overhead zu der ganzen Sache, muss ich mir überlegen, sobald Gerüste mehrfach vorkommen ist
ja schon viel gespart, trotz Overhead.
Naja, wie ich bereits erwähnte, spart Dein Format gegenüber MOD schon jetzt ganz erheblich an Speicher.
zatzen hat geschrieben:Ich würde eine Assembler Routine schreiben, welche die Track-Gerüste ausliest - die Leseroutinen
selbst wären weiterhin simpel.
Die Gerüstdaten würden komprimiert (aber on-the-fly auslesbar durch die asm-Routine), und
die Anzahl der Gerüste auf 256 beschränkt (theoretisch gibts es so viele Kombinationsmöglichkeiten:
Anzahl Tracks * Anzahl Patterns, bei 16 Kanälen wäre da bei 16 Patterns Schluss), wenn die
Gerüsttabellen alle belegt sind gäbe es für abweichende Tracks die Deklaration, dass der dann doch
wieder ein individuelles Gerüst bekommt durch Bits einlesen, oder wenn es ein Track ist für den sich
das Anlegen des Gerüsts in der Datenbank gar nicht lohnt.
Viel Arbeit für den Converter - aber wenig für den Reader.
Ja, ich bin ebenfalls ein großer Freund davon, Daten, die später im fertigen Projekt verwendet werden, soweit wie möglich "vorzubereiten", damit sie dann im Projekt (Spiel, Anwendung, etc.) selbst mit so wenig Aufwand an Speicher und Rechenzeit genutzt werden können.
zatzen hat geschrieben:Und ja, ich bin da noch weiter am Überlegen, weil ich eben nicht nach dem Prinzip "Hauptsache fertig" denke, sondern in diesem Fall die Motivation auch die Herausforderung ist, Patterndaten so gut zu schrumpfen wie
möglich.
Ja, wie aber bereits von mir (und anderen) erwähnt, ist es natürlich einerseits gut, durch das Schrumpfen von Patterndaten die Gesamtgröße der Sounddaten zu reduzieren. Andererseits machen in vielen (eher den meisten) Fällen die Patterndaten prozentual den geringsten Anteil an solchen Sounddaten aus - der größte Teil wird ja durch die Samples abgedeckt. Mein WAV2SMP Programm supportet übrigens sogar einen WAV-Typ (Typ 6), der quasi 12 Bit WAV mit 8 Bit darstellt, mit einer Variante von "Fließkomma" Darstellung (3 Bits Exponent). Der Qualitätsverlust ist so geringer als bei simpler Konvertierung von 16 zu 8 Bit, da leise Töne "genauer", laute ungenauer, aber eben mit größerer Amplitude dargestellt werden - was im Normalfall dem menschlichen Gehör entspricht. Eine Abart des Ganzen (z.B. mit 6bit-Darstellung) könnte auch den Sample-Speicher reduzieren, so daß man 8bit-Samples mit "6bit-Fließkomma" speichert. Natürlich wäre keine wirkliche Fließkomma-Berechnung (Bitschieberei) nötig, sondern nur eine im Code integrierte 64 Bytes große Tabelle...
Egal... Man kann eine Menge anstellen...
zatzen hat geschrieben:Aber ok - wieder drüber nachgedacht - es würde wohl kaum was einbringen aber außerdem weitere Beschränkungen mit sich bringen. Dann vielleicht doch lieber noch ergänzen, dass die Notenwerte über eine Zwischentabelle laufen, wenn es sich denn lohnt und auf dem Kanal nur ein Sample ist (weil sonst mehrere Tabellen nötig wären).
Ein Konverter von Deinem Format in ein anderes wäre auch schon wieder eine ziemliche Arbeit...
zatzen hat geschrieben:Über C++ wird immer gesagt, es optimiert den Code besser, als man es per Hand und Assembler selber könnte. Wahrscheinlich stimmt das sogar für die heutige Rechnerarchitektur. Aber die ist ja auch zugunsten der
Performance Speicher-verschwendend ausgelegt. Alles wird 32-Bit-weise Strukturiert, weil es sonst langsamer
wäre.
Naja, das "Problem" beim Optimieren ist, daß ein Compiler nur den "Code an sich" optimieren kann, aber nicht wirklich "weiß", was der Code am Ende anstellen wird - d.h. mit welchen Daten das entsprechende Programm später arbeiten wird. Desweiteren kann ein MENSCH vielleicht auch seinen gesamten Code mehr oder weniger (zumindest sinngemäß) im Kopf haben, um zu wissen, was dieser wann tut und wie die Code-Teile zusammenarbeiten. Bei einem Compiler kann ich mir nicht vorstellen, daß dieser einen Source, der ein 10 oder 20 kByte (!) Resultat an Binärcode ergibt, gleichzeitig überblickt und im Ganzen dahingehend optimiert, daß alle Teile des gesamten Klotzes optimal miteinander zusammenarbeiten - und das, ohne zu wissen (nur quasi zu erraten), was dieses Programm am Ende tut. Selbst eine heutige KI wäre wohl kaum imstande, aus so einem Code zu ermitteln, was für eine Art Programm es ist.

ISM (ohne die externen Routinen zum Laden/Entladen von Samples) hat schon ca. 13,5 kB Binärcode.

Bei GameSys2 sind es ca. 20 kB. Die "Adressierungsarten" für die Parameter bestehen aus riesigen Unterprogrammen, um so wenig wie möglich Sprünge und Aufrufe zu verwenden, da natürlich jeder Befehl aus GameSys2 bis zu 4 solcher Parameter haben kann und jeder Parameter jede der 30 Adressierungsarten haben kann, zusätzlich aber auch noch definierte Größen. Würde ich hier generalisierte Routinen benutzen, wäre die Performance schnell im Keller. Ein Hochsprachenprogrammierer würde aber spontan genau das tun. Bei einer VM wie GameSys2 wäre das aber dermaßen kontraproduktiv, daß damit ihr ganzer Nutzen in Zweifel gezogen werden würde: In einem solchen Fall würde man dann wohl besser fahren, wenn man gleich alle "Figuren" durch festcodierten Hochsprachencode steuern würde, da wäre die VM nur eine weitere Abstraktionsstufe, die das Ganze langsamer machen würde, ohne den geringsten Vorteil.

Und ja - manchmal muß man mit Absicht Speicher verschwenden, um Performance zu erhalten, manchmal absichtlich Performence verschwenden, um Speicher zu sparen. Man kann zwar einem Compiler heutzutage schon mitteilen, ob er auf Speicher oder Performane optimieren soll - aber oft ist hier die Mischung von einem Code-Teil zum nächsten die bestmögliche Methode. Und die kann man nur erkennen, wenn man weiß, "was das Programm macht".

Ich selbst tue manchmal schon wirklich seltsame Sachen, weil ich mich inzwischen mit den Fähigkeiten der x86-Register, sowie den Flags recht gut auskenne. Das sind manchmal auch Dinge, die in keiner Anleitung stehen. Ich modifiziere z.B. manchmal Code, indem ich ganze Befehlssequenzen während der Laufzeit aus Tabellen durch andere austausche oder nutze die Flags als Bitmuster für Entscheidungstabellen... usw. Und ob ein Hochsprachen-Compiler solche Dinge selbständig macht, wage ich doch noch zu bezweifeln. Außerdem ist bekannt: Je mehr man selbst optimiert (oder Dinge wie die o.g. bereits in Hochsprache tut), umso schwerer wird es für den Compiler zu optimieren - Programmierer und Compiler "optimieren dann manchmal gegeneinander".
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Danke für das Testen mit dem Stackframe!

Man könnte ja auch sagen, Zatzen, nimm dir mal ein dickes Buch und kümmer dich selbst,
aber ich habe bereits ein dickes Assembler Buch und davon ausgehend schätze ich, dass
das Thema zu umfangreich ist um es sich innerhalb ein paar Bücher anzulesen - ich komme
sehr gut damit zurecht dass ich praktisch mit Learning by Doing da rangehe und Schritt
für Schritt neue Möglichkeiten umsetze.
Also quasi mehr wie ein Quereinsteiger.

Wenn ich das richtig verstanden habe, verbraten Sprungbefehle nicht unerheblich viele
Taktzyklen. So verstehe ich auch den Sinn von Loop-Unrolling, neben der Einsparung
von etwas wie "DEC CX" spart man eben auch den Sprung, der viel mehr Zeit frisst.
Nun habe ich in meinen Mischroutinen neben der CX Schleife auch noch einen Sprung
drin, nämlich nach der Überprüfung mit der Sample-Länge. Das werde ich, wenn ichs
denn hinkiege, vorher berechnen und CX anpasssen, so dass ich es in der Schleife gar
nicht mehr überprüfen muss.

Zu ISM ein Gedanke: Wenn ich es richtig verstehe definierst du je Note mit 4 Byte
wo genau diese zustehen hat und alle anderen Parameter. Die Darstellung könnte
nun ja so ähnlich ausfallen, als wenn du sagst, okay, ich will das jetzt als Noten-
blatt haben zum Ausdruck, damit das jemand auf dem Klavier spielt.
Heisst also, in der Darstellung redundant bzw. mit Leerräumen, der besseren
Übersicht wegen, aber trotzdem editierbar und intern ohne Verschwendung
gespeichert. Hast du also meinetwegen bei eine Note die angezeigt wird
eine Wiederholung definiert, so könntest du diese Note irgendwie einfärben
und die Duplikate ebenfalls, so dass man sie als zugehörig erkennt.
Oder nicht einfärben sondern eine kleine Nummer dran, wie auch immer.
Aber das Tracker-Format als Darstellung wäre wohl noch das naheliegendste.

Nach wie vor wäre dieses Textfile sehr von Nutzen...
Oder habe ich es schon? OPCODES.TXT/BYTECODES.TXT?
mov ax, 13h
int 10h

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

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Hier die neue Version von ZSMPLAY. Dürfte deutlich schneller sein als die vorherige.
@Dosenware, könntest du nochmal 16-stimmige Musikstücke damit testen ob sie
noch stottern?
http://www.zatzen.net/ZSMP002B.ZIP
mov ax, 13h
int 10h

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

Re: Trackermodul-Engine (sehr einfach)

Beitrag von Dosenware »

Times 29-33 läuft jetzt stotterfrei :like:
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Super, danke!
Ich glaube da macht sich die Einfachheit des Formats bezahlt.

Aber ich habs mal verglichen - ich kann ja bei DOSBOX die Cycles runterschrauben...
X-Tracker spielt das Stück noch flüssig bei ca. 4300 Cycles, mein Player braucht 5000...
Allerdings ist das bei X-Tracker irgendwie anders geregelt. Bei wenig Leistung bleibt
der Sound noch stabil, aber das Update der Anzeige wird sehr langsam, d.h. die
Framerate geht zurück auf bis zu 1/sec...
Bei mir bleibt die Anzeige flott, aber der Sound fängt an zu stottern.

Naja, vielleicht nicht dramatisch, aber bei so einem abgespeckten Format sollte es eigentlich
Ehrensache sein dass es weniger Rechenaufwand braucht als das Original.
Das wäre noch mein Ziel, den Rechenaufwand zu unterbieten. Bzw. wird es auch
einfach an Speicherzugriffen liegen, oder bestimmte Befehle sind einfach langsam,
so dass man vielleicht besser Tabellen verwendet.
mov ax, 13h
int 10h

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

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Ich bräuchte mal Hilfe...

Ich würde statt "mov al, fs:[si+bx]; cbw" gern "movsx ax, fs:[si+bx]" verwenden, oder wie auch immer
man das formulieren muss dass der asm weiss dass ich nur ein Byte aus fs:[si+bx] laden will.
Leider kennt Pascal kein movsx und bevor ich mich blöd suche kann mir vielleicht jemand sagen
ob das funktionieren kann und welche Bytecodes das wären...

Hier nochmal eine der Kernschleifen, der einzige Bereich der wirklichen Einfluss auf die Geschwindigkeit hat:

Code: Alles auswählen

  @loopvol7:
  cmp bx, bp
  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
Ich will euch hier nicht zuspammen mit Code, aber vielleicht sieht ja jemand noch was.

Kleine Erläuterung zum Code:
(E)BX: momentane Sampleposition
(um 16 Bit rotiert, daher nach EBX+EDX noch ADC BX,0 (danke, DOSferatu))
(E)DX: Sample Geschwindigkeit d.h. Fortschreiten pro Fortschreiten im Puffer (ebenfalls 16 Bit rotiert)
BP: Sample-Länge
ES:[DI]: 16 Bit-Puffer Adresse (ich mische in einen 16 Bit Puffer, damit ich die Samples nicht schon
beim Zusammenmischen "stauchen" muss und außerdem später die Möglichkeit habe, die Lautstärke
optimal zu regeln)
DS:[SI+BX] bzw. FS:[SI+BX]: Sample Adresse

Ich habe gestern schon versucht, eine Verbesserung herbeizuführen durch Vorberechnung
von CX, anhand der zu erwartenden abzuspielenden Sample-Länge, dann kann man sich
das cmp bx, bp und den Sprung sparen. Schien aber gar nicht so viel auszumachen, und
es wäre nur für die Routinen möglich gewesen, die keine Sample-Schleifen spielen, also
hab ichs wieder rausgenommen. Eine merkliche, jedoch immer noch bescheidene
Verbesserung hatte ich heute mit dem Versuch, statt einem Word direkt ein DWord mit
einem Schlag pro Schleifendurchlauf in den Puffer zu mischen, und CX dafür vorher zu halbieren.
Die Lautstärke wird (für obigen Code als Beispiel) in der ersten Hälfte durch SAL EAX, 21
umgesetzt und im zweiten Teil durch SAL AX, 5.
Das funktionierte ganz gut, einen Strich durch die Rechnung machte nur dass man
EAX nochmal rotieren musste, damit die Words in der richtigen Reihenfolge in den
Puffer addiert werden. Jetzt ist mir eingefallen dass man diese Rotation auch
nachträglich für den gesamten Puffer erledigen könnte, da er sowieso noch in einen
8 Bit Puffer übertragen werden muss. Da wäre also noch was möglich.
Ich muss mich dann nur noch irgendwie um den Rest kümmern, wenn der eigentliche
Wert von CX, also die Länge an Samplepunkten, die eigentlich in Puffer geschrieben
werden sollen, ungerade ist. Aber das wird wohl machbar sein.
mov ax, 13h
int 10h

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

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

So, CBW hab ich schonmal raus, und das hat relativ deutlich was gebracht, dann habe ich
noch DOSferatus Methode mit dem bspw. FFFF0002 Zähler an EDI umgesetzt, dann wurde es auch
nochmal etwas schneller:

Code: Alles auswählen

  @loopvol7:
  cmp bx, bp
  jnb @end
  db 64h; mov ah, ds:[si+bx]
  sar ax, 3
  add es:[di], ax
  db 66h; add bx, dx
  adc bx, 0
  db 66h; add di, cx
  jc @loopvol7
Ich lade einfach die Bytes in AH und shifte entsprechend anders - was unterhalb Bit 0
der Samples passiert ist nicht hörbar, so brauche ich nicht einmal AL zu nullen.
CX enthält den Addierwert. Über ein Register gehts wohl sowieso schneller, ich
wüsste aber auch gerade nicht wie man einen 32 Bit immediate Wert innerhalb
Pascal gültig machen kann.

Damit habe ich jetzt die vorher erwähnte Grenze von 4300 Cycles fast erreicht,
schonmal nicht schlecht. Darunter zu kommen wäre noch mein Ziel womit ich
mich zufrieden geben würde.

Ich dachte ja, die Lautstärken mit Shifting zu erledigen wäre ein super Trick
damit es schnell geht, aber würde es über Tabellen noch schneller gehen??
mov ax, 13h
int 10h

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

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Weiterhin in der Hoffnung, euch nicht zu nerven oder zu langweilen, hier eine weitere Modifikation.
Wie man sieht fällt einem doch immer noch was ein.

Code: Alles auswählen

  @loopvol7:
  db 64h; mov ah, ds:[si+bx]
  sar ax, 3
  add es:[di], ax
  db 66h; add bx, dx
  adc bx, bp
  db 66h; add di, cx
  jc @loopvol7
Ich habe jetzt doch die Schleifenlänge vorausberechnet (in ASM natürlich), auch wenn ich das nur
für das Abspielen schleifenloser Samples hinkriege bringt es doch was in der Praxis. CMP BX, BP fällt
dadurch weg, und BP habe ich als genulltes Register für ADC BX, BP eingesetzt, wird wohl schneller
sein als mit 0 als immediate. Jetzt komme ich bei "Times.ZSM" mit ca. 4000 Cycles hin, von ein paar
dünn gesäten Stolperern abgesehen läuft es flüssig.
Ich glaube, je weniger Zeit diese Kernschleife braucht, desto mehr fallen dann langsam auch die
übrigen Prozeduren ins Gewicht.
mov ax, 13h
int 10h

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

Re: Trackermodul-Engine (sehr einfach)

Beitrag von DOSferatu »

zatzen hat geschrieben:Danke für das Testen mit dem Stackframe!
Keine Ursache.
zatzen hat geschrieben:Man könnte ja auch sagen, Zatzen, nimm dir mal ein dickes Buch und kümmer dich selbst,
aber ich habe bereits ein dickes Assembler Buch und davon ausgehend schätze ich, dass
das Thema zu umfangreich ist um es sich innerhalb ein paar Bücher anzulesen - ich komme
sehr gut damit zurecht dass ich praktisch mit Learning by Doing da rangehe und Schritt
für Schritt neue Möglichkeiten umsetze.
Also quasi mehr wie ein Quereinsteiger.
So geht's mir auch. Oft braucht man nur "die eine entscheidende Information" und will nicht den ganzen
Wust "ringsherum" wissen. Beispiel: Ich weiß mittlerweile seit Jahren, wie ich die verschiedenen Mode-X
(dieser spezielle VGA-Grafik"Hack") baue, habe da auch eigene zusätzliche gebaut. - Aber ich weiß dann
eben NUR DAS - weil ich da die Register und welche Werte die kriegen müssen, nachgelesen habe, weil
es mal wo erklärt war (unter anderem übrigens auch im ASM86FAQ.TXT).

Da muß ich nicht ALLE Register der VGA auswendig kennen und wie jedes einzelne funktioniert - obwohl
ich da so ein Buch direkt von Tseng Labs habe (englisch), das haarklein alle Register Bit für Bit auflistet ---
für das sich wahrscheinlich damals manche Programmierer 'n Fuß abgehackt hätten ... und ich hab das
irgendwann "mal einfach so" von einem bekommen, der früher mal unter DOS programmiert hat. Manchmal
muß man auch Glück haben...

Aber wenn ich nur mal "den Mode-X brauche", weil ich gerade mit etwas anderem beschäftigt bin, dann
brauch ich auch NUR gerade mal das. Und da bin ich froh, wenn ich das auch mal so "zusammenfassend"
kriege ohne die ganze "Theorie dahinter" (obwohl ich die inzwischen auch kenne).
zatzen hat geschrieben:Wenn ich das richtig verstanden habe, verbraten Sprungbefehle nicht unerheblich viele
Taktzyklen. So verstehe ich auch den Sinn von Loop-Unrolling, neben der Einsparung
von etwas wie "DEC CX" spart man eben auch den Sprung, der viel mehr Zeit frisst.
Das "perfide" an Sprungbefehlen ist, daß sie nicht nur die Taktzyklen verbrauchen, die angegeben sind.
Zusätzlich löschen sie auch den Prefetch-Queue. Normalerweise holen "neuere" CPUs (also glaub schon
ab 286er, wenn nicht sogar schon 86er) während sie einen Befehl abarbeiten schon die nächsten paar.
So müssen sie nicht dauernd auf den Speicher zugreifen - bei Pentium und darüber dient das auch dem
sog. "Pairing" (wo möglich).

Wird gesprungen, wird das gelöscht, weil die CPU ja in dem Fall davon ausgehen muß, daß das eben
"im Vorfeld geholte" nun nicht mehr gebraucht wird, weil man ja an anderer Stelle im Speicher weiterlesen
muß. Somit muß die CPU da dann wieder "neu holen". Das verschwendet u.U. etwas Zeit.

Das Ganze kann man aber auch Nutzen, und zwar, wenn man WILL, daß der Prefetch-Speicher gelöscht wird.
Warum soll man sowas wollen? Nunja, Leute wie z.B. ich arbeiten auch gerne mal mit selbstmodifizierendem
Code. Manchmal werden einfach die Register knapp oder man braucht in einer Schleife eine vorberechnete
Konstante. Da ist zwar ein Register immer die schnellere/bessere Wahl, aber manchmal, wie gesagt, braucht
man die anderweitig. Die schlechteste Wahl (wenn es um Geschwindigkeit geht), ist, solche Konstanten irgendwo
indirekt aus dem Speicher zu holen (lokale/globale Variablen), weil das langsamer ist als "immediate" Konstanten
direkt im Code. Also packt man z.B. die berechnete Konstante (z.B. einen Adder oder sowas) an die gewünschte
Stelle (dafür nutzt man z.B. Labels HINTER einem Befehl und packt ein Word dann nach CS:[@Label-2] oder ein DWord
an CS;[@Label-4]) Dem eigentlich Befehl gibt man "irgendwas" als Wert, weil das ja sowieso überschrieben wird.
(Ich nehme da gern 77, 7777 oder 77777777 (damit es Bytes, Words und DWords werden und damit ich gleich sehe,
daß das Werte sind, die nachher überschrieben werden).

Was hat das mit Sprüngen zu tun?
Naja, sollte die zu modifizierende Stelle zu nah an der Stelle im Code stehen, die sie modifiziert (ich glaube 16 Bytes ist die Prefetch-Größe), dann ist der folgende (unveränderte!) Code schon eingelesen (und in der CPU!) und wird unverändert ausgeführt und erst ab dem nächsten Sprung oder wenn aus anderen Gründen das nächste Mal diese Stelle erreicht wird, wird die veränderte Version eingelesen. Bei Schleifen, vor denen so eine Änderung erfolgt und die Stelle ist gleich am Schleifenanfang oder zumindest noch "nah genug", wird erst ab dem zweiten Schleifendurchlauf (d.h. wenn der Sprung am Schleifenende kam) alles richtig gemacht.

Das nervt dann natürlich. FALLS man also nicht sowieso irgendwo nach der Stelle, die in den Code schreibt, noch einen Sprung hat, bevor die "beschriebene" Stelle ausgeführt werden würde oder die "beschriebene" Stelle noch weit genug weg ist, kann man sich behelfen, indem man einen unbedingten Short Jmp zum nächsten Befehl macht. Prinzipiell macht der quasi "gar nix" - hihi, außer dem Prefetch-Speicher zu löschen, denn das funktioniert auch bei Sprungweite 0, obwohl es da ja nicht nötig wäre. So gesehen ist es ein

Code: Alles auswählen

jmp @beispiel; @beispiel: 
Ich hab aber nicht jedes Mal Lust, extra ein Label zu definieren und will auch nicht prüfen, ob es ein Short Jmp wird (weil ein near jump hier nicht nötig ist). Deshalb mach ich einfach:

Code: Alles auswählen

dw $00EB;
(es reicht auch $EB, die 00 ergibt sich automatisch, weil man mit dw ja WORD deklariert).
Das setzt $EB,$00 in den Speicher, also ein Short Jmp zum nächsten Befehl. Klar. Verbraucht Zeit, weil Prefetch löschen usw. - aber hier ist es ja mit Absicht.

Das nur so als Tip.

Achja, zu Deiner Frage, wie man einen DWord-Parameter definiert.

Code: Alles auswählen

add ES:dword ptr[BX],$12345678;
geht ja unter Pascal nicht. Also macht man:

Code: Alles auswählen

db $66;add ES:word ptr[BX],$5678;dw $1234;
Klar?
zatzen hat geschrieben:Zu ISM ein Gedanke: Wenn ich es richtig verstehe definierst du je Note mit 4 Byte
wo genau diese zustehen hat und alle anderen Parameter.
Nein. Die Befehle sind direkt aufeinanderfolgend und bestehen pro Befehl aus 2 Byte (nicht 4).
Die Befehle sind normalerweise geteilt. Die oberen 7 Bit sind (meist) der Befehl, die unteren 9 sind der Parameter.
Es gibt aber auch Befehle, wo nur 8 Bit Parameter sind, dann ist das ein "zweigeteilter" Befehl, es sind dann also
quasi 2 in der Befehlstabelle aufeinanderfolgende Befehle, die beide 8 Bit Parameter haben.
(Ja, es gibt dann noch weitere mögliche Teilungen. Das ist alles im Data Sheet _ISM.TXT erklärt.)

Ein "Noten"Befehl besteht z.B. aus 7 Bit für die Note und 9 Bit für die (relative) Länge.
Die 7 Bit gehen dabei von $00 bis $5F, also sind 96 Noten möglich, 8 Oktaven à 12 Halbtöne.
(Durch Transponieren der oberen Noten können aber insgesamt mehr als 10 Oktaven erreicht werden, wenn man will.)
DIe Länge geht dann von $000 bis $1FF (also 0 bis 511), wobei 128 = 1 Sekunde entspricht (bei Speedfaktor=1).
D.h. die kleinste Einheit ist bei mit eine 1/128 Note.
(Höhere Speedfaktoren verlangsamen die Geschwindigkeit. D.h. bei einem Speedfaktor von 20 und einem Notenwert von 64 wären es z.B. 10 Sekunden.)
Ich definiere NICHT, wo die Note zu stehen hat. Das ergibt sich von allein durch die Reihenfolge der Notenbefehle.
Ein Notenbefehl enthält nichts außer dem Ton und seine Länge. Alles andere wird durch andere Befehle definiert und gilt so lange, bis man es durch diese anderen Befehle wieder ändert.
Beispiele sind die Hüllkurve, die Wellenform (oder Sample) und die Lautstärke. Ebenfalls sind es spezielle Befehle, die das Portamento bewirken (ich nenne es dort Bindung/Bending).
Nur Befehle, deren Parameter als "Längen" gelesen werden, haben einen "Zeitraum". Also Noten, Mute (Stille), Portamento und "Release" (Ausklingen) Befehle. Alle anderen Befehle haben eine "Abspiel-Länge" von 0 und tun NICHTS mit dem Soundpuffer (sondern ändern nur Werte in der Registertabelle/matrix der jeweiligen Stimme).
zatzen hat geschrieben:Die Darstellung könnte nun ja so ähnlich ausfallen, als wenn du sagst, okay, ich will das jetzt als Noten-
blatt haben zum Ausdruck, damit das jemand auf dem Klavier spielt.
Das wäre bei meinem Format ohne weiteres möglich. Die Längenparameter bestimmen die "Notenform" (also ob der Kopf leer oder voll ist und ob die Note Punkte bekommt oder einen oder mehrere Notenschwänze.... usw.)
zatzen hat geschrieben:Heisst also, in der Darstellung redundant bzw. mit Leerräumen, der besseren
Übersicht wegen, aber trotzdem editierbar und intern ohne Verschwendung gespeichert.
Hast du also meinetwegen bei eine Note die angezeigt wird
eine Wiederholung definiert, so könntest du diese Note irgendwie einfärben
und die Duplikate ebenfalls, so dass man sie als zugehörig erkennt.
Oder nicht einfärben sondern eine kleine Nummer dran, wie auch immer.
Aber das Tracker-Format als Darstellung wäre wohl noch das naheliegendste.
Ja, wie gesagt. Es wäre an sich zwar kein Problem, es im Tracker-Format auszugeben oder zu editieren - aber die ganzen Spezialbefehle könnte man so nicht einsetzen, da diese, weil sie eben keine "Abspiellänge" haben, dann irgendwie ja alle auf die gleiche Zeile zur aktuellen Note müßten oder so. Und die ganzen Sprungbefehle, Schleifenbefehle und Sonderbefehle zur "Berechnung" könnte man auch nicht wirklich sinnvoll im Trackerformat unterbringen.
Dadurch würde man viele der nützlichen Funktionen von ISM gar nicht verwenden können.
Prinzipiell kann man in ISM auch 10 Befehle schreiben, die irgend etwas definieren/ändern, bevor überhaupt die nächste Note gespielt wird. Wo sollten diese im Tracker stehen?
zatzen hat geschrieben:Nach wie vor wäre dieses Textfile sehr von Nutzen...
Oder habe ich es schon? OPCODES.TXT/BYTECODES.TXT?
Auf meiner Webseite zu finden unter MS-DOS / Coding auf Seite 2
"80486 Opcode & Bytes" und
"ASM86FAQ.TXT" (da ist auch gleich ein Link zum FAQREAD, was das Lesen/Suchen in diesem 1MB Textfile vereinfacht)
Oder kurz als Link: http://www.imperial-games.de/html/dosd2.htm
DOSferatu
DOS-Übermensch
Beiträge: 1220
Registriert: Di 25. Sep 2007, 12:05
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von DOSferatu »

zatzen hat geschrieben:X-Tracker spielt das Stück noch flüssig bei ca. 4300 Cycles, mein Player braucht 5000...
Vergrößerung des Soundpuffers (bei Reduzierung der Anzahl Aufrufe der Pufferfüllroutine) würde auch Beschleunigen.
Wieso? Weil die benötigte Zeit
Anzahl_Schleifendurchläufe * Dauer_eine_Schleife + Init_Exit_Zeit
ist.
Init_Exit_Zeit ist ja immer gleich (die Zeit für den Aufruf, Rücksprung, bzw das Initialisieren der Werte vor der Schleife.) Also wird die durchschnittliche Performance quasi durch Erhöhung der Anzahl_Schleifendurchläufe verbessert, weil die Init_Exit_Zeit dann prozentual weniger Anteil an der Gesamtausführzeit hat.

Wenn man nicht 43x pro Sekunde, sondern nur 22,5x pro Sekunde die Ausgaben refreshen würde, würde es wohl auch ausreichen, dafür mit doppelter Puffergröße...

Nur so eine Idee.
DOSferatu
DOS-Übermensch
Beiträge: 1220
Registriert: Di 25. Sep 2007, 12:05
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von DOSferatu »

[quote="zatzen"]
Ich würde statt "mov al, fs:[si+bx]; cbw" gern "movsx ax, fs:[si+bx]" verwenden, [/quote}
db §0F,$BE,$00

$0F,$BE ist der Opcode für MOVSX
und die $00 ist ein MOD-REG-R/M Byte und setzt sich zusammen aus: %00 %000 %000
%00 = Indiziert laden BYTE (word wäre z,B. 01)
%000 = AL, AX oder EAX (je nachdem ob byte, word oder dword geladen wird)
%000 = [bx+si] (andere Kombinationen haben andere Werte)

Diese MOD-REG-R/M Bytes kann man z.B. auch in den Files nachlesen, die Du von dieser Website runterladen kannst.

Hier noch eine Kopie dieses einen Files, was ich noch nicht auf die Seite gestellt hatte:

Code: Alles auswählen

MM RRR MMM

MM  - Memory addressing mode
RRR - Register operand address
MMM - Memory operand address

RRR Register Names
Filds  8bit  16bit  32bit
000    AL     AX     EAX
001    CL     CX     ECX
010    DL     DX     EDX
011    BL     BX     EBX
100    AH     SP     ESP
101    CH     BP     EBP
110    DH     SI     ESI
111    BH     DI     EDI

---

16bit memory (No 32 bit memory address prefix)
MMM   Default MM Field
Field Sreg     00        01          10             11=MMM is reg
000   DS       [BX+SI]   [BX+SI+o8]  [BX+SI+o16]
001   DS       [BX+DI]   [BX+DI+o8]  [BX+DI+o16]
010   SS       [BP+SI]   [BP+SI+o8]  [BP+SI+o16]
011   SS       [BP+DI]   [BP+DI+o8]  [BP+DI+o16]
100   DS       [SI]      [SI+o8]     [SI+o16]
101   DS       [DI]      [DI+o8]     [SI+o16]
110   SS       [o16]     [BP+o8]     [BP+o16]
111   DS       [BX]      [BX+o8]     [BX+o16]
Note: MMM=110,MM=0 Default Sreg is DS !!!!

32bit memory (Has 67h 32 bit memory address prefix)
MMM   Default MM Field
Field Sreg     00        01          10             11=MMM is reg
000   DS       [EAX]     [EAX+o8]    [EAX+o32]
001   DS       [ECX]     [ECX+o8]    [ECX+o32]
010   DS       [EDX]     [EDX+o8]    [EDX+o32]
011   DS       [EBX]     [EBX+o8]    [EBX+o32]
100   SIB      [SIB]     [SIB+o8]    [SIB+o32]
101   SS       [o32]     [EBP+o8]    [EBP+o32] Note: MMM=101,MM=0 Default Sreg is DS !!!!
110   DS       [ESI]     [ESI+o8]    [ESI+o32]
111   DS       [EDI]     [EDI+o8]    [EDI+o32]

---
SIB is (Scale/Base/Index)  SS BBB III   Note: SIB address calculated as:
                                        <sib address>=<Base>+<Index>*(2^(Scale))
Fild   Default Base
BBB    Sreg    Register   Note
000    DS      EAX
001    DS      ECX
010    DS      EDX
011    DS      EBX
100    SS      ESP
101    DS      o32        if MM=00 (Postbyte)
       SS      EBP        if MM<>00 (Postbyte)
110    SS      ESI
111    DS      EDI

Fild  Index
III   register   Note
000   EAX
001   ECX
010   EDX
011   EBX
100              never Index SS can be 00
101   EBP
110   ESI
111   EDI

Fild Scale coefficient
SS   =2^(SS)
00   1
01   2
10   4
11   8
Das ist eine recht einfache Erklärung der MOD-REG-R/M Bytes (und der S-I-B Bytes).
Falls Fragen, einfach fragen. Ich habe es einfach mal so reinkopiert.
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitrag von zatzen »

Ja, der Puffer ist ja beliebig einstellbar. Ich wollte hier erstmal eine Anzeige die sehr schnell updatet, aber
ich kann es auch direkt mal testen mit der doppelten Puffergröße, das würde mir verraten wieviel die Routinen
um die Kernschleifen herum noch ausmachen. Diese Mischroutinen habe ich aber jetzt auch schon als reine
Assembler-Routinen, ohne Stackframe, wenn ich das richtig verstehe, ich habe nicht mehr
"mix_in_channel(length: word); assembler" sondern nur noch "mix in channel; assembler" und hole
die Länge aus einer globalen Variable. Neben den Clipping-und-in-8-Bit-überführ-Routinen (und das
Werte-holen für die Lautstärkeanzeige) ist das meiste in Pascal geschrieben, je nachdem wie die
Performance bei größerer Puffergröße aussieht werde ich da noch was dran verbessern.

Wahrscheinlich ist mittlerweile wohl die höchste Performance aus den innneren Schleifen rausgeholt.
Aber das habe ich ein paar Posts vorher fast auch gedacht. Und dann das ganze nochmal um 20%
schneller hinbekommen...

Selbstmodifizierender Code ist für mich erstmal noch eher utopisch, aber gut zu wissen woran
es liegt dass JMPs langsam sind und was sie tun.

Danke für die TXTs!
mov ax, 13h
int 10h

while vorne_frei do vor;
Antworten