Trackermodul-Engine (sehr einfach)

Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 462
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Fr 6. Mär 2020, 18:06

Ich habe auch mal in der Pascal-Hilfe nachgesehen, da steht durch die Anweisung interrupt bzw. bei Interrupt-Routinen werden Register als Pseudo-Parameter übergeben, also möglicherweise bei Assembler-Prozeduren auch in Form von Locals bzw. Parameter, so dass BP und SP verändert werden und BP auf dem Stack liegt. Die "interrupt" Anweisung einfach weglassen geht wohl nicht, da viele Register durch die Player-Routine verändert werden. Wenn man nur ein Warteflag ändert in der Interrupt-Routine müsste man es ohne Register-Sichern machen können, jedenfalls habe ich das so früher schonmal gemacht in einer kleinen Demo in 100% Assembler. Naja gut, oder ich mache das eben alles "händisch", lasse interrupt weg und schreibe stattdessen pushf und pusha rein und unten die entsprechenden pops, oder vielleicht statt pusha explizit nur die Register die tatsächlich im Code verändert werden. pusha sichert nicht ES oder DS, daher wäre evtl. ein einzelnes Sichern besser. Ich denke ich werde mir auch mal das Kompilat dieser Interrupt-Routine ansehen...
Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 462
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Fr 6. Mär 2020, 20:12

Okay, disassembliert, also, AX-DX, SI, DI, DS und ES werden gepusht, außerdem BP und mit SP zusammen ein Stackframe angelegt der später mit LEAVE aufgelöst wird, und danach werden auch die Register wieder vom Stack geholt. Danach folgt ein IRET. Also ich denke mal, das muss nicht unbedingt sein, dann lieber per Hand benutzte Register sichern. Ich habe nur in Pascal bisher noch nie Interrupt-Routinen ohne die Deklaration "Interrupt" gemacht. Kennt eigentlich jemand einen guten Disassembler? Ich habe jetzt für diese Codesegment https://defuse.ca/online-x86-assembler.htm benutzt, aber der interpretiert einiges nicht richtig, und ist generell auf 32 Bit minimum ausgelegt.
Naja, also zu obigem Vorhaben - ich verändere im Player alle oben aufgeführten Register, auch BP, nur SP nicht. Ich kann mir also nur den Stackframe sparen. SP müsste ich mir sparen können, keine der Unterroutinen haben einen Stackframe. BP allerdings auch, das wird innerhalb der Playerroutine gesichert. Probleme könnte es geben, wenn DS außerhalb verändert wird und dann die Interrupt-Routine plötzlich dazwischen kommt, dann sind die Datenzugriffe fehlerhaft. Demnach müsste man darauf achten dass DS niemals verändert wird... Oder man macht, was generell auch eigentlich vernünftiger wäre: In der Interrupt Routine nur den nächsten Tick flaggen, und alles andere in der Haupschleife erledigen. Alternativ könnte man zur Sicherheit den Ausgangswert von DS in einer Variable speichern und in der Interrupt-Routine dann zurück in DS schreiben. Aber nein - drüber nachgedacht, das geht ja gar nicht. Wie will ich auf eine Variable zugreifen wenn DS nicht mehr stimmt?! Dann bleibt wohl nur, dass man DS nicht mehr verändern darf sobald die Interrupt-Routine aktiviert ist.
DOSferatu
DOS-Übermensch
Beiträge: 1182
Registriert: Di 25. Sep 2007, 12:05
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon DOSferatu » Mo 9. Mär 2020, 20:49

Ich antworte später ausführlicher - aber ich bringe mal Licht in's Dunkel.
Es gibt einige Arten (5!) von RET - mit verschiedenen Opcodes.

1.)
einfacher (near) RET: (Opcode $C3)
Holt NUR IP (ein Word) vom Stack zurück. CS bleibt erhalten.

2.)
spezieller near RET: (Opcode $C2)
Holt IP (ein Word) vom Stack zurück. CS bleibt erhalten.
Danac holt es ein Word vom Stack. Dieses Word gibt an, daß soviele Bytes im Stack "übersprungen" werden sollen (bzw zurückgesprungen - wobei das beim Stack ja "nach oben" geht).
Das dient dazu, die auf dem Stack angelegten Parameter/Variablen etc. für die Subroutinen (von Hochsprachen) wieder freizugeben.

3.)
far RET, also RETF: (Opcode $CB)
Holt (in diesere Reihenfolge) IP und CS vom Stack (2 Words). (ist also das Gegenstück zu einem FAR CALL)

4.)
spezieller far RET (also RETF) (Opcode $CA)
(vor allem für Hochsprachen, die Stack-Parameter-Übergaben haben) : (Opcode
Holt (in diesere Reihenfolge) IP und CS vom Stack (2 Words), danach wieder so ein "Anzahl-Word", das wieder angibt, wieviele Bytes übersprungen werden sollen.

5.)
Interrupt-Return, also IRET (Opcode $CF)
Macht fast das Gleiche wie der far RET bei 3.), nur holt er danach noch FLAGS vom Stack.
Also holt er (in dieser Reihenfolge) vom Stack: IP, CS, FLAGS.

Um also eine Interrupt-Routine (wie z.B. die Original ISR vom Ticker oder Keyboard) so aufzurufen wie eine Subroutine, braucht man nur das tun:
PUSHF
CALL FAR (Adresse der ISR)

Weil die sich ja mit einem IRET beendet, erwartet die, daß da vor der Rücksprungadresse noch was liegt. Und wenn sie schon FLAGS erwartet, sollte man das auch tun.

Erklärung Interrupt: Wie man sieht, sichert eine ISR nichts weiter als FLAGS und die (FAR-)Rücksprungadresse.
(Man muß also FLAGS nicht pushen, weil das der Interruptaufruf selbst tut. Er sperrt dabei auch gleich das I-Flag.)
Alle anderen Register, die man benutzen will, muß man selbst sichern. Weil aber eine ISR kaum anders funktioniert wie eine normale FAR-Routine, braucht man auch nur die Register sichern, die man verändern will.

Noch etwas: Wenn man eine Subroutine benutzt und entweder Parameter übergibt oder lokale Variablen benutzt (oder beides), sollte man nicht mit BP herumspielen, wenn man sich nicht sicher ist, was man tut. Jeder Zugriff auf eine lokale Variable (auch von ASM aus) wird umgewandelt in einen Zugriff auf SS:BP+Offset (wobei Offset eben dem Offset entspricht vom Aufruf.

Beim Aufruf einer Rotuine weiß man ja nicht, wo der Stackzeiger gerade ist, daher wird dieser nach BP gesichert, damit der Zugriff auf lokale Variablen oder übergebene Parameter relativ zum Stackzeiger sind. Wenn man also BP ändert und NICHT sichert/zurückändert und danach auf eine lokale Variable (übergebene Parameter werden eigentlich auch nur wie lokale Variablen behandelt) zugreift, geht das in die Hose.

Die "interrupt"-Anweisung hinter dem Prozedurkopf macht das was Du geschrieben hast: die Handvoll (16bit!) Register sichern, inkl. DS und ES und am Ende zurückholen -sowie ein Stackframe einrichten. Auf 8086 "manuell", ab 286er mit ENTER und LEAVE, wobei je nach Aufrufart manchmal trotzdem die manuelle Variante benutzt wird - hatte das schonmal erklärt. Das Ganze ist also nur etwas, um es Hochsprachen-Codern leichter zu machen - wenn man ASM kann, kann man das auch alles selbst.

Wichtig (daher das 16bit! da oben): Weil Turbo-Pascal sich der Existenz von 32bit-Systemen nicht bewußt ist, sichert diese obengenannte Automatik nur den 16bit-Teil von 32-Bit-Registern. Wenn man also z.B. in der ISR die oberen 16bit von EAX verändert, werden diese nicht zurückholt - außer man sichert es selbst.

So, genug geschwafelt. Hoffe, es hilft.
Sollte es dazu noch Fragen geben - bin ich wie immer bereit zu helfen.
(Freue mich ja so dermaßen, daß es "da draußen" noch andere Leute gibt, die programmieren - und auch noch hardwarenah und unter DOS. Das muß unterstützt werden.)
Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 462
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Mo 9. Mär 2020, 22:10

Vielen Dank für diese Erläuterungen!

Es scheint mir, dass Pascal, auch wenn es dann ASM Routinen sind, automatisch je nach Situation die RETs in passende Opcodes umwandelt, denn ich habe überall nur RET geschrieben und nirgends RETF. Oder habe ich da nur Glück gehabt?

Alle Prozeduren die vom Player genutzt werden (bis auf die Initialisierungen) sind rein in Assembler gehalten und ohne Locals und Parameter. Daher habe ich BP für eigene Zwecke verwendet. Aber gut, dass Du nochmal darauf hinweist, wann genau man da aufpassen muss.

Und ja, eine Frage habe ich noch, nämlich mit dem Register DS. Ist das nicht eigentlich ein Dilemma im Zusammenhang mit Interrupt-Routinen? Man biegt DS ja gerne mal um z.B. für den Heap, aber wenn in der Zeit wo DS dann nicht mehr aufs Datensegment zeigt eine Interrupt Routine dazwischenfunkt die auf Variablen zugreifen will, dann haben wir doch ein Problem. Mir fällt dazu spontan nur ein, dass man Interrupts sperren muss (CLI) für die Zeit in der DS nicht aufs Datensegment zeigt.
DOSferatu
DOS-Übermensch
Beiträge: 1182
Registriert: Di 25. Sep 2007, 12:05
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon DOSferatu » Di 10. Mär 2020, 22:45

zatzen hat geschrieben:Vielen Dank für diese Erläuterungen!

Es scheint mir, dass Pascal, auch wenn es dann ASM Routinen sind, automatisch je nach Situation die RETs in passende Opcodes umwandelt, denn ich habe überall nur RET geschrieben und nirgends RETF. Oder habe ich da nur Glück gehabt?

Ja, Glück gehabt. Ich verlasse mich nicht auf sowas. Ich benutze immer RETN, wenn ich einen NEAR-Return haben will oder RETF, wenn ich einen FAR-Return will. In ASM kann man teilweise etwas "kreativ" zu Werke gehen und nicht immer ist dem Compiler klar, was man da angestellt hat, wenn man mit dem Stack herumspielt.

INNERHALB des gleichen Assemblerblocks, sind RET natürlich normalerweise NEAR. Da sollten die CALLs dann natürlich auch NEAR sein.

zatzen hat geschrieben:Alle Prozeduren die vom Player genutzt werden (bis auf die Initialisierungen) sind rein in Assembler gehalten und ohne Locals und Parameter. Daher habe ich BP für eigene Zwecke verwendet. Aber gut, dass Du nochmal darauf hinweist, wann genau man da aufpassen muss.

Kleiner Hinweis dazu: BP ist, genau wie die anderen 7 Register, ab 386er ja 32bit. Falls man von Registern nur die unteren 16bit benutzen will und den Rest sichern, kann man z.B. das tun:

Code: Alles auswählen

db $66;rol BP,16;

Das tauscht quasi die oberen und die unteren 16 Bit und somit kann man quasi beides haben - sowohl den ursprünglichen Wert als auch einen anderen. Außerdem ist es schneller als PUSH/POP.
Wenn man so ein Register übrigens sichern will und das neue soll auf 0 initialisiert werden, bietet sich stattdessen SHL an:

Code: Alles auswählen

db $66;shl BP,16;

So ist BP "nach oben" gesichert und gleichzeitig auf 0 gesetzt - mit EINEM Befehl.

zatzen hat geschrieben:Und ja, eine Frage habe ich noch, nämlich mit dem Register DS. Ist das nicht eigentlich ein Dilemma im Zusammenhang mit Interrupt-Routinen?

Ich kenne dieses Dilemma - habe dazu aber eine Lösung gefunden.

zatzen hat geschrieben:Man biegt DS ja gerne mal um z.B. für den Heap, aber wenn in der Zeit wo DS dann nicht mehr aufs Datensegment zeigt eine Interrupt Routine dazwischenfunkt die auf Variablen zugreifen will, dann haben wir doch ein Problem. Mir fällt dazu spontan nur ein, dass man Interrupts sperren muss (CLI) für die Zeit in der DS nicht aufs Datensegment zeigt.

Nicht nötig.
Ja, in manchen meiner Subroutinen verändere ich DS absichtlich nicht, weil ich andauernd auch auf globale Variablen zugreifen muß und dann müßte ich das jedes Mal wiederherstellen. In Interrupts allerdings ist es schon störend, nichts "außerhalb des Interrupts" nutzen zu können - daher sichere ich DS auf etwas elegante Weise.
Wir sind uns ja einig: Alles, worüber man sich sicher sein kann, wenn ein INT aufgerufen wird, ist, daß CS definiert ist, denn CS:IP werden ja schließlich automatisch geholt, um den INT auszuführen. Achja, und man weiß, daß FLAGS gesichert ist.

Also - dann benutzen wir doch CS. Nämlich, indem wir einfach IM CODE das DS sichern! Ich habe da mal so ein kleines Testprogramm geschrieben. Der Unfug, den es anstellt, dient nur dazu, zu zeigen, daß Interrupt und Hauptprogramm gleichzeitig laufen ohne sich gegenseitig zu behindern - im Hauptprogramm wird zwischendurch DS geändert...

Ich habe es getestet - es funktioniert.
(Weil ich es als "allgmeines Anschauungsmaterial" gemacht habe, habe ich da auch in Englisch so Kommentare reingetan.)

Code: Alles auswählen

var INT8PTR:Pointer absolute 0:32;
var OLDINT8:Pointer;
var TOPLAY:word;
var TIME:longint;
var TEXT:string;
var INDEX:byte;
{----------------------------------------------------------------------------}
procedure INTER; assembler;
asm
push DS
call near ptr @saveDS
dw CS:offset @saveDS
{here starts your code for interrupt - some silly example..........}
push ES
push AX
push SI
push DI
push BX
mov AX,$B800
mov ES,AX
lea SI,DS:TEXT
xor DI,DI
mov BX,DI
mov BL,INDEX
@loop1:
cmp BL,DS:[SI]
jbe @NoChange
mov BL,1
@NoChange:
mov AL,DS:[SI+BX]
mov ES:[DI],AL
inc DI
mov AX,DI
inc AX
shr AX,1
add ES:[DI],AL
and ES:byte ptr[DI],$F
inc BL
inc DI
cmp DI,160
jb @loop1
inc INDEX
mov AL,INDEX
cmp AL,DS:[SI]
jbe @NoWrap
mov INDEX,1
@NoWrap:
pop BX
pop DI
pop SI
pop AX
pop ES
{here ends your code for interrupt.................................}
  {here an example to call the old int 8}
   pushF {because the INT8 ISR ends with an IRET. Note that NOW I-Flag is clear!}
   call OLDINT8
  {end of the interrupt routine}
pop DS
push AX
mov AL,$20
out $20,AL
pop AX
IRET{here your interrupt ends}
{--the following is only executed once - and saves DS--}
@saveDS:
pop BX  {catch the address saved by the call above into BX}
mov CS:byte ptr[BX-3],$2E {write a "CS:" prefix where first CALL was}
mov CS:word ptr[BX-2],$1E8E {write a "MOV DS,@saveDS," after the CALL}
pop CS:word ptr @saveDS {pop DS into the place where "pop BX" stands}
retF {leave normally, because its called from main program}
end;
{----------------------------------------------------------------------------}
{MAIN PROGRAM}
begin
INTER;{1x, to save DS}

asm mov AX,3;int $10;end;{set 80x25, clear screen}
writeln;
writeln('upper line: done in int8, lower line: copied in main program, changing DS!');

INDEX:=1;
TEXT:='This is a Test * (C) Imperial Games 2020 * See how to save DS for interrupt with modified code * ';

{change INT 8 to your INTER routine}
asm CLI;end;
OLDINT8:=INT8PTR;
INT8PTR:=@INTER;
asm STI;end;

{a test loop to "do something while the interrupt runs"}
repeat
write(#13'Change DS to copy lines,');
asm
push DS
mov AX,$B800
mov DS,AX
add AX,30
mov ES,AX
xor SI,SI
xor DI,DI
mov CX,80
cld
rep movsw
pop DS
end;
write('...but INT uses original DS!');

TIME:=memL[64:108]*1080 div 19663;{get ticker and calculate into seconds}
write(' Time still works: ',TIME div 3600:2,':',TIME mod 3600 div 60:2,':',TIME mod 60:2);
until memW[64:26]<>memW[64:28];{wait until key pressed}

{get back your int 8}
asm CLI;end;
INT8PTR:=OLDINT8;
asm STI;end;

memW[64:26]:=memW[64:28];{clear keaboard buffer}
end.
{explanation:
after INTER; is called once from the main program (with DS set "normally")
the "nop;nop;call near ptr @saveDS" above is changed into:
"mov DS,CS:word ptr @saveDS"
and thus it gets the real DS everytime when an interrupt is called!}


Erklärung auf Deutsch, was das macht:
Damit einem im Interrupt nicht Zyklen durch sinnlose Sprünge verlorengehen habe ich die Routine "hybrid" gemacht. Bevor man den Interrupt aktiviert, muß man EINMALIG(!) die Routine wie eine normale Procedure aufrufen. In der Routine ist nach dem PUSH DS ein Sprung nach unten (HINTER das IRET). Dieser führt nur die Befehle aus, um den Code zu modifizieren, damit es danach im Interrupt etwas anderes macht.

Die Sequenz

Code: Alles auswählen

call near ptr @saveDS
dw CS:offset @saveDS

ist 5 Bytes lang. Der CALL ist 3 Bytes und der einzelnstehende WORD-Wert, der genau die Lage des Labels @saveDS angibt, ist 2 Bytes. Die Routine unten überschreibt den CALL mit $2F,$8E,$1E, das ist der Code für mov DS,CS:,,, - und das xxx sind die restlichen 2 Bytes. Cooler wäre zwar, DS direkt mit dem Wert zu laden - aber wie wir wissen, können Segmentregister nur entweder über ein normales Register oder über eine Speicherreferenz geladen werden. Unten (hinter dem IRET) das Ganze wird nur 1x benötigt. Deshalb überschreibt es praktischerweise gleich den POP BX Befehl mit dem Wert, den DS jetzt gerade hat. Das heißt, dieser Einmal-Aufruf der Routine sollte erfolgen, wenn DS gerade den RICHTiGEN Wert hat (also am besten gleich am Anfang des Programms, wenn noch nichts geändert wurde....).

Die INT-Routine hat durch die Modifikation dann keinen CALL (oder JUMP oder wasauchimmer) mehr drin, sondern hat als erste zwei Befehle quasi diese:

Code: Alles auswählen

push DS
mov DS,CS:word ptr @saveDS

Und das ist es ja, was man sowieso braucht: DS muß ja sowieso gesichert werden (weil man nicht weiß, ob es im Hauptprogramm gerade NICHT "original" ist (und das muß ja vom INT so wiederhergestellt werden!) und danach holt es das "originale" DS, was ganz zu Anfang durch diesen Einmal-Aufruf gesichert wurde quasi "aus seinem eigenen Code". Praktischerweise habe ich es HINTER das IRET getan - so muß nichts übersprungen werden oder so.

Den ganzen Schlunz in Pascal ringsherum habe ich nur gemacht, um zu zeigen, wie man das Ganze innerhalb von Pascal einbauen könnte.

Damit die Subroutine "irgendwas macht" habe ich diesen lustigen Scrolltext da hingemurkst - das ist nicht gerade schick gecodet, soll quasi nur zeigen, daß alles, was man da innerhalb des INTs macht, geht. Und: Man muß nur die Register sichern, die man wirklich benutzt.

Man KANN danach am Ende die Original-Routine (im Falle von INT8 also den Ticker) aufrufen, einfach mit einem CALL (und einem PUSHF davor! nicht vergessen!). Das IRET des normalen INT8 würde zwar INTs wieder am Ende freigeben - aber das PUSHF macht gleich 2 nützliche Dinge: Zum Einen sorgt es natürlich dafür, daß nicht abgestürzt wird - weil ja ein IRET IP,CS und FLAGS holt. Zum Anderen ist bei FLAGS ja (weil es INNERHALB des INT ist) zu der Zeit das INT-Flag gesperrt, d.h. das "zurückholen" von FLAGS von der Originalroutine wird trotzdem keine INTs freigeben (die vielleicht am Ende in "unseren neuen" reingrätschen können. Die Freigabe macht erst unser eigener INT mit seinem eigenen IRET.

Das POP DS am Ende ist wichtig - schließlich haben wir es zu Anfang gePUSHt.
Außerdem muß man noch (vor allem, falls man NICHT die Original-Routine aufruft!), dieses OUT $20,$20 machen (also über AL), damit setzt man den Interrupt-Controller zurück. (Wenn man das NICHT macht, kommen keine weiteren INTs!)
Die Original-Routinen haben natürlich selbst am Ende diese OUT-Sachen drin (sonst würden sie ja nicht funktionieren).

Achja, wieso da am Anfang ein CALL und kein JMP? Naja, JMP wäre auch gegangen, aber 1. weiß man nicht (weil es von der Länge des Codes im INT abhängt) ob der Compiler/Assembler einen SHORT oder NEAR JUMP macht. (Ein Short JUMP hat nur 2 Bytes, nicht drei. Dann muß man noch ein NOP davor machen, damit genug Platz für den neuen (MOV DS...) Befehl ist. Außerdem habe ich durch den CALL (den ich NICHT "returniere!") nämlich auf dem Stack jetzt die Offset-Adresse, die genau hinter dem Call ist (also die "Rücksprung-Adresse"). Die lade ich in BX und greife dann mit BX auf diese Stelle da oben zu, um den Code zu ändern. Natürlich kann man das auch einfach mit Labels machen und dann LABEL und LABEL+1 ... und damit den Befehl ändern. Aber man kann es ja auch mal auf die coole Art machen. Dann wird der Befehl geändert UND natürlich ist ein zweites POP da unten und das POPt den Inhalt von DS gleich in die Stelle hinter @saveDS - damit es von dort später geholt werden kann. Man kann ja nicht nur in Register POPen, sondern auch direkt in Speicher.

Daß ein CALL ein paar Zyklen mehr braucht als ein JMP, macht hier das Kraut nicht fett. Erstens wird der ja insgesamt nur EINMAL ausgeführt (danach wird er ja überschrieben) und zweitens wird das wohl unten wieder eingespart durch die Zugriffe per BX.

Ich hoffe, ich konnte das einigermaßen erklären. Es ist zwar modifizierter Code, aber ich finde, das ist eine einfache und sehr elegante Lösung, um das Ursprungs-DS zu retten, um es im INT benutzen zu können.

Wie immer: Falls noch Fragen auftreten, gerne fragen.
Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 462
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Di 10. Mär 2020, 23:40

Danke nochmals für die schnelle Antwort!

Was mich bzgl. RET's etwas verwirrt ist, dass im Assembler86FAQ nur RET aufgeführt ist, und dass es dort so beschrieben ist als sei durch die Art des CALLs bedingt ob nur IP oder auch CS zurückgeholt werden. D.h. demnach müsste die CPU durch den CALL "wissen", wie sie den Return zu interpretieren hat.

Es freut mich, dass es eine Lösung zu dem DS Problem gibt. Grob kann ich es schon nachvollziehen, aber ich möchte es erst selber verwenden wenn ich es so verstanden habe dass ich es ohne nachzusehen selber coden kann. Bis dahin vermeide ich DS-Änderungen, womit man ja meistens sowieso am besten fährt, zumal man stattdessen auch FS und GS benutzen kann, wenn auch etwas kryptisch innerhalb Pascal. Noch eine Frage wäre noch, ob Pascal selbst den Hochsprachencode so kompiliert, dass eine zeitweise Änderung von DS vorkommt.

Übrigens habe ich Bedenken, ob ich bei den ZV2 Routinen mit den Registern hinkomme, d.h. mit SI, DI, BX und BP. Kombinieren kann man ja nur immer SI/DI mit BX/BP, wenn ich das richtig erfahren habe.
DOSferatu
DOS-Übermensch
Beiträge: 1182
Registriert: Di 25. Sep 2007, 12:05
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon DOSferatu » Mi 11. Mär 2020, 20:50

zatzen hat geschrieben:Danke nochmals für die schnelle Antwort!

Was mich bzgl. RET's etwas verwirrt ist, dass im Assembler86FAQ nur RET aufgeführt ist, und dass es dort so beschrieben ist als sei durch die Art des CALLs bedingt ob nur IP oder auch CS zurückgeholt werden. D.h. demnach müsste die CPU durch den CALL "wissen", wie sie den Return zu interpretieren hat.

Um Himmels Willen! - NEIN!
Auf GAR KEINEN FALL "wissen" die CPU-Befehle irgend etwas voneinander. Jeder Befehl steht für sich allein. Ein RET macht einfach nur das, was der Opcode ihm vorgibt. Man kann ein RET auch ausführen, ohne einen CALL gemacht zu haben! Man kann einen Offset (oder eine Adresse) auch einfach manuell auf den Stack schmeißen und dann RET ausführen. (Den Trick benutze ich gern, wenn ich mal irgendwo zu einer FAR-Adresse springen will und die gerade irgendwo in einem Register rumliegen habe.)

Was möglich ist, ist, einen NEAR-RET zu einem FAR-CALL zu machen, aber nur, wenn es einer ist, der keine zusätzlichen Bytes vom Stack holen soll.
D.h. man könnte so eine Routine dann NEAR oder FAR aufrufen. NEAR-RET holt nur IP, FAR-RET holt IP und CS. Allerdings wirft ein FAR-CALL ja auch CS und IP (in dieser Reihenfolge) auf den Stack - d.h. wenn man so eine Routine dann mit NEAR-RET beenden würde, müßte man danach noch das zusätzliche Word vom Stack holen (add SP,2). Aber dieses ganze Gefrickel macht überhaupt keinen Sinn - man spart dadurch quasi nix, wenn man dafür dann außenrum so Herummurksen muß - das lohnt nur, wenn man irgendeinen kreativen Trick/Hack einsetzen will.

Also, nochmal: Die Befehle machen nur für sich das, was sie machen sollen. ein FAR CALL wirft CS und IP auf den Stack und wechselt dann zum neuen Segment und dort in den neuen Offset. Ein NEAR CALL wirft nur IP auf den Stack und wechselt dann innerhalb des gleichen Segments (das in CS steht) zum neuen Offset. Ein FAR RET holt IP und CS vom Stack und wechselt dann zum neuen Segment (CS) und dort zum Offset (IP). Ein NEAR RET holt IP vom Stack und wechselt innerhalb des aktuellen Segments zum Offset (IP). Die beiden zusätzlichen "normalen" RETs sind für Hochsprachen gedacht, weil ja am Anfang einer Subroutine Parameter über den Header übergeben werden oder auch im Kopf (zwischen Header und BEGIN) noch lokale Variablen angelegt werden... Diese ganzen Dinge landen zusätzlich auf dem Stack - und beim Beenden muß der Stackpointer ja wieder da stehen, wo er vor dem CALL war, deshalb diese RETs, die zusätzlich einen Parameter haben, der entsprechend viele Bytes noch "überspringt".

Und der IRET - naja, bei dem macht es ja Sinn, u.a. daß FLAGS zu speichern (vor allem, weil ein INT ja selbst ein Bit darin ändert - das INT-Erlauben-Bit). Und weil man im Interrupt ja nicht NICHTS machen will, würde sich ja sowieso mindestens irgend etwas in FLAGS ändern - und das Hauptprogramm darf ja davon nichts mitbekommen...

zatzen hat geschrieben:Es freut mich, dass es eine Lösung zu dem DS Problem gibt. Grob kann ich es schon nachvollziehen, aber ich möchte es erst selber verwenden wenn ich es so verstanden habe dass ich es ohne nachzusehen selber coden kann.

Naja, es ist etwas mehr Code als nötig gewesen wäre - das ganze "Drumherum" habe ich nur gemacht um zu demonstrieren, wie man es anwendet und daß es funktioniert. In Wirklichkeit ist der einzige Witz daran, daß man einfach nur direkt irgendwo im Codebereich des INT (also irgendwo, wo CS:xx ist) das DS vorher hinterlegt, damit es der INT finden kann. Man MUß das auch nicht so elegant wie ich machen. Aber weil ja CS das einzige Segmentregister ist, was zum Zeitpunkt des Aufrufs des INT einen sichergestellten Wert enthält, ist das meiner Meinung nach die einzige Möglichkeit, innerhalb eines INT an das DS zu kommen.

Eigentlich ist das nicht ganz richtig. Der Assembler von Pascal bietet zwei Pseudo-Konstanten an: @DATA und @CODE.
mit SEG @DATA kann man das Haupt-Datensegment angeben, mit SEG @CODE das Haupt-Codesegment. Allerdings kann man ja leider einem Segment keine Konstante zuweisen (dafür gibts keinen Opcode). Also geht das dann wieder nur über ein Register: mov AX,SEG @DATA; mov DS,AX;

Bin aber nicht sicher, ob das immer funktioniert - normalerweise sollte es aber.

zatzen hat geschrieben:Bis dahin vermeide ich DS-Änderungen, womit man ja meistens sowieso am besten fährt, zumal man stattdessen auch FS und GS benutzen kann, wenn auch etwas kryptisch innerhalb Pascal. Noch eine Frage wäre noch, ob Pascal selbst den Hochsprachencode so kompiliert, dass eine zeitweise Änderung von DS vorkommt.

Naja, normalerweise würde ich nein sagen - andererseits KENNT Pascal ja noch kein FS/GS. Diese in ASM gern als "String-Operationen" bezeichneten Dinge (REP MOVSW und ähnliches) brauchen ja DS und ES. (Man kann dabei DS auch mit etwas anderem "überschreiben - wenn man hat! - aber da bleiben ja nur CS und SS, weil, wie gesagt, FS und GS benutzt Pascal von sich aus nicht.) Also... ja, es könnte entweder sein, daß Pascal DS auch mal ändert ODER es ändert, aber nur in CLI/STI eingegrenzt. Kann ich leider nicht sagen - habe noch nicht alle Befehle von Pascal quasi manuell überprüft.

zatzen hat geschrieben:Übrigens habe ich Bedenken, ob ich bei den ZV2 Routinen mit den Registern hinkomme, d.h. mit SI, DI, BX und BP. Kombinieren kann man ja nur immer SI/DI mit BX/BP, wenn ich das richtig erfahren habe.

So ist es: Es gibt diese Kombinationen:
[BX], [SI], [DI], [BX+SI], [BX+DI], [BP+SI], [BP+DI]
dann noch alle +Bytewert, alle +Wordwert (hier auch [BP+Bytewert] und [BP+Wordwert]) - nur [BP] einzeln gibt es nicht, das wird immer intern umgewandelt in [BP+0]. (Grund dafür ist, daß an der "Stelle", wo das von der Abfolge her stehen würde, der einfache Wert (ohne Indexregister) steht, also [Wert] - sonst hätte man das nicht umsetzen können.)

ABER: Das Ganze gilt nur für 16bit. Wir haben ja auch die Möglichkeit der 32-Bit-Adressierung! Wenn man damit im 16bit-Mode auf Offsets >65535 zugreift, stürzt zwar der Rechner ab - aber da muß man eben drauf achten, daß das nicht passiert! Indem man die oberen 16bit der Register =0 setzt. Denn in der 32-Bit-Adressierung kann man ALLE Register (außer natürlich die Segmentregister) zur Adressierung benutzen, diese werden dann aber in ihrer 32-Bit-Entsprechung benutzt (also EAX statt AX).

Dazu folgt dem normalen Mod-R/M-Byte, das den normalen Befehlen folgt, wenn sie das $67-Präfix haben, zusätzlich noch ein S-I-B Byte (Scale-Index-Base).

Das heißt so, weil man bei einem der beiden Register sogar angeben kann, daß es mit 1, 2, 4 oder 8 multipliziert wird (wenn man beide auf das gleiche Register setzt, sind so also auch Indezes wie 3, 5 und 9 möglich).

Das S-I-B Byte hat also in seinen 8 Bit: 2Bit, die angeben, ob *1, *2, *4 oder *8 (bei einem der Register) und dann 3Bit für das eine (Base) Register (das ist das, was multipliziert werden kann) und 3Bit für das andere (Index) Register (das immer *1 ist). Da gibt's auch so "Sonderfall" der auch an der Stelle steht, wo normal EBP wäre... also so Fall wo ohne Register sondern nur mit Wert bzw in Kombination mit Wert...

Die Register sind bei x86 ja bekanntlich "numeriert" in dieser Reihenfolge:
AX, CX, DX, BX, SP, BP, SI, DI - so sind dann also auch die Bitmuster:
(000, 001, 010, 011, 100, 101, 110, 111)
Das gilt dann auch für die Exx-Register: gleiche Reihenfolge.

Nun kennt ja TurboPascal keine 32bit-Sachen, deshalb muß man, WENN man diese Geschichte nutzen will, das natürlich alles schick manuell basteln - aber ich habe Tabellen da, falls man's braucht.

ja, diese super-indizierte Methode ist natürlich recht schick - allerdings, wie gesagt: Da immer dran denken, daß die oberen Bits =0 sind, bzw. daß das ERGEBNIS, also der "Offset" von der Konstruktion dann ein 32-Bit-Wert wird, der auch im 16-Bit-Mode NICHT "wraparoundet"! Und wenn dieses Ergebnis einen Wert >$FFFF erreicht, dann wird so ein Fehler-INT ausgelöst (weiß grad nicht auswendig, welcher - steht sicher irgendwo, z.B. in der ASM86FAQ) - was im Klartext bedeutet: Wenn man den nicht abgefangen hat (was man sowieso kaum tut): Absturz. Und WENN man ihn abfängt ... naja, dann hat man kaum Performance gewonnen durch die Konstruktion, weil wenn da jedesmal eine Fehler-INT-Bereinigungs-ISR laufen soll...

Wollte nur der Vollständigkeit halber drauf hinweisen, daß Dir im 32bit-Mode (bzw mit der 32-Bit-Address-Option) auch die anderen Register zur Indizierung zur Verfügung stehen.
Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 462
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Sa 14. Mär 2020, 18:12

DOSferatu hat geschrieben:Eigentlich ist das nicht ganz richtig. Der Assembler von Pascal bietet zwei Pseudo-Konstanten an: @DATA und @CODE.
mit SEG @DATA kann man das Haupt-Datensegment angeben, mit SEG @CODE das Haupt-Codesegment. Allerdings kann man ja leider einem Segment keine Konstante zuweisen (dafür gibts keinen Opcode). Also geht das dann wieder nur über ein Register: mov AX,SEG @DATA; mov DS,AX;

Bin aber nicht sicher, ob das immer funktioniert - normalerweise sollte es aber.

Selbstdefinierte Konstanten liegen ja auch im Datensegment, mit denen kann es also nicht funktionieren. Auch im Hauptprogramm DS im Codesegment zu hinterlegen und dann mit "mov ax, cs:[offset @dsbackup]" darauf zuzugreifen wäre wahrscheinlich unsicher, da sich CS ändern kann. Aber wenn man diese Konstanten @DATA und @CODE als Compilerkonstanten betrachten könnte auf die referenziert werden kann unabhängig vom Registerinhalt von DS oder CS, dann könnte man daraus vielleicht wirklich eine simple Lösung zu dem Problem basteln, ohne modifizierten Code. Aber das lässt sich ja auch relativ einfach überprüfen.

DOSferatu hat geschrieben:ABER: Das Ganze gilt nur für 16bit. Wir haben ja auch die Möglichkeit der 32-Bit-Adressierung! Wenn man damit im 16bit-Mode auf Offsets >65535 zugreift, stürzt zwar der Rechner ab - aber da muß man eben drauf achten, daß das nicht passiert! Indem man die oberen 16bit der Register =0 setzt. Denn in der 32-Bit-Adressierung kann man ALLE Register (außer natürlich die Segmentregister) zur Adressierung benutzen, diese werden dann aber in ihrer 32-Bit-Entsprechung benutzt (also EAX statt AX).

Dann wohl am einfachsten "db 66h; xor ax, ax"? Klar, wenn ich den Wert behalten will wohl lieber "db 66h; ror ax, 16; xor ax, ax; ror ax, 16" - sind aber auf nem 486 direkt 5 Takte. Ach ich dumme Nuss - "db 66h; and ax, 0ffffh" geht ja auch.
Das sind natürlich tolle Möglichkeiten, gerade auch mit der Multiplikation, ich müsste mir nur wenn ich das "per Hand" bastle mir den Assembler-Code als Kommentar dazuschreiben. Ich brauche diese Übersichtlichkeit, deshalb rücke ich auch die Zeilen ein.

Danke für diesen Hinweis, vielleicht wird er bei ZVID2 sehr wichtig werden. Aber dazu muss dann auch erstmal die Sache mit dem Unchained Mode geklärt werden. Und irgendwie meine ich, dass die Idee mit dem nur teilweise wiederherstellen und nur veränderte Bereiche in den Grafikspeicher kopieren auch im Unchained Mode sinnvoll sein könnte, sogar mit Scrolling, wenn man das mittels vier Seiten macht. Aber alles noch Neuland...
Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 462
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Do 26. Mär 2020, 20:06

Hallo DOSferatu!
Ich habe das mit dem @data einmal ausprobiert mit folgendem Code:

Code: Alles auswählen

  var ds1, ds2: word;
begin
  asm
    mov ds1, ds
    push ds
    xor ax, ax
    mov ds, ax
    mov ax, seg @data
    pop ds
    mov ds2, ax
  end;
  writeln(ds1, ' ', ds2);
end.

Ergebnis: Beide Variablen, ds1 und ds2, haben den gleichen Wert.
Das müsste eigentlich beweisen, dass "seg @data" zum einen auf das Datensegment zeigt bzw. DS entspricht, und zum anderen unabhängig von dem Register DS seinen Wert behält. Deine Code-modifizierende Interrupt Routine ist ein tolles Kunststück, aber ich würde das ganze dann einfach über "seg @data" lösen. Oder was meinst Du?
Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 462
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Fr 27. Mär 2020, 18:53

Okay, das Problem scheint gelöst. Hier nur die kleinere der beiden Interrupt-Routinen:

Code: Alles auswählen

procedure interrupt_flagtick; assembler;
asm
  push ax
  push ds
  mov ax, seg @data
  mov ds, ax
  mov btplay_next_tick, 1
  mov ax, tickervalue
  add tickerwrap, ax
  pop ds
  jc @call_oldint
    mov al, 020h
    out 020h, al
    pop ax
    iret
  @call_oldint:
    pop ax
    pushf
    call old_intvec
    iret
end;

So wie ich das nachgelesen habe beeinflusst ein POP die Flags nicht, daher müsste hier JC noch funktionieren.
DOSferatu
DOS-Übermensch
Beiträge: 1182
Registriert: Di 25. Sep 2007, 12:05
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon DOSferatu » Fr 8. Mai 2020, 20:36

zatzen hat geschrieben:Ich antworte hier schonmal...

DOSferatu hat geschrieben:[Portierung des Beni Tracker Players]Ja, habe das so nebenher mitbekommen. Wie weit bist Du?

Ich bin soweit fertig mit der Unit. Die eigentliche Abspielroutine habe ich erstmal nur 1:1 in Assembler umgeschrieben, aber dabei die Möglichkeiten von Assembler genutzt, Register öfters auf ihrem Wert belassen zu können, und auch Berechnungen beim Adressieren von Speicher stark zu vereinfachen oder zu vermeiden.

Ja, wie ich schon (sicher mehrmals) erwähnt habe: Wenn man erstmal mit Assembler angefangen hat, merkt man, daß es einen nicht einschränkt, sondern oft sogar das Gegenteil.

zatzen hat geschrieben:Datenfeldadressierungen in Pascal arten ja je nachdem im Kompilat in Code mit MUL-Anweisungen aus.

Wollte immer mal prüfen, ob das so ist - vermute es aber auch. Vielleicht gibt es wenigstens Optimierungen für eindimensionale Felder mit Datenfeldgrößen in 2er-Potenzen.

zatzen hat geschrieben:Ich dachte noch dran, die Abspielroutine ganz neu nach meinem eigenen Gutdünken zu schreiben, wenn ich die Vorgänge verstanden habe. Aber die Mühe schien mir vorerst zu groß. Aber so eine Unit kann man ja jederzeit updaten und trotzdem nach außen hin abwärtskompatibel halten. Immerhin ist das Kompilat durch die Umsetzung in Assembler schon um ein paar KB geschrumpft.

Naja, vor allem ist es nun portiert und dadurch in Pascal einsetzbar. Wie Dir ja schon selbst ausgefallen ist, hat das GW-BASIC, sowohl was Speicherverfügbarkeit als auch Ausführungsgeschwindigkeit anbetrifft, gewisse Grenzen - die Borlands TurboPascal zwar auch hat, dort aber wohl etwas höher liegen.

zatzen hat geschrieben:Was ich aber gemacht habe, und da kennst Du mich mittlerweile, ist dass ich die Patterndaten komprimiert im Speicher ablege. Diese werden vom Tracker schon relativ intelligent angelegt, nämlich Track-weise, und man muss selbst im Tracker ein mehrspuriges "Order" aus diesen Einzelspalten zusammenstellen. Diese Spalten-Patterns haben je 64 Einträge, die wiederum aus Note, Oktave, Instrument, Effektnummer und Effektdaten bestehen. Der Tracker speichert diese Informationen jeweils zusammengepackt in 3 Bytes, der Player expandiert das aber im Original auf 5 Bytes und legt es im EMS ab, zusätzlich richtet er einen Puffer im Datensegment ein für alle 9 Kanäle, der für jeden Order aus dem EMS gefüttert wird, also 5 Bytes * 64 * 9.

Tja, und so in der Art wird es auch von AtavISM gehandhabt - trackweise und er stellt selbst das Pattern daraus zusammen, erkennt identische Tracks (speichert diese dann nur einmal) und "packt" die Tracks intern (weil sie ja intern im "programm-artigen" ISM-Format vorliegen, mit Option für Schleifen und Subroutinen). Ich hatte das eben so gestaltet, daß es für den AtavISM-User nicht mehr auffällt.

zatzen hat geschrieben:In BASIC kam dann noch Faktor zwei dazu, weil es nur Integer aber nicht Byte kennt.

Ja, Hochsprachen, die "allzusehr" auf "ich bin eine Hochsprache und niemand muß wissen wie ein Computer WIRKLICH funktioniert" ausgelegt sind, sind mir inzwischen ein Greuel. Nichtmal byteweise arbeiten zu dürfen oder nicht die Wahl zu haben, ob mit oder ohne Vorzeichen - da würd' ich ausrasten...

zatzen hat geschrieben:Ich mache ziemlich das Gegenteil, lege die Patterndaten auf den Heap, aber komprimiert: Jedes Spaltenpattern bekommt eine Tabelle, die alle Ereignisse (Byte-Triplets) enthält, aber mit jeglichen Redundanzen rausgekürzt. Dazu kommt 64 mal eine Info, welcher Tabelleneintrag zu welcher Zeile gehört, diese Info nur so viele Bit breit, wie zur Adressierung aller Tabelleneinträge nötig ist. Angenommen man hat typischerweise Tabelleneinträge < 17, dann reichen 4 Bit-Werte für die Adressierung und die Tabelle ist maximal 16*3 Bytes groß. Man hat dann also statt 192 Bytes für das unkomprimierte Pattern nur 16*3 + 32 Bytes = 80 Bytes, plus ein paar Bytes für die Deklaration der Bitbreite der Adressierung und der Offsets der Tabellen und Adressreferenzen.

Ja, SO komplex hab' ich's bei AtavISM nicht gemacht - aber das liegt auch daran, daß AtavISM ja nicht einer "eigenständigen Grundidee" entspricht, sondern nur eine zusätzliche Möglichkeit darstellen soll, Musikdaten einzugeben, die ohne Änderung von der ISM-Player-Routine abgespielt werden können. Daher hat das ganze Ding ja auch so ein "Template" aus ISM-Daten um sich herum. Die Speicherersparnis ist bei AtavISM also allgemein geringer als bei prISM.

zatzen hat geschrieben:Insgesamt ergeben sich nicht selten Kompressionsraten auf geringer als 25%, da z.B. bei Schlagzeug oder simplen Melodien die Redundanzen noch stärker ausgeprägt sind. Nehmen wir ein Bassdrum-Pattern als Extrem, zwei Tabelleneinträge (Bassdrumereignis und Leerzeile), dazu eine 1 Bit Adressierung, also 6 + 8 Byte.

Allerdings hatte ich ISM ja nie primär zum Speichersparen ausgelegt (hatte ich ja mal erwähnt) - das ist nur ein Nebenfeffekt der beabsichtigten Arbeitsweise, die mir angenehmer ist als in "Trackern":
Ich bin ein eher bescheidener Musiker/Komponist - meine Stücke bestehen meist aus 2-3 Stimmen - mein allererstes vierstimmiges Stück war allen Ernstes dieses abartige "Entenlied" was ich da gezimmert hatte - und das auch eher als Experiment/Machbarkeitsstudie. Meine Stücke bestehen aus einer Hauptmelodie, einer Begleitmelodie und einer Art Percussion. Ich mache meist zuerst die Begleitmelodie. Diese (und die Percussion) sind aber SEHR repetativ - und wenn ich irgend etwas an Begleitmelodie oder Percussion ändern will (weil es mir nicht mehr so gefällt oder es nicht mehr so paßt), brauche ich das nur an einer einzigen Stelle (bzw. sehr wenigen Stellen) ändern und schon ist es wieder wie gewünscht. Außerdem kann ich so z.B. den gleichen Ablauf transponieren/schneller/langsamer spielen, um mehr Dynamik in das Stück zu kriegen und selbst diese Teile werden dann gleich mit geändert. Das spart mir eine Menge Zeit - und unkreativ/unmusikalisch, wie ich bin, bin ich froh, wenn ich überhaupt mal ein einigermaßen gescheites Stück zusammenhabe.

Das ist die ganze (oder ein Hauptteil) der Erklärung für ISM. Der Rest ergab sich daraus, daß ich ein so ähnlich funktionierendes Ding (von jemand anderem programmiert) auf dem C64 hatte und gern benutzt hatte. (Übrigens ist sowohl prISM als auch meine Malprogramme alle basierend auf Ideen/Bedienkonzepten von entsprechenden C64-Programmen entstanden. Sowohl mein Textmode- als auch mein Grafikmode-Malprogramm basieren in der Bedienung grob auf dem C64-Programm "Amica-Paint".)

zatzen hat geschrieben:Und ich brauche keinen Puffer, die Daten werden für jede Zeile/Track sozusagen mit "random access" gelesen.

Naja. AtavISM puffert nur den aktuellen Track während der Bearbeitung für den User. Beim Abspielen ist aber nichts mehr davon da, das ganze Stück liegt abspielbar im Speicher und für ISM macht es keinen Unterschied, ob es in prISM oder AtavISM erstellt wurde - das war auch das Hauptanliegen.

zatzen hat geschrieben:Hier noch die bisherige Interrupt-Routine:

Code: Alles auswählen

procedure introut_playtick; interrupt; assembler;
asm
  mov btplay_next_tick, 1
  call btplay_playtick
  mov ax, tickervalue
  add tickerwrap, ax
  jc @call_oldint
    mov al, 020h
    out 020h, al
    jmp @end
  @call_oldint:
    call old_intvec
  @end:
end;
RET oder IRET hat hier nicht funktioniert, daher JMP @end. Ist da vielleicht noch etwas auf dem Stack?

NATÜRLICH ist etwas auf dem Stack! Hatte ich ja in dem anderen Thread bereits geschrieben - vielleicht überflüssig, das jetzt nochmal zu erwähnen:
Die Pascal-Anweisung "Interrupt" legt einen Stack-Frame an UND sichert auch die ganzen Register auf den Stack! Die müssen da natürlich wieder runter! Wenn Du sowieso in Assembler programmierst, spar' Dir die "Interrupt"-Anweisung! Die ist nur für Hochsprache nützlich. In Assembler machst Du das sowieso alleine. Da würd ich einfach am Anfang db $66;pushA; machen und am Ende db $66;popA;IRET.

zatzen hat geschrieben:btplay_playtick ist die "dicke" Abspielroutine, der Kern der ganzen Sache.

Schon klar. Ich selbst spiele ja nicht im Interrupt ab. (Der spielt sowieso nicht ab, sondern berechnet nur die Daten. Das Abspielen passiert ja im SoundBlaster...)
Kann man natürlich so machen, dann braucht man in der Hauptschleife nicht ständig darauf achten, ob der Puffer schon abgelaufen ist, sondern das im Interrupt machen UND gleich darauf entsprechend reagieren. So braucht man das nicht in jeder Schleife machen.

Mein Konzept ist da leicht anders - was aber nur daran liegt, daß es bei mir immer nur EINE Hauptschleife gibt.

zatzen hat geschrieben:In DOS jedenfalls lohnt sich Assembler, wie ich schon gesehen habe, dass eine Zeile Pascal-Code auf mehr als doppelt so viel Takte und deutlich mehr Instruktionen gebläht wurde als dasselbe intelligent in Assembler formuliert.

Ja, das liegt daran, daß die Hochsprachen Befehle immer die gleichen Dinge tun - mitunter braucht man manche Dinge, die ein Befehl tut, an der Stelle nicht wirklich. Und in Assembler läßt man eben alles weg, was man nicht braucht. Bestimmt ist jede Subroutine in Hochsprachen IMMER mit Prüfroutinen ausgestattet, die jedes Mal testen, ob übergebene Werte im korrekten Wertebereich liegen und das entsprechend berücksichtigen. In Assembler WEIß man ja, was man tut (naja, sollte man zumindest) und braucht einen EINMAL geprüften Wert nicht immer wieder prüfen. Außerdem müssen nur Werte geprüft werden, die von unbekannten Quellen stammen - Usereingabe, geladenes File u.ä. - auf Werte, die man selbst im Programm erzeugt hat, sollte man sich verlassen können, die müssen nicht durch 5 Prüfroutinen gejagt werden.

zatzen hat geschrieben:Die Beni Tracker Player Portierung besteht zu ca. 95% aus Assembler, da bekommt man langsam Lust oder den Mut, sich komplett von Pascal zu verabschieden und alles in Assembler zu schreiben. Man müsste nur mehr kommentieren, sonst kann man bei den ganzen Anweisungen doch schonmal die Übersicht verlieren. Aber naja, selbst wenn alle Procedures in Assembler geschrieben sind ist ein bisschen Pascal-Code als Gerüst doch ganz nett.

Ja, ich nutze gern Pascal als Gerüst, weil ich z.B. keine Lust hätte, selbst einen EXE-Header zu bauen. Aber viele Dinge, vor allem "Dinge, die andere Dinge ausführen", da setze ich zunehmend auf 99% Assembler, weil ich einfach den Speicher- UND Geschwindigkeitsvorteil sehe. Kleiner Nachteil ist, daß Entwicklung in Assembler etwas länger dauert - aber zum Glück hetzt mich ja keiner. (Oder gibt's hier etwa wirklich jemanden, der auf mein neues Spiel wartet?)

zatzen hat geschrieben:CALLs: Wenn ich nicht irre (und ich meine so hab ich es auch im ASM FAQ gelesen) gilt RET sowohl für NEAR als auch für FAR CALLs. Mir schwirrte noch "RETF" im Kopf, aber das gibt es wohl gar nicht.

Doch, RETF gibt es. Aber Ich hatte das ganze CALL/RET Thema ja schon ausführlich behandelt. Will ja nur noch auf diesen ursprünglichen Beitrag antworten, bevor quasi durch die nachfolgenden Sachen der Zusammenhang in diesem Topic etwas verlorenging.

zatzen hat geschrieben:Der große Unterschied zu DOS ist das Multitasking (auch wenn es nur emuliert ist), das ich bei DOS vermissen würde. Bei DOS hatte man ja immerhin schon eine Faszination dafür, nämlich wenn man TSRs benutzt hat. Sei es um gleichzeitig Musik zu hören und etwas anderes tun zu können, oder einen Screengrabber zu nutzen. Ich würde mich mittlerweile schon etwas eingeschränkt fühlen wenn ich immer jedes Programm erst beenden müsste um ein anderes zu benutzen, vor allem wenn ich eigentlich beide eher parallel brauche.

Naja, beim Programmieren ist das für mich eher kein Problem, da man Turbo-Pascal ja auch zwischendurch verlassen kann und wieder zurückgehen ("DOS aufrufen"). Pascal speichert dann alles und wenn man mit Exit zurückgeht, ist alles wieder so wie bevor man rausging.

Und: Computer (bzw. "moderne OS") können wohl Multitasking - ich aber nicht: Ich kann immer nur EIN Programm gleichzeitig bedienen - deshalb würde mir so ein "Taskwechsel" Zeug auch ausreichen: Immer wenn man ein anderes Programm benutzt, halten alle anderen an.


zatzen hat geschrieben:
DOSferatu hat geschrieben:Ich finds 'ne steinkalte Schande, daß VGA keinen Rasterzeileninterrupt hat

Ich habe vor Jahren mal in irgendeinem Code kommentiert gelesen "wait for vertical retrace". Ich bin mir nicht sicher ob das auch mit CRTs zu tun hatte und bei TFTs & Co. gar nicht funktioniert hätte.

Ja, deshalb ja "wait for vertical retrace". Gäbe es einen Interrupt dafür, bräuchte man nicht warten und "pollen". Es GIBT ein Bit (Flag) in einem VGA-Register, das anzeigt, ob gerade ein VR stattfindet - nur muß man das eben dauernd abfragen, um das zu erfahren. Beim Grafikchip des C64 kann man sogar die Bildschirmzeile festlegen, wann ein Interrupt erfolgen soll (oder mehrere) - also nicht nur beim Bildende.

zatzen hat geschrieben:
DOSferatu hat geschrieben:[Soundpuffer]Das "live" Ausführen, sobald angefordert funktioniert auf alten Konsolen, weil die einen dedizierten Soundchip haben, der neben der CPU her arbeitet und sofort etwas macht, sobald man es will. Aber so Dinge wie die Sound Blaster wollen ja erst einen fertig berechneten Puffer.

Viele Spiele nutzen ja die Kombination Adlib + Digisound, und letzterer geht dann nicht über einen Mischpuffer sondern spielt nur per DMA direkt Sounddaten ab, das geht dann ohne Verzögerung, aber eben nur "einstimmig".

Naja, daß ich drauf kam, Daten digital zu berechnen/abzuspielen, liegt wohl u.a. daran, daß ich damals nur mit PC-Speaker (sowohl "analog" als auch im "digitalen Modus") gearbeitet habe und der hat ja nicht so Dinge wie AdLib drin. Das komplette ISM (und prISM) hatte ich 100% mit dieser "Digi-Speaker"-Variante entwickelt und die SoundBlaster-Fähigkeit hatte ich dann quasi "nachgerüstet". Seit ich mir natürlich eine Blaster-Unit gebaut habe, ist SoundBlaster abspielen für mich genauso einfach wie PC-Speaker.

Schade, daß kein "Covox Speech Thing" oder wie das Ding hieß habe und auch keinen so Nachbau. Wäre mal cool, diese Methode per LPT zu hören... Ja, ich weiß: Kann man sich auch selber löten - wenn man kann.

zatzen hat geschrieben:Was Puffer-berechneten Sound angeht muss ich mir mal ein paar Spiele ansehen, wie z.B. Pinball Dreams, die Konversion vom Amiga, bei dem ich mich an keine besonderen Verzögerungen erinnere.

Naja, es werden kurze Puffer sein. Aber dank Puffer-Vorberechnung und Puffer-DMA wird hat man eben trotzdem eine Verzögerung. (Das Ding mit Ursache und und Wirkung.) Bei der ganzen Dynamik eines Spiels fallen einem so Dinge wahrscheinlich nur nicht so auf.

zatzen hat geschrieben:Dosbox: Sehr erfreulich dass der ROR/ROL Bug wahrscheinlich demnächst raus ist.

Naja, wie gesagt: Wenn man Core auf =dynamic stellt, hat man den Bug auch jetzt schon nicht. Eine offizielle neue Version (die dann den Bug entfernt hat) gibt es wohl noch nicht. Könnte man sich höchstens selbst compilieren. (Aber das mach ich sowieso nicht.)
Aber wenn ich mein Zeug weitergebe und das soll unter DOSBox laufen, muß ich ja davon ausgehen, daß die User nur eine "reguläre" Version haben. Aber, wie bereits gesagt: Ich verzichte weder auf Code-Tricks, noch optimiere ich "in Richtung DOSBox". Mein Zielsystem ist die reale Maschine. Wenn ein Emulator die fehlerhaft emuliert, ist der Emulator schuld, nicht das Programm, das auf der echten Maschine fehlerfrei läuft.

zatzen hat geschrieben:Für mich stellt Dosbox ein wenig auch soetwas dar, wie meinetwegen Ende der 80er Leute z.B. für den C64 oder Konsolen in einer technisch fortschrittlicheren Umgebung programmierten. Es ist nicht ganz damit zu vergleichen aber ich habe vor allem zwei Vorteile: Ich kann einen Hex-Editor oder andere Daten-lesende Programme parallel laufen lassen ohne dass ich für einen Refresh der Daten Pascal beenden muss. Und was ich schonmal sagte, bei dummen Programmierfehlern, sei es in Assembler oder Pascal, die zu einem Absturz führen, muss ich keinen echten Rechner neu booten. Kurzgesagt, das Debugging innerhalb einer Multitasking-Umgebung ist für mich wenigstens einfacher.

Ja, hast schonmal erwähnt. Wenn mein 486er einen Komplettabsturz hat und ich den neu booten muß, braucht der, selbst wenn die Treiber geladen werden, 20 Sekunden zum Booten. Genau die richtige Zeit zum "runterkommen" (vom Ärgern über Absturz) und zum "in sich gehen". Der Computer ist ja nie schuld (ja, außer bei echten Hardwarebugs), der Computer macht ja nicht das, was man will - sondern das, was man eingibt. Die meisten Fehler sind also "Layer 8 Error".

Und, wie erwähnt, kann man auch aus der Pascal-IDE "aus- und wieder einsteigen".

zatzen hat geschrieben:Es wäre natürlich schön, wenn mein aktueller Rechner noch 16 Bit DOS könnte, selbst wenn man mal Soundblaster außen vor lässt. Mein DMF-Renderer wäre blitzschnell.

Wieso kann Dein aktueller Rechner kein 16 Bit DOS? Etwa so UEFI-Dreck statt BIOS?

zatzen hat geschrieben:
DOSferatu hat geschrieben:[Trenz... 2D-Katakis]

Nur in eine Richtung scrollend scheint technisch einen deutlichen Unterschied zu machen, ich denke da an Super Mario, da konnte man ja auch nur immer weiter nach rechts laufen. Das hatte wohl auch mit der Berechnung der Feindbewegungen zu tun, die somit minimiert werden konnten (Feinde bewegten sich auch erst sobald sie sichtbar waren).

Klar, in der 8-Bit-Zeit haben die Leute eine Menge Dinge gemacht, um aus schwacher Technik mit wenig Speicher trotzdem eine Menge rauszuholen. Dem Spieler ist sowas auch damals nicht aufgefallen.

zatzen hat geschrieben:Ich bin aktuell eher noch am überlegen, wie ich den Grafikdatenumsatz minimal halte, und bei Scrolling sehe ich keinen anderen Weg als den ganzen Bildschirm für jeden Frame komplett neu zu zeichnen. Wenn ich dafür eine Lösung habe komme ich gerne nochmal auf die Ideen von Manfred Trenz zurück.

Naja, für das, was ich so mache (Mehrwege-Scrolling) ist auch nichts anderes möglich als Komplettframe neuzeichnen. Obwohl es ja, wie gesagt, auch noch die Möglichkeit gibt, die Softscroll-Register der VGA zu benutzen, die Scanline mehrere Bildschirme breit zu machen usw...

zatzen hat geschrieben:
DOSferatu hat geschrieben:Und meine neue Idee dazu ist eben, daß ein "Level" nicht die Daten enthält, sondern nur die Namen der Files (und ein paar Parameter), aus denen es besteht, bzw. die es braucht. Und die eigentlichen Files sind unabhängig davon im Datenfile verteilt. Eigentlich ist die Idee nicht so die totale Revolution oder so - aber vorher hatte ich daran eben nicht gedacht.

Ja, ich glaube ich hätte das auch so gelöst wie Du es jetzt machst, auch wenn dadurch die Ladezeit vielleicht minimal länger wird. Oder, ein anderer Ansatz, wenn ich genug Speicher habe bzw. die Gesamtdaten klein genug - Alles am Anfang laden und dann beliebig in den Levels benutzen.

Naja. Genug Speicher werd ich nie haben. Ich gehe ja davon aus, daß man mehrere Levels mit verschiedenen Hintergrund-Sets hat und eben auch dazu passende Figurentypen (und eben auch "allgemeine" Figurentypen, die es "überall" oder eben oft gibt. Und dann z.B. so Figuren wie "Endgegner", die man ja nur für das jeweilige Level braucht, weil jedes Hauptlevel natürlich einen eigenen Endgegner hätte - wäre ja sonst langweilig.

Und gerade, wenn man, wie Du, auch noch viel Platz für Samples übrig lassen will, ist das "von Grafik pro Level nur das laden, was gebraucht wird", ja sicher eine gute Idee. Wie schon erwähnt: Grafiken sind ein unheimlicher Speicherfresser. Da kann man packen wie man will. Und, wie (auch schonmal) erwähnt: (Zu) komplizierte Entpack-Routinen brauchen Speicher und kosten Performance. Da muß man dann den richtigen Mittelweg finden.

zatzen hat geschrieben:[.... Sprites....]
Es geht um das Codieren. Das (eher kleine) Problem ist, dass sich mehrere Sprites in einem Datensatz eine Meta-Palette teilen. Bisher habe ich den Encoder nur auf ein Sprite ausgelegt, bei mehreren muss die Meta-Palette je nachdem erweitert werden. Es funktioniert bisher gut für ein Sprite und für das Feature "mehrere" muss ich eben nochmal am Code rumfummeln mit der Gefahr dass Bugs reinkommen, das ist die kleine Hürde.

Etwas merkwürdig finde ich das schon. Wenn ICH irgendwas für Sprites machen würde, hätte ich erst gar nicht mit irgend etwas angefangen, das "erst mal nur für EIN Sprite" funktioniert - weil ich NIE vorhatte, irgend ein Spiel zu machen, das nur EIN Sprite braucht.

zatzen hat geschrieben:Aber ein wichtiger Vorteil Deiner Methode, die Grafik in Planes zu halten, ist die dass es ein festgelegter Bereich ist den man ohne Heap-Fragmentierung be- und entladen kann?

Nö. Diese "Sprite-Bahnen" können verschieden groß sein - da gibt es keine festgelegten Größen. Und bei meiner dynamischen Speicherverwaltung muß es das ja bekanntermaßen auch nicht. Heap-Fragmentierung tritt in meinen Programmen nicht auf.

Es geht eher darum, daß man verschiedene Grafiken "ein-/ausblenden" kann, wenn man sie nicht braucht. Die Image-Tabelle der Sprites bleibt erhalten! D.h. gleiche Images haben auch immer den gleichen Image-Index. Nur zeigen bei Figuren, die man gerade nicht braucht, die Image-Zeiger auf nichts Sinnvolles. Ich weiß nicht, ob ich das verständlich genug erklären kann.

zatzen hat geschrieben:
DOSferatu hat geschrieben:Es ist manchmal schon erstaunlich, was eine einfache andere Farbanordnung mit dem gleichen Image anzustellen vermag.

Ich könnte das bei ZVID2 durch ändern/austauschen der Metapalette machen, allerdings ist das doch eher umständlich und mein Grafikstil eher derjenige, der für alle Charaktere völlig eigene Bitmaps vorsieht. Hatte ich ja schonmal gesagt, und ich kenne die Umfärbung von Sprites eher nur von 8 Bit Konsolen wie NES, wo die Sprites aus 8x8 Blöcken bestehen die je nur 2 Bit haben und zwingend eine "Meta"-Palette brauchen.

Naja, bei mir haben sie ja schon deswegen eine Meta-Palette, weil sie 15 aus 256 Farben haben können, und zwar BELIEBIGE 15 Farben der 256. Die Farben müssen NICHT in der 256-Palette direkt hintereinander liegen! Und wenn sie schon so eine Meta-Palette haben, können sie auch mehrere haben. So hat man noch mehr Figuren bei minimalem zusätzlichem Speicherbedarf. Kann ja jeder anders sehen - aber ich finde die Idee großartig.

zatzen hat geschrieben:Aber natürlich gibt es auch andere sinnvolle Anwendungen, wenn Du "Battle Chess" kennst, die werden wahrscheinlich auch für die roten bzw. blauen Figuren auch nur anderen Paletten genommen haben.

Ist anzunehmen. Und (wie sicher bereits erwähnt) es betrifft z.B. auch die Spinnen in Xpyderz.

zatzen hat geschrieben:Skript:
Ich weiss zwar jetzt aus dem Zusammenhang gerissen nicht genau, welchem Zweck so ein Script dient, aber Du sagst ja direkt einleitend dass es für einen Editor ist. Also wenn man so will für ein UI und was "dahinter" steckt. Klingt gut.

Naja, unter anderem. Es ist in dem Fall sozusagen eine Art primitive Interpreter-Sprache, die durch eine 100% Assembler-Routine ausgeführt wird. Vorteil für mich ist, daß ich diese "Skripts" dann einzeln nachladen kann, wenn ich sie brauche und nur das, was ich gerade brauche, im Speicher halten muß. Ursprüngliche Idee war, daß das z.B. für so Menüs im Spiel benutzt werden kann - weil das Ding auf den ganzen Speicher und auch auf Variablen zugreifen und damit arbeiten kann. So kann man damit irgendwelche Einstellungen machen, kann aber das Menüsystem trotzdem vom Rest trennen.

Das Ganze hat etwas damit zu tun, daß ich in den letzten Jahren immer mehr dazu übergegangen bin, mein ganzes Zeug "modular" zu machen. Um dem aber gerecht zu werden, und es deswegen nicht in totale Speicher- und Performancefresser ausarten zu lassen, wird das modulare Zeug mehr und mehr in Assembler gebaut...

Ich sehe schon: In 20 Jahren hab ich so viel coole Routinen hier, daß ich damit das Spiel des Jahrhunderts bauen könnte - und niemand wird mehr eine Maschine oder Emulator besitzen, auf dem das überhaupt lauffähig wäre...

So, damit habe ich endlich auch mal auf diesen Beitrag geantwortet.
Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 462
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Do 21. Mai 2020, 23:55

Hallo!

Ich bin in letzter Zeit mal wieder auf den Geschmack gekommen, Musik abzumischen. Passt ja mehr oder weniger auch etwas in den Thread hier. Naja und da habe ich mir gedacht, guck ich mal nach was ich noch so an älteren Musikstücken habe die ich bisher noch nicht "veröffentlicht" habe und mache ein neues Album daraus. Vielleicht schaffe ich das noch bis Ende dieses Jahres. Je nachdem sind das teilweise noch 4-Kanal MODs mit vielen 8 kHz Samples - ich suche die Quellen raus von denen ich damals gesamplet habe (habe einiges auf Kassetten gefunden und suche aber noch) und restauriere das alles. Ich erfülle mir da einen kleinen Traum, produziere nochmal die Sachen ab 1994 mit den technischen Möglichkeiten die ich heute habe. Das ist nicht nur nach dem Motto "Kristallsound" sondern erlaubt jetzt auch die uneingeschränkten gestalterischen Ideen, die damals technisch begrenzt waren.
Programmiert habe ich mir dafür auch noch schnell etwas, ich brauchte ein Tool das mir die Samples aus X-Tracker Modulen als vernünftig benannte und mit korrekter Samplerate versehene WAV Dateien rausholt. Kein großes Ding, aber musste gemacht werden. Und dabei durfte ich nochmal erleben wie wunderbar klar sowas geht mit gut strukturierten Binärdateien. Ich hatte auch schon angefangen für Renoise ein Tool zu bauen (damit ich damit musikalische Videos machen kann) aber so ein XML erscheint mir doch etwas haarig. Irgendwie so seriell, eben wie HTML, Strukturen werden nicht angekündigt durch nen Zähler sondern sie kommen einfach. Irgendwie müsste ich da auch etwas System reinkriegen, bisher ist alles nur provisorisch. Man muss das wohl regelrecht "parsen" bzw. gewissermaßen kompilieren.

Sicherlich habe ich demnächst dann wohl viel mit Musik zu tun, aber das muss nicht heissen dass deswegen das Programmieren total versiegen wird. Ich denke eher, durch mehr kreative Aktivität kommt der Stein allgemein mehr ins Rollen. Trotzdem ist mein jetziger Standpunkt (der sich aber ändern kann), dass ich lieber ein kleines Spiel machen möchte das nicht so viel Arbeit bedeutet. Ob Mode-X notwendig ist, ist fraglich. Sauberes 4:3 mit quadratischen Pixeln wäre schön, aber mit meinen derzeitigen Kenntnissen über Mode-X würde die Erschliessung dessen möglicherweise halb so lange dauern wie die Programmierung des ganzen Spiels. Motiviert wäre ich nach wie vor auch durch meine Spielerei mit ZVID2, die sich in Mode-X so wie ich das sehe ziemlich kompliziert gestalten würde.
Wie gesagt bin ich Mode-X absolut nicht abgeneigt, d.h. wenn ichs erstmal gerafft habe. Ansonsten freuen sich alle sicherlich am meisten wenn ich einfach ein Spiel raushaue das Spaß macht, selbst wenn es steinzeitliches MCGA ist oder gar per Soundpuffer getaktet - immerhin Sound abschaltbar und dann über Timer. Mode-X und nicht-per-Soundpuffer timen sind meine Ziele, aber ich kann das aktuell noch nicht. Ich werde da keinem Ideal gerecht wenn ich es so mache wie ich es momentan nur kann, aber wenn das Spiel Spaß macht, was soll's. Es wird dann eher so sein dass es einen 486 braucht obwohl es anders programmiert auch auf einem langsamen 386 funktionieren würde. Oder - ich mache AdLib + One-Shot DMA Digisound. Naja, ich laber mich hier schon wieder aus allem raus. Einfach mal sehen was wird. Ich möchte ZVID2, das Ding mit den 4x4 Blöcken für Sprites verwenden - auch wenn das Performance kostet, aber das ist für mich interessanter, auch wenns keiner versteht. Gleiche Sache mit ZSM, das kostet vor allem Performance wegen der Delta-Codierung der Samples. Aber auch da fasziniert mich dass es klein ist und trotzdem vernünftig klingt.
Mode Q fand ich noch interessant - nutzt komplette 64K und hat 256 Zeilen. Die direkte X/Y Adressierbarkeit spielt für mich wohl keine Rolle. Wirklich interessant fände ich den Modus wenn er kein quadratisches sondern 4:3 Bild erzeugen würde und die Pixel dann auch jeweils 4:3 wären. Ich weiss nicht ob man das entsprechend "tweaken" kann, aber wenn, dann fände ich das interessant, es wäre dann ein ähnlicher Bildmodus wie das NES mit seinen 256x240, wobei da die Pixel glaube ich auch 4:3 sind.

DOSferatu hat geschrieben:Wie Dir ja schon selbst ausgefallen ist, hat das GW-BASIC, sowohl was Speicherverfügbarkeit als auch Ausführungsgeschwindigkeit anbetrifft, gewisse Grenzen - die Borlands TurboPascal zwar auch hat, dort aber wohl etwas höher liegen.

GW-BASIC wäre ja dieses C64-ähnliche Ding. Es handelt sich hier um QuickBASIC, da hat man schon eine komfortable IDE welche die Subroutinen gesondert anzeigt, zudem braucht man keine Zeilennummern. Es ist allerlei möglich, ich habe aber zu den Zeiten als ich damit arbeitete keine Analogie zu Pascals GETMEM gefunden, daher war ich auf das kleine Datensegment angewiesen, und mangels Units auch auf 64 KB Codesegment, die oft viel zu schnell voll waren, zudem im Interpreter-Modus größere Programme zuliessen als man kompilieren konnte, das war oft ärgerlich. Bei Pascal würde mich am ehesten stören, dass die IDE viel Speicher belegt und man daher vielleicht nur 300 KB Speicher frei hat, aber sobald man kompiliert hat und das Programm ausserhalb aufruft hat man entsprechend mehr. Das spricht im Prinzip für die Entwicklung von Engines, so dass man die Daten designt und testet ohne die Pascal IDE parallel laufen zu haben.

DOSferatu hat geschrieben:[...]Nichtmal byteweise arbeiten zu dürfen[...]

In 64 Bit Zeiten und wenn man es nicht mit Speicherkritischen Daten zu tun hat vielleicht irgendwie verständlich. Vieles wird als DWORD gespeichert wo ein Byte reichen würde, und ich selbst nutze in Freepascal oft WORD oder DWORD wo vielleicht auch BYTE reichen würde, weil ich Überläufe bei Kalkulationen befürchte, die ich zumindest in DOS Pascal kenne. Also wenn man Byte * Byte rechnet kann es passieren dass das Ergebnis ein übergelaufenes Byte wird statt einem Word, oder so ähnlich. Kennst Du bestimmt. Dafür gibts ja aber das "Typecasting" word() oder was auch immer.

DOSferatu hat geschrieben:Ja, SO komplex hab' ich's bei AtavISM nicht gemacht

Klar. Das ging in meinem Fall auch vor allem deshalb, weil die Daten nicht mehr verändert werden mussten, deshalb das Bit-packing.

DOSferatu hat geschrieben:Meine Stücke bestehen aus einer Hauptmelodie, einer Begleitmelodie und einer Art Percussion. Ich mache meist zuerst die Begleitmelodie. Diese (und die Percussion) sind aber SEHR repetativ - und wenn ich irgend etwas an Begleitmelodie oder Percussion ändern will (weil es mir nicht mehr so gefällt oder es nicht mehr so paßt), brauche ich das nur an einer einzigen Stelle (bzw. sehr wenigen Stellen) ändern und schon ist es wieder wie gewünscht. Außerdem kann ich so z.B. den gleichen Ablauf transponieren/schneller/langsamer spielen, um mehr Dynamik in das Stück zu kriegen und selbst diese Teile werden dann gleich mit geändert. Das spart mir eine Menge Zeit - und unkreativ/unmusikalisch, wie ich bin, bin ich froh, wenn ich überhaupt mal ein einigermaßen gescheites Stück zusammenhabe.

Ja, im Grunde ist so ein Vorgehen sehr effizient und nutzt den Speicher optimal. Ich habe auch viele repetetive Elemente wenn ich etwas trackere. Aber es gibt auch ständig Veränderungen, und da wäre es für mich etwas fummelig wenn ich z.B. wie im Beni Tracker jedes mal diese Veränderung definieren müsste und dazu noch wo sie genau reinkommt. AtavISM gibt sich ja nach aussen hin wie ein normaler Tracker, und so bin ich das Arbeiten gewöhnt. Meistens habe ich in X-Tracker um weiterzukommen einfach das komplette letzte Pattern kopiert und dann entsprechend gewünschte Änderungen vorgenommen. Das ergibt Speichertechnisch viele Redundanzen, und so existieren diese auch in ZSM, obwohl dort alles erstaunlich klein gehalten wird. Allerdings sind auch komplexe Musikstücke wie sie in der Klassik zu finden sind denkbar und diese würden dann nicht mehr beanspruchen als redundante Popularmusik.

DOSferatu hat geschrieben:Ich kann immer nur EIN Programm gleichzeitig bedienen - deshalb würde mir so ein "Taskwechsel" Zeug auch ausreichen: Immer wenn man ein anderes Programm benutzt, halten alle anderen an.

Ja, im Prinzip könnten Taskwechsel genügen, daran hatte ich noch gar nicht gedacht, es gibt aber Ausnahmen: Wenn ich hier Musik abmische sehe ich mir gern das Spektrum in einem anderen Programm an das parallel läuft (im Aufnahmemodus), oder man hat langwierige Berechnungen, codierungen, sonstwas laufen und möchte nebenbei nochwas anderes machen. Letztlich kann man solche Situationen aber auch durch mehrere PCs lösen, das merke ich gerade dass das auch praktisch sein kann, weil ich seit ner Weile nur noch mit einem Laptop im Internet unterwegs bin und den anderen Rechner offline parallel nutze, da ist man noch ein Eckchen flexibler manchmal. So könnte ich mir besagte Spektrum auch auf dem Laptop anzeigen lassen und spare mir den Taskwechel.

DOSferatu hat geschrieben:Schade, daß kein "Covox Speech Thing" oder wie das Ding hieß habe und auch keinen so Nachbau. Wäre mal cool, diese Methode per LPT zu hören... Ja, ich weiß: Kann man sich auch selber löten - wenn man kann.

Ich habe mir so einen gelötet als ich 14 oder 15 war. Ziemlich einfach, man braucht nur einen LPT Stecker, Audiobuchse, Folienkondensator, und 20 oder so 1% Widerstände 10k oder was das war. Der Klang ist ziemlich gut, vor allem bei hohen Rates.
Das ist auch mit der Grund warum ich es schade fand dass heutige PCs keine parallele Schnittstelle mehr haben. Man konnte so einfach elektronische Basteleien damit machen. Heute braucht man für alles einen Arduino oder sowas.

DOSferatu hat geschrieben:[Zatzens Beobachtung bei Pinball Dreams]Naja, es werden kurze Puffer sein. Aber dank Puffer-Vorberechnung und Puffer-DMA wird hat man eben trotzdem eine Verzögerung. (Das Ding mit Ursache und und Wirkung.) Bei der ganzen Dynamik eines Spiels fallen einem so Dinge wahrscheinlich nur nicht so auf.

Ich habe mit Dosbox ein Video mitgeschnitten. Ich kann das bei Gelegenheit mal ganz akkurat überprüfen, d.h. bei genau welcher Stelle sich z.B. ein Hebel bewegt und wann dazu der Sound einsetzt.

DOSferatu hat geschrieben:Wieso kann Dein aktueller Rechner kein 16 Bit DOS? Etwa so UEFI-Dreck statt BIOS?

Ja, tatsächlich UEFI. Aber allein schon Windows 7 verbietet 16 Bit Programme.
Über sowas wie einen modernen Rechner in Dos zu booten habe ich noch gar nicht nachgedacht. Bisher genügt mir da aber auch Dosbox.

DOSferatu hat geschrieben:Naja, für das, was ich so mache (Mehrwege-Scrolling) ist auch nichts anderes möglich als Komplettframe neuzeichnen. Obwohl es ja, wie gesagt, auch noch die Möglichkeit gibt, die Softscroll-Register der VGA zu benutzen, die Scanline mehrere Bildschirme breit zu machen usw...

Softscroll/Scanline mehrere Bildschirme - das geht dann aber auch nur in dem Sinne dass man den Bildbereich nur vergrößert, oder? Sonst hat man ja mittendrin einen kleinen Hänger wenn ein kompletter neuer Screen gezeichnet werden muss.
Oder Stückeln? Müsste gehen... Aber das ist dann definitiv erst für mich möglich wenn ich Mode-X kapiert habe.

DOSferatu hat geschrieben:["META"Paletten bei Sprites]Naja, bei mir haben sie ja schon deswegen eine Meta-Palette, weil sie 15 aus 256 Farben haben können, und zwar BELIEBIGE 15 Farben der 256. Die Farben müssen NICHT in der 256-Palette direkt hintereinander liegen! Und wenn sie schon so eine Meta-Palette haben, können sie auch mehrere haben. So hat man noch mehr Figuren bei minimalem zusätzlichem Speicherbedarf. Kann ja jeder anders sehen - aber ich finde die Idee großartig.

Bei ZVID2 würden im Grunde auch 16 Byte reichen um weitere Farbgebungen zu realisieren, aber das wäre dann eine META-META Palette und man müsste das im Programm doppelt tabellarisch nachschlagen. Ich finde Deine prinzipiell simple Reduktion der Spritefarben auf 15 + Transparenz vernünftig, auch wegen der Performance. Bei mir ist es einfach so, dass ich zu verspielt bin mit diesen gepackten Formaten von mir. Ich glaube diese ganzen komischen Sachen die ich mir ausdenke motivieren mich mehr ein Spiel zu machen als das Spiel selbst. Vielleicht bin ich als eigentlicher "Musiker" einfach nicht so ideal für die Grafikprogrammierung.

Ich mache das ja irgendwie nur nebenbei. Ich spiele viel rum, experimentiere, manche haben schon gesagt ich soll nicht immer das Rad neu erfinden. Beim Programmieren bin ich auf jeden Fall nicht professionell, mir macht da das Tüfteln Spaß, und gerade auch in Assembler, da schreibe ich gerne mehr oder weniger komplizierte Routinen wie z.B. Entpackroutinen, die dann trotz allem verhältnismäßig schnell sind, jedenfalls schneller und überhaupt erst tolerabel gegenüber dem als hätte man sie in Hochsprache geschrieben. Das macht so einen Teil der Faszination für mich aus. Es ist ein bisschen wie Rästelhefte lösen manchmal, nur dass die Ergebnisse nicht in der Tonne landen, sondern evtl. in einem Spiel, das dann vielleicht nicht performt wie ein professionelles, aber doch besser ist als nichts.
ZSM hat sich jedenfalls schon gelohnt, ich höre mir damit schonmal gerne ein bisschen Musik an. Ich könnte auch einfach die originalen MODs in Windows im Winamp abspielen, aber irgendwie mag ich das Interface meiner Software, die ganzen Anzeigen usw., die hat Winamp nicht, und letztlich hält mich wohl auch bei der Stange, dass ich genau dieses Format auch in ein Spiel bringen könnte, mit genau dem eigentümlichen Klang, und so verschaffe ich mir ein Gefühl dafür, was wie klingt und wieviel Speicher es braucht etc.

Über Emulatoren würde ich mir mal keine Sorgen machen. Vielleicht wenn irgendwann 256 Bit Windows kommt.
Benutzeravatar
zatzen
DOS-Kenner
Beiträge: 462
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Trackermodul-Engine (sehr einfach)

Beitragvon zatzen » Do 28. Mai 2020, 19:02

Ich habe mal den Ton vom Pinball Dreams Mitschnitt vermessen.

Ich bin da auf verschiedene Ergebnisse gekommen. Von 13 ms Versatz bis hin zu ca. 50 ms.
Ich vermute, dass die Länge des Puffers so gewählt ist dass die Frequenz der halben Bildwiederholrate entspricht, hier
also ca. 35 Hz. Die Musik klingt für meine Ohren gerade bei eigentlich sauber zu erwartenden, geraden Tönen etwas unsauber, vielleicht wird hier kein Auto-Init-DMA angewendet sondern jedesmal die DMA Übertragung neu angeschubst, was kleinste Aussetzer verursacht, die dann den Klang ein wenig "zerstückeln" bzw. modulieren.
Ich sehe da durchaus auch einen Grund: Man kann mit manuellem DMA-Auftrag den Sound per Timer takten, da so ein One-Shot-DMA nicht durchlaufen muss sondern abgebrochen wird sobald der nächste kommt. Das klingt zwar dann nur bei perfektem Timing optimal, orientiert sich aber eben immer fest am Timer und gallopiert nicht von selbst davon. Ob das für unabhängige Programmierung von Sound und Spieltiming ein Vorteil sein kann weiss ich noch nicht.
Und wie gesagt ist das hier nur eine Vermutung, aber ich bin mir fast sicher, so einen "Stückelsound" früher einmal bei einem Spiel erlebt zu haben.
Ein Vorteil davon, den Soundpuffer per One-Shot einzubinden wäre, dass man sich beim Hängen von Frames keine nervigen Soundloops einhandelt. Wenn das Timing per Interrupt gelöst wird und man eine verlässliche Samplerate bei der Soundkarte einstellt (was durchaus beachtet werden muss, da der Soundblaster mit seiner 256-Stufen Formel da eher ungenau ist) spricht eigentlich nichts dagegen, One Shot DMA zu nutzen - es sei denn so ein DMA-Befehl hat eine hörbare Verzögerung im einstelligen Millisekundenbereich. Wenn das möglich wäre, käme mir jedenfalls erstmal, so wie ich das sehe, die ganze Geschichte etwas "zahmer" vor. Aber wenn ich's mir recht überlege, wenn man wirklich Bild und Ton unabhängig timen will ist Auto-Init-DMA klar im Vorteil.

Überhaupt, auch zum Thema Grafik, mache ich lieber alles Schritt für Schritt. Für mich steht da erstmal ein Spiel an bei dem ich die Möglichkeiten von Assemblerroutinen mit Transparenz auskoste, das ist für mich noch relativ neu, auch wenn ich das ein oder andere Testspiel vor 20 Jahren schon gemacht habe. Deine bisherigen Erklärungen zu Mode-X, DOSferatu, sind nicht in den Ofen geschossen, auch nicht die zur unabhängigen Programmierung von Bild und Ton. Wenn man die Soundpuffer klein halten kann und sie dann wegen Unabhängigkeit trotzdem schnell berechnet werden sehe ich überhaupt kein Problem darin, dass ich das so mache.

Wer ist online?

Mitglieder in diesem Forum: Bing [Bot] und 2 Gäste