Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Diskussion zum Thema Programmierung unter DOS (Intel x86)
Benutzeravatar
Thomas
DOS-Kenner
Beiträge: 426
Registriert: Mi 22. Jun 2016, 12:29
Wohnort: Nähe von Limburg / Lahn

Re: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von Thomas »

Will ich gern tun. Ich schicke Dir den original Beni Tracker und mein Geschreibsel. Aber es ist noch etwas unstrukturiert, sei also gewarnt.
Auch möchte ich hier nocheinmal klarstellen das der Beni Tracker nicht von mir ist.
Ist, wenn auch selten, in diversen BASIC Foren zu finden (z.B hier: http://petesqbsite.com/downloads/sound.shtml, der zweite Eintrag) und wenn ich es richtig verstanden habe, darf man ihn ohne Einschränkungen nutzen, verändern, etc.
Ach ja, die ReadB Funktion habe ich bereits als Unterfunktion von LoadMOD und mit dem normalen Read ging es auch. Ich vermute mittlerweile dass es doch eher an den Variablen Typen liegt. Wenn ich alle Integer in Pascal auch als Integer deklariere kommt gar kein Ton. Wenn ich die Patternvarieblen als Byte deklariere geht es aber die Pattern sind durcheinander. Wenn ich die Instrument Variablen als Byte deklariere, klingt es verzerrt. Im original werden auch in LoadMOD zwei dynamische Arrays angelegt was in Pascal ja nicht möglich ist. Ich habe sie dann statisch mit dem höchsten zu erwartenden Wert angelegt. 203 Pattern und 31 Instrumente. Vielleicht liegt auch da der Hund begraben. Warum er byteweise und als String in BASIC einliest habe ich noch nicht herausgefunden. Basic unterstützt auch binäres lesen mit Puffer. Ich habe ja in den Anfängen 2017 ein Diskcopy geschrieben was nicht langsamer war als das DOS Diskcopy.
Das Format ist jedenfalls in der Readme beschrieben.

Ach so, eine Empfehlung war ja Allegro zu nutzen aber wie geht das denn jetzt genau? Das compilieren bekomme ich wohl hin aber ich wollte jetzt nicht extra auf C umschwenken. Was ich so gelesen habe, ist Allegro jetzt aber nicht direkt in TP/BP nutzbar. Dafür gäbe es wiederum so einen Allegro.PAS Wrapper. Kostet der dann aber nicht wieder Speicher und Geschwindigkeit? Desweiteren soll Allegro wohl eine reine 32 Bit lib sein was mich doch in den PM zwingt? Ich wollte schon gern bei Pascal bleiben und definitiv bei DOS.

Nochmals vielen Dank für Deine Unterstützung
Ein bisschen DOS kann oft mehr als ein Haufen Fenster.

Gigabyte GA-586HX, P54C 100@75MHz, 24MB RAM, AVGA3-22-1M ISA, RTL8029AS PCI, Goldstar Prime 2 ISA, MA5ASOUND, Dreambl. X2 DB, HD 4x2GB, 48x CD, 3,5" Floppy, 2xRS232, 1xPar., PS/2 Maus
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von zatzen »

Hallo, Du kannst mir die Sachen per email schichen. Adresse ist zatzen bei gmail.
Bis dahin sehe ich mir schon einmal das Dateiformat und den Sourcecode an, so wie BT18 das liefert.
Den Beni-Tracker hatte ich auch schonmal ausprobiert, habe dann aber erstmal das Interesse verloren weil der bei mir nicht auf Anhieb funktionierte.
Read von Pascal habe ich bisher immer als Textlesefunktion verstanden, so wie man es auch nutzen kann um Tastatureingaben zu machen.
Wenn Du in Pascal große Datenfelder anlegst, ist irgendwann schnell das Datensegment voll (64 KB).
In Pascal kann man Datenfelder mehr oder weniger dynamisch anlegen, indem man entsprechend der benötigten Größe Speicher auf dem Heap reserviert und über einen Pointer adressiert. Um das ganze dann wie ein Datenfeld behandeln zu können muss man sich eine Struktur anlegen die auf den Pointer verweist. Ein konkretes Beispiel kann ich nachliefern, habe es leider gerade nicht vollständig im Kopf.
Jedenfalls, man könnte dann wenn man die Daten nicht mehr braucht den Heap-Speicher wieder freigeben, aber dann könnte man es wieder mit dem Problem der Heap-Fragmentieren zu tun bekommen.

Den Namen Allegro kenne ich nur von einen Notations-Programm her, was glaube ich die AdLib nutzt, aber kein Tracker ist.

32 Bit könnte auch einfach nur heissen, dass 32 Bit Befehle drin sind bzw. 32 Bit Register verwendet werden und es trotzdem im Realmode arbeitet.
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: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von zatzen »

Ich habe mir das Dateiformat und die Laderoutine einmal angesehen.
Ersteinmal fällt auf, dass die Patterngröße bei 192 Bytes liegt und die maximale Anzahl der Patterns bei 204.
Daraus ergibt sich, dass maximal < 40 KB geladen werden, und da frage ich mich, ob man das nicht einfach auf dem Heap unterbringen kann und gar keinen EMS bemühen muss, mit dessen Einrichtung einige Leute vielleicht Probleme haben. Ich habe vielleicht Vorurteile gegenüber BASIC, aber zu meiner Zeit konnte ich auf soetwas wie den Heap gar nicht zugreifen, und vielleicht wird daher hier direkt der EMS genommen.
Wieviele Spuren hat der Tracker? Ein Pattern mit 192 Byte kann ja nämlich nur für einen Kanal gelten. Ich habe ein Bild gesehen von dem Tracker, dort waren 5 Kanäle zu sehen, würde also 40 volle Patterns maximal ergeben, was im Grunde ja auch durchaus genügt für ein Musikstück.
Die fünf Parameter, Note, Instrument, Oktave, Effekt Hi+Lo sind in den Dateien auf 3 Byte zusammengepackt, das wird so wie ich das sehe im EMS dann auf 5 Byte ausgepackt. Es dürfte aber möglich sein, die Player Routinen entsprechend zu ändern dass sie die kompakten Daten vom Heap lesen und dabei "entwirren".

Naja, ich will Dir nicht zu sehr ins Handwerk pfuschen, aber eine EMS Nutzung nur wegen ein paar KB ist irgendwie übertrieben, wenn man es auch initial-dynamisch noch kompakt auf dem Heap halten könnte.

Ich werd mal in die Player-Routine sehen, da sieht man ja die Variablen und Speicherlesevorgänge die letztlich von Bedeutung sind.
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
Thomas
DOS-Kenner
Beiträge: 426
Registriert: Mi 22. Jun 2016, 12:29
Wohnort: Nähe von Limburg / Lahn

Re: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von Thomas »

Moin Zatzen.
Deine Theorie zu Basic und der EMS Nutzung klingt durchaus plausibel. In BASIC hat man ja sowas wie getmem nicht. Zwar kann man dynamische Arrays anlegen und auch Variablen Puffer mit 64kB (habe in den Anfängen Mal sowas gemacht als Double Buffer für den Bildschirm, praktisch ein 320x200 großer Sprite) aber nicht direkt auf dem heap ablegen. Ich denke er hat Ems verwendet weil das cooler war. Über DOS INT 21 kann man glaube ich auch 64k für die Patterndata auf dem Heap reservieren und genauso rein poken wie er das über den EMS pageframe macht. Die EMS Routinen machen aber auch nicht die Probleme. Vor dem eigentlichen laden der 5 Variablen die in den EMS kommen werden die ja per eraseptn Prozedur auf N=12, O=0, I=0, EH=0 und EL=0 gesetzt. Wenn ich mir diese anzeigen lasse, haben sie nach dem Laden immer noch diese Werte. Kann doch eigentlich nicht sein oder? Und wie ich ja schon sagte, die Effekte werden ja in Gänze ingnoriert. Wenn du sagst die Daten lägen kompakt in der Datei und werden dann im EMS "ausgepackt" liegt da vielleicht schon der Hund begraben? Irgendwo ein Rechenfehler drin oder so. Dass die Daten im EMS genauso "verpackt" sind wie in der Datei und daher nur Kauderwelsch heraus kommt?
Zu deinen Fragen zu dem Tracker: Nein, man sieht nur 5 Kanäle auf einmal, kann aber mit Tab switchen. Er hat die vollen 9 AdLib Kanäle.
Bei 40 Pattern bin ich gerade nicht sicher... Ich meine mein Stage Track hat mehr als 40 Pattern. Oder meinst du nur diejenigen Pattern die auch Verschieden sind? Also zB ich habe 10 Pattern die sich auch komplett unterscheiden auf 90 Positions durch Wiederholung verteilt.
Ich sehe Mal zu dass ich Dir heute Abend mal den Kram schicke. Willst Du dann auch die von mir geänderte Tracker Version haben, die mit 8 Waveforms und Stereo arbeitet? Die ist aber nur mit DirectQB lauffähig und ich mag das jetzt auf die Schnelle nicht zurück coden.
Das mit den "emulierten" dynamischen Arrays über Getmem und Pointer habe ich versucht aber nicht hinbekommen. Ich habe das bisher so gelöst daß ich die beiden Dyn Arrays in der LoadMod Prozedur statisch mit der maximalen Anzahl Pattern und Instrumente anlege. Vielleicht passt da etwas nicht.
Wenn ich jetzt Speicher reserviere, wie kann ich dann den erhaltenen Pointer mit Mem nutzen? Mem braucht Seg:Ofs. Ofs ist durch den Code ja vorgegeben. Aber ein Pointer enthält doch beides?
Ich habe mir erlesen dass man dem Pointer einfach den Wert zuweist der in den Speicher soll aber wenn ich jetzt ein Byte zuweisen, wird der Pointer dann auch automatisch um ein Byte weiter gesetzt? Oder funktioniert da auch sowas wie Inc (Pointer^)?
OK, gerade nochmal über Pointer informiert. Also Inc(Pointer) wäre möglich. Den Pointer vom Bytetyp deklariert, dann müsste er mit Inc ja um ein Byte versetzt werden. Was ich aber nicht heraus bekommen habe ist ob ich den Pointer mit Mem verwenden kann. Auslesen kann man sie mit Addr(Pointer) aber ich musste ja Seg und Ofs darauf anwenden. Oder denke ich hier nur zu kompliziert?
:???:

Ich habe da gerade so eine Idee:
Wenn ich einen array mit einem Eintrag erstelle, also Array[0..0] zB, dann mit GetMem (Pointer, 65000) ca 64k Speicher reserviere, dann könnte ich diesen Platz ja mit dem Array voll machen. Kann ich dann über den Pointer die Array Größe anpassen? Ich glaube nämlich das Problem mit der Ausgabe liegt tatsächlich bei dem Array wo die Patterndaten geladen werden. Das wird ja im Basic Code in der Laderoutine dynamisch angelegt von 0 - NumPattern - 1. Mein statisches Array ist aber jetzt viel Größer. Vielleicht liest er deswegen Mist aus dem Speicher wenn er abspielt.
Oder konkret an alle gefragt: wie kann ich das

Code: Alles auswählen

DIM PtnMap (0 TO NPtn -1)
in Pascal "emulieren" ?

Zu der Allegro Library kann ich auch noch nicht viel sagen. Aber den Allegro Sequenzer kenne ich auch. Der war von CPS. Hast Du auch Mal einen Highscreen Blaster oder CPS Soundblaster gehabt?

Naja, bis dahin erstmal.
Ein bisschen DOS kann oft mehr als ein Haufen Fenster.

Gigabyte GA-586HX, P54C 100@75MHz, 24MB RAM, AVGA3-22-1M ISA, RTL8029AS PCI, Goldstar Prime 2 ISA, MA5ASOUND, Dreambl. X2 DB, HD 4x2GB, 48x CD, 3,5" Floppy, 2xRS232, 1xPar., PS/2 Maus
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von zatzen »

Hallo!

EMS mag cool sein, finde ich aber total mit Kanonen auf Spatzen geschossen wenn da maximal 40 KB genutzt werden, sowas passt wie gesagt locker auf den Heap, zumal die Musikstücke ja meistens noch kleiner als 40 KB sind. Ich denke, wenn man das EMS-Feature rausnimmt, wird der Code auch nochmal ein Stück kompakter, was wiederum mehr Speicher für das Spiel erlaubt und beim Code selbst zur Übersicht beiträgt. Klar, dann haben wir die Patterndaten im Heap hängen, Jacke wie Hose sozusagen, aber eben keine eventuellen Probleme mit EMS. Bei mir stürzt der Tracker ab wenn ich ein Musikstück laden will, zudem hab ich auf die schnelle keinen Ton aus dem Ding bekommen.
Die Patterns sind bei diesem Tracker, im Gegensatz zu den meisten anderen, Kanal-weise gespeichert, und man kann sie flexibel kombinieren. Daher reichen dann die maximalen 203 Patterns auch für längere Stücke. Wie Du schon schreibst, 10 Patterns auf 90 Positions verteilt. Ziemlich schlaues Konzept eigentlich.
Wenn Du auch einen Sinn darin siehst, dass man auf EMS verzichtet und die < 40 KB auf dem Heap reserviert würde ich Dich gerne dabei unterstützen, den Code entsprechend umzugestalten.
Ich habe die Laderoutine in Pascal umgesetzt, so dass die Patterns auf den Heap geladen werden. Dabei habe ich nicht den BASIC Code 1:1 übersetzt sondern mich hineingedacht was da speichermäßig passiert und das in Pascal nachgebildet. Die Fehlerabfänge habe ich rausgelassen, weil diese nur zum Tragen kämen, wenn das Musikstück in irgendeiner Weise defekt oder jenseits der Definitionsgrenzen wäre.

Code: Alles auswählen

{$G+} { 286er ASM Anweisungen erlauben }

var
  length, nptns, ninstrs: byte;
  ptnmap: array[0..203] of byte;
  insmap: array[0..30] of byte;
{ die kleinen Arrays ptnmap und insmap sind hier im Datensegment, }
{ die größeren ord und die Patterns selbst auf dem Heap (s.u.) }

{ ptnmap und insmap werden scheinbar nur zum Laden gebraucht, }
{ ich bin mir gerade nur nicht sicher ob es nachteilig ist in }
{ einer Procedure Arrays zu definieren }

type instrument = record
  mul1, mul2, lev1, lev2, atd1, atd2, sur1, sur2, wav1, wav2, fbcon: byte;
end;
var
  ins: array[0..30] of instrument;

type ord_struc = array[0..255, 0..8] of byte;
{
 im Original v(0-8), p(0-255) - hier andersherum,
 da sonst eine Byte-für-Byte-Schleife beim Laden
 erforderlich wäre.
 Es dürfte kein Problem sein, die Variablenreihenfolge
 im Programm zu drehen.
}

var
  ord_pointer: pointer;
  ord: ^ord_struc absolute ord_pointer;
  { Zugriff: ord^[p, v] , siehe auch unten bei patn }

type encpatn = record
  hword: word;
  lbyte: byte;
end;
{
  Die drei Bytes die einen Noteneintrag bilden.
  Word als erstes ist praktikabel, da damit mit
  nur einer Variable Note, Oktave sowie Instrument
  gewonnen werden können, lbyte entspricht el.
}

type patn_struc = array[0..203, 0..63] of encpatn;
var
  patn_pointer: pointer;
  patn: ^patn_struc absolute patn_pointer;
  {
  Auf die Patterns, dessen Speicher ich hier auf dem Heap angelegt habe,
  kann mittels dieser auf einen Pointer zeigenden Stuktur
  so zugegriffen werden:

    patn^[(pattern-nummer), zeile].hword   oder -.lbyte

  Im Original werden die Patterns anders referenziert (modptr o.ä.),
  da bliebe aber sowieso noch einiges anzupassen.
  }
  patn_seg, patn_ofs: word;

procedure loadmod(filenm: string);
{ darf in dieser Form wegen den GetMem's nur einmal aufgerufen werden }
{ es sei denn man gibt den hier reservierten Speicher wieder frei (freemem) }
  var
    f: file;
    a, biggestptn: byte;
begin
  assign(f, filenm); reset(f, 1);

  { "Header" }
  blockread(f, length, 1); blockread(f, nptns, 1); blockread(f, ninstrs, 1);

  { Pattern / Instrument - Mapper }
  blockread(f, ptnmap[0], nptns);
  blockread(f, insmap[0], ninstrs);

  getmem(ord_pointer, word(length) * 9); { Sequencer auf Heap einrichten + laden }
  blockread(f, ord_pointer^, word(length) * 9);

  { Größte Pattern-Nummer aus dem Mapper beziehen }
  { und anhand dessen Speicher reservieren }
  biggestptn := 0; for a := 0 to nptns - 1 do
    if ptnmap[a] > biggestptn then biggestptn := ptnmap[a];
  getmem(patn_pointer, word(biggestptn+1) * 192);

  { seg und ofs für die Assembler Routinen ermitteln, die evtl. benutzt werden }
  patn_seg := seg(patn_pointer^); patn_ofs := ofs(patn_pointer^);

  { Patternspeicher nullen und Patterns gemäß Mapper auf den Heap laden }
  fillchar(patn_pointer^, word(biggestptn+1) * 192, 0);
  for a := 0 to nptns - 1 do blockread(f, patn^[ptnmap[a], 0].hword, 192);

  { Instrumente, Speicher nullen und gemäß Mapper laden }
  fillchar(ins[0], 31 * 11, 0);
  for a := 0 to ninstrs - 1 do blockread(f, ins[insmap[a]], 11);

  close(f);

  { Laden aus Datei erledigt, es bleiben noch Initialisierungen zu tun. }
end;


{ Beispiel, wie man über eine Assembler-Function direkt an die Patterndaten gelangt }
function readnote(ptn, row: word): byte; assembler;
asm

  { Folgende Taktangaben beziehen sich auf 486 }

  { Pattern-Offset berechnen (ptn * 192) }
  mov cx, ptn { 1 Takt }
  shl cx, 6 { 2 Takte } { -> * 64 }
  mov ax, cx { 1 Takt }
  add cx, cx { 1 Takt } { -> * 128 }
  add cx, ax { 1 Takt } [ -> * 192 }
                  { Summe: 6 Takte }
                  
  { Vergleich dazu Berechnung mittels MUL: }
  { mov ax, ptn   1 Takt }
  { mov bx, 192   1 Takt }
  { mul bl        13 - 18 Takte }
  {               Summe: 15 - 20 Takte }

  { Row-Offset berechnen }
  mov bx, row { 1 Takt }
  mov ax, bx  { 1 Takt }
  add bx, bx  { 1 Takt } { -> * 2 }
  add bx, ax  { 1 Takt } { -> * 3 }
              { Summe: 4 Takte }

  { Pattern-Offset zu Row-Offset addieren }
  add bx, cx { 1 Takt }  { bx enthält nun korrekten Offset }

  { Pointer einlesen }
  mov es, patn_seg { 1 Takt }
  mov si, patn_ofs { 1 Takt }
  { les si, patn_pointer ginge auch, braucht aber 6 Takte }

  { Byte lesen und Notenwert gewinnen }
  mov al, es:[si+bx]   { 1 Takt }
  shr al, 4 { 2 Takte }

  { Funktion gibt den Wert in AL zurück }

  { Gesamte Function also 16 Takte, plus ggf. Eintritts/Austritts-Vorgänge }

end;

begin
end.
So, das war jetzt etwas viel auf einem Haufen, aber es könnte ein paar Deiner Fragen klären.
Zuerst noch zu der Assembler-Routine: Die besteht ja zu fast 70% aus der Berechnung des Offsets in die Patterndaten.
Man kann es auch in Pascal halten:

Code: Alles auswählen

function readnote(ptn, row: byte): byte;
begin
  readnote := patn^[ptn, row].hword shr 12;
end;
Zugegebenermaßen - bedeutend kürzer und wahrscheinlich auch schneller. Ich hatte nur mal Lust auf ein bisschen Assemblergefummel.

Wenn man Speicher auf einen Pointer reserviert kann man mit mem[seg(pvar^):ofs(pvar^)+x] darauf zugreifen, bequemer ist aber wie in meinem obigen Code die Definition einer Datenstruktur mittels Type, und dann eine Variable von diesem Typ die man mit ABSOLUTE auf den Pointer legt. Ich muss da bislang auch immer noch nachgucken wie das genau geht, aber es steht ja oben im Code.
Ein Pointer ist eine 32 Bit Variable und enthält Segment und Offset, ja.
Meines Wissens zeigt ein Pointer immer fest auf einen Speicherort, und "Pointer^" zeigt auf das erste Element des zugewiesenen Speichers. Siehe in meiner Laderoutine, da wird Blockread oder Fillchar auf "Pointer^" angesetzt, wie auf Variablen oder Arrays. Ich kann es gerade nicht prüfen, aber addr(Pointer) ist denke ich nicht das gleiche wie Pointer^, addr liefert die Adresse einer Variablen oder Routine, also wiederum einen Pointer, wenn ich nicht irre (sorry für die Unklarheit).
Zu Deiner Idee mit dem Array: Das wird auch im Code oben gemacht. Es werden soundsoviel Bytes reserviert auf einen Pointer, auf die dann mit einer Array-Struktur zugegriffen werden kann in der Form hier ord^[p, v]. Zugriff wie auf ein normales Array, nur mit ^ zwischen Name und Klammern.
Thomas hat geschrieben:

Code: Alles auswählen

DIM PtnMap (0 TO NPtn -1)
in Pascal "emulieren" ?
Das sind maximal 203 Byte, daher habe ich es statisch angelegt, selbst wenn es nur einmal in der Laderoutine gebraucht wird. Man könnte es auch über eine Datenstruktur und Pointer und GetMem einrichten und dann mit FreeMem wieder lösen, aber da muss man den Aufwand abwägen, ob der sich lohnt um 203 "Leichenbytes" zu vermeiden.
Auf der anderen Seite ist im Originalcode "ord" statisch definiert mit insgesamt 9 * 255 Bytes, ich richte es nur so groß wie nötig ein. Type Definitionen belegen keinen (nennenswerten) Platz, man könnte auch ein solch als Type definiertes Array 65000 Byte groß machen, auch wenn man nur 255 nutzen will.

Nein ich habe den Soundblaster Pro gehabt, dann eine GUS, dann Soundblaster 64 Gold wenn ich mich recht entsinne.

Bei dem Tracker an sich interessiert mich im Moment eigentlich nur, die Lade- und Abspielroutinen nach Pascal zu portieren, und wenn Du einverstanden bist, ohne EMS. Mit DirectQB hatte ich bisher nicht zu tun...
Es wäre ganz nett, wenn auch ich am Ende eine Unit hätte die PIS abspielen kann.
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
Thomas
DOS-Kenner
Beiträge: 426
Registriert: Mi 22. Jun 2016, 12:29
Wohnort: Nähe von Limburg / Lahn

Re: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von Thomas »

Uff, ziemlich harter Stoff für den Anfang... :???:
Im großen und ganzen verstehe ich halbwegs, was du da tust. Aber das ganze Pointer Gedöns ist noch ziemliches Neuland.

Zum Tracker selbst: Hast Du den in Quickbasic 4.5 kompiliert? In QBasic läuft der nicht wegen der Int Calls. Obwohl, da meckert das ja nur wegen unbekannter Bezeichner oder so. Bei mir lief der immer auf Anhieb. Außer in Dosbox. Da läuft nur mein auf DirectQB EMS umgemünzter Player.
Mit EMS bin ich nicht verheiratet. Aber die zwanzig Zeilchen Code einzusparen bringt sicher nicht die 5 - 16 KB Ersparnis der Songdaten. Habe halt immer Angst, sollte doch noch mal ein Spiel zusammen kommen, das es wieder nur für einen Level reicht (Speichermäßig). Obwohl mich da die paar kB auch nicht rausreißen. Ich benutze den halt gern weil ich den recht einfach finde und sich der Player leicht an DirectQB anpassen lies. Damals war ich programmiertechnisch ja noch nicht viel über null hinaus.

Zu den dyn arrays: Ja, da hatte ich einen Denkfehler drin. Ich dachte wenn ich den statisch definiere, dann wird alles was nach hinten leer ist, mit eingelesen, was aber völliger Quatsch ist. Die FOR Schleife ist ja durch nptns - 1 bzw. Ninstr - 1 begrenzt. Wie Du schon sagtest, unnütz das dynamisch anzulegen bei solch einem kleinen Programm und den paar bytes für Pattern und Instrumente. Selbst in Basic.

Ok, also Pointer sind kompatibel zu Seg und Ofs und die beiden liefern dann die Adressteile wohin der Pointer zeigt und nicht die Adresse wo der Pointer selbst liegt? Ach so, das bestimme ich ja ob ich ihn mit oder ohne Zirkumflex angebe, ja.
Aber irgendwie fehlt mir die Fähigkeit deinen Ladecode auf den Rest des Trackers zu abstrahieren.
Wo müsste ich jetzt in diesem fiesen IF THEN ELSE und CASE OF Wirrwar wie ansetzen um die Pointer auszulesen und durch den OPL chip zu jagen?
Ich glaube ich habe mir da etwas zu hohe Ziele gesteckt...

Zu Get- / Freemem: Ich stimme mit Dir völlig überein das 203 Leichenbytes nicht der rede wert sind aber ich möchte mir angewöhnen, nicht mehr benötigten Speicher auch gleich wieder frei zu geben. Vorallem wenn er innerhalb einer Prozedur/Funktion wirklich nur einmal benötigt wird.
loadMOD wird nur einmal ausgeführt. Klar, wenn man im laufenden Programm das MOD wechseln möchte, muss man die Routine schon öfter ausführen. Weist Du wie Pascal das händelt? Wird reservierter Heap bei normalem Programmende automatisch freigegeben wenn kein FreeMem gesetzt wurde? Ich frage deswegen weil ich denke dass GetMem den DOS Int 21 nutzt und wenn man Speicher von DOS angefordert hat, ist der verloren wenn man ihn nicht mehr freigibt.

Wegen der Soundkarten habe ich nur gemutmaßt. Mir ist aber wieder eingefallen dass Allegro Einzelsoftware von CPS war und nur die Demo bei deren Soundkarten bei war.

Klar fände ich es super wenn Du mich unterstützt, auch ohne EMS. Mir geht es bei dem Ganzen ja mehr um den Lerneffekt und um den Geist fit zu halten.
Wie ist denn dein letzter Satz gemeint? Wenn wir zusammen daran coden, wobei ich denke dass du eher den Großteil stemmst, hast du am ende doch eine PIS Unit?! Und wenn es so läuft dass Du mir Tipps gibst, ist doch selbstverständlich dass ich dir die Ergebnisse zukommen lasse.

P.S.: Eine Kleinigkeit die mir in deiner Laderoutine aufgefallen ist:
Ord kann man nicht als Variablenbezeichner nehmen. Ord ist eine Funktion zur Ausgabe von Ordinalzahlen.
OK, etwas klugscheißerisch jetzt aber ich dachte ich weise Mal darauf hin.
Ein bisschen DOS kann oft mehr als ein Haufen Fenster.

Gigabyte GA-586HX, P54C 100@75MHz, 24MB RAM, AVGA3-22-1M ISA, RTL8029AS PCI, Goldstar Prime 2 ISA, MA5ASOUND, Dreambl. X2 DB, HD 4x2GB, 48x CD, 3,5" Floppy, 2xRS232, 1xPar., PS/2 Maus
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von zatzen »

Ersteinmal vorne weg, weil mir das keine Ruhe gelassen hat (wird für Dich vielleicht auch später interessant) - meine Assembler-Function ist wohl doch schneller als der Pascal-Einzeiler. Ich habe mir im Code mit ASM db '....' END Markierungen gesetzt und das Programm kompiliert. Im Hex-Editor zeigt sich, dass die eine Pascal Zeile 39 Maschinencode-Bytes erzeugt, meine Assembler function liegt mit 37 Bytes knapp drunter, zudem haben fast alle meine Befehle nur einen Takt.
Hier das Disassemblat von der Programmzeile "readnote := patn^[ptn, row].hword shr 12":

Code: Alles auswählen

0x0000000000000000:  8A 46 04       mov  al, byte ptr [bp + 4]	// 1 Takt
0x0000000000000003:  30 E4          xor  ah, ah	// 1 Takt
0x0000000000000005:  8B F0          mov  si, ax	// 1 Takt
0x0000000000000007:  D1 E0          shl  ax, 1	// 3 Takte
0x0000000000000009:  01 F0          add  ax, si	// 1 Takt
0x000000000000000b:  8B D0          mov  dx, ax	// 1 Takt
0x000000000000000d:  8A 46 06       mov  al, byte ptr [bp + 6]	// 1 Takt
0x0000000000000010:  30 E4          xor  ah, ah	// 1 Takt
0x0000000000000012:  69 C0 C0 00    imul ax, ax, 0xc0	// 13 - 26 Takte
0x0000000000000016:  C4 3E 50 00    les  di, ptr [0x50]	// 6 Takte
0x000000000000001a:  03 F8          add  di, ax	// 1 Takt
0x000000000000001c:  03 FA          add  di, dx	// 1 Takt
0x000000000000001e:  26 8B 05       mov  ax, word ptr es:[di]	// 1 Takt
0x0000000000000021:  C1 E8 0C       shr  ax, 0xc	// 2 Takte
0x0000000000000024:  88 46 FF       mov  byte ptr [bp - 1], al	// 1 Takt

Takte insgesamt: 35 - 48
Meine Function (16 Takte) ist also mehr als doppelt so schnell wie der Zugriff auf das Datenfeld in Pascal-Manier, und vielleicht kann man sie auch noch weiter optimieren. Vielleicht erscheint Dir das momentan nicht so interesssant, aber es ist immer gut wenn man oft aufgerufene Funktionen schnell hält.
An dieser Stelle noch Grüße von DOSferatu, der liest hier schonmal mit und freut sich dass es noch mehr Dos-Coder gibt die in Dos hardwarenah Spiele machen wollen. Er könnte hier auch gut helfen, wollte sich aber nicht einfach so einmischen.
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
Thomas
DOS-Kenner
Beiträge: 426
Registriert: Mi 22. Jun 2016, 12:29
Wohnort: Nähe von Limburg / Lahn

Re: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von Thomas »

Doch, finde das sehr interessant und hatte ich auch ehrlich gesagt vermutet. Ich habe auch gedacht, was machst Du hier eigentlich, wenn ich kurze, knappen Pascal Funktionen zu Spass und zum lernen in ASM geschrieben habe. ASM ist doppelt so lang oder länger und das soll schneller sein? Doch, ist es aber. Das wurde mir hier nocheinmal von dir bestätigt.
Im PrisM Thread lese ich auch mit und staune regelrecht. Dass dosferatu sich einmischt, na da bitte ich doch drum. Ich war ja auch schon geneigt ihn direkt zu fragen, habe mich aber dann doch nicht so recht getraut...
Aber ich fürchte, bis ich in Pascal das hinbekomme was ich mithilfe einer Spielbibliothek und Quickbasic geschafft habe, ist es noch ein steiniger und seeeehr langer weg.
Ein bisschen DOS kann oft mehr als ein Haufen Fenster.

Gigabyte GA-586HX, P54C 100@75MHz, 24MB RAM, AVGA3-22-1M ISA, RTL8029AS PCI, Goldstar Prime 2 ISA, MA5ASOUND, Dreambl. X2 DB, HD 4x2GB, 48x CD, 3,5" Floppy, 2xRS232, 1xPar., PS/2 Maus
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von zatzen »

Ich kann nachvollziehen wenn man erstmal nicht versteht was Pointer sind. Ich habe mal halbherzig C gelernt, da war ständig was mit Pointern und ich habe nie wirklich verstanden was das soll.
Eigentlich ist es aber ganz einfach.
Um auf die 640 K Speicher zuzugreifen bedient man sich im Realmode Segmenten und Offsets. Man kann es sich so vorstellen dass die Segmente 16 Byte-weise gerastert sind, d.h. um auf Speicherstelle 16000 zuzugreifen muss das Segment, wenn Offset 0 sein soll, 1000 betragen. Maximal kann so bis zu 1 MB Speicher adressiert werden, weil 16 * 65536 = 1048576. Bei MCGA beginnt der Grafikspeicher bei Segment $A000, was linear auf die Speicherstelle $A0000 weist, was in Dezimal 655360 entspricht, vielleicht ganz interesssant. Aber vielleicht kann das jemand anders das ganze noch besser und ausführlicher erklären.
Jedenfalls, Speicher-Adressen bestehen im Realmode immer aus Segment und Offset, das dürfte ja auch Dir geläufig sein, und ein Pointer ist einfach eine Variable die aus Segment und Offset besteht, also zwei Words, somit zusammen 32 Bit.
Man kann aus einem Pointer jederzeit mit seg(poivar^) und ofs(poivar^) Segment und Offset extrahieren.
GetMem sucht sich einfach auf dem Heap einen freien Speicherbereich der angeforderten Größe und schreibt die Anfangsadresse dieses Bereichs in einen Pointer. Freemem braucht den Pointer und die reservierte Größe, um den Speicherbereich wieder freizugeben. Somit sind dynamische oder temporäre Arrays machbar, wenn auch händisch.
Man kann sich aber für so gut wie alles Routinen basteln.
Wie man über eine Array-Struktur auf einen mittels GetMem reservierten Speicherbereich zugreifen kann habe ich ja im Code vom vorletzten Post erklärt, und das steht auch nochmal unten hier als Code.

Ich verwende einen Leistungsstarken 64 Bit Rechner weil ich viel mit Musikproduktion zu tun habe. Und seit mein Pentium 200 MMX den Geist aufgegeben hat verwende ich DosBox für Dosprogramme. Nicht so toll, es kann allerdings auch Vorteile beim Assembler-Coding haben, dann muss ich bei einem Absturz nur die Dosbox neu starten.
So habe ich den Beni Tracker auch in DosBox laufen lassen, und zwar die beiliegende kompilierte EXE.
Was EMS angeht - das musst Du entscheiden. Aber ich würde es als Optimierung ansehen darauf zu verzichten. Und ich erinnere mich noch dass ich zu "echten" Dos-Zeiten eine Bootdiskette für Spiele brauchte die EMS wollten. XMS und EMS gleichzeitig ging nicht, oder irgendwas war da.

Genau, seg(poivar) liefert das Segment in dem poivar liegt, seg(poivar^) liefert das Segment auf das poivar zeigt.
Es sind alles nur Speicherbereiche, Bytes, Nullen und Einsen. Ein Variablentyp ist sozusagen nur die Verpackung die angibt, wie die Bytes interpretiert werden sollen. Deswegen geht ja auch sowas wie if boolean(bytevar) then ...
Ein Pointer ist einfach nur: xxyy wobei xx die beiden Bytes für das Segment sind und yy die beiden fürs Offset.

Basic hat eine etwas andere Syntax als Pascal, deshalb ist es bei einer Portierung wichtig dass man nicht versucht, den Code 1:1 umzusetzen sondern vielmehr zu sehen, was Basic so alles im Speicher "anrichtet" und das eben dann mit Pascal nachbasteln. Also weniger den Code sehen, sondern vielmehr das Geschehen im Speicher (und Variablen sind auch nur Speicherbereiche). Ich kann da gerne helfen, ich weiss nur nicht inwieweit Du die Portierung selbst machen willst, ob wir zusehen dass wir das zusammen stemmen, bis hin dass ich es vielleicht letztlich alles mache, oder umgekehrt, ich versuche alles, und lasse mir bei Problemen von Dir helfen. Die 16 KB Codelisting finde ich nicht allzu abschreckend.
Thomas hat geschrieben: Zu Get- / Freemem: Ich stimme mit Dir völlig überein das 203 Leichenbytes nicht der rede wert sind aber ich möchte mir angewöhnen, nicht mehr benötigten Speicher auch gleich wieder frei zu geben. Vorallem wenn er innerhalb einer Prozedur/Funktion wirklich nur einmal benötigt wird.
Du könntest Dir für solche Zwecke Allzweck-"Schmutz"-Variablen deklarieren, z.B.:

Code: Alles auswählen

type tmp_bytestruc = array[0..65527] of byte;
type tmp_wordstruc = array[0..32763] of word;
type tmp_intstruc = array[0..32763] of integer;
var
  tmp_poi: pointer;
  tmpbyte: ^tmp_bytestruc absolute tmp_poi;
  tmpword: ^tmp_wordstruc absolute tmp_poi;
  tmpint: ^tmp_intstruc absolute tmp_poi;
Beispiel für Speicher reservieren, Zugriff und Speicher freigeben:

Code: Alles auswählen

  getmem(tmp_poi, 256);
  tmpbyte^[123]  := 12;

  {oder auch Integer-strukturiert:}
  tmpint^[99] := -4711;
  
  freemem(tmp_poi, 256);
Man muss nur ein bisschen mitdenken - GetMem reserviert immer Bytes - wenn man 1000 Integer oder Word-Elemente hat muss man natürlich 2000 Bytes reservieren. Maximal sind 65528 möglich, 8 weniger als 65536.
Man muss sich diese Sache mit den Variablen wie tmpbyte: ^tmp_bytestruc absolute tmp_poi vorstellen wie eine Struktur die über den Speicher gelegt wird. Man kann hier auch gleichzeitig über tmpbyte wie auch tmpword oder tmpint darauf zugreifen, die Daten werden nur entsprechend als Byte, Word oder Integer interpretiert und die Indizes sind bei 16 Bit Strukturen gegenüber 8 Bit halbiert, verstehst Du was ich meine?

Doch, nach Programmende ist alles aus dem Speicher raus, darum muss man sich nicht kümmern.

Was das Nachladen von Musikstücken angeht - vielleicht ist es möglich, die komplette Musik für ein Spiel in einem Modul zu halten und dann je nach Level an bestimmte Order-Positionen zu springen.

Okay, also nochmal zu dem Punkt wieviel Du und ich jeweils an der Portierung arbeiten: Es wäre natürlich wohl aus einer Hand irgendwie runder. Andererseits macht es auch Sinn, wenn ich mich jeweils nur auf bestimmte Teile konzentriere wie solche Pattern-Note-Leseroutinen. Aber sehen wir mal.
Ja stimmt, dann habe ich eine PIS Unit. Vielleicht war ich mir da irgendwie nur unterbewusst unsicher, weil ich aus dem Tracker bislang keinen Pieps rausbekommen habe und bisher nur Code geschrieben habe ohne das Programm laufen zu lassen (allerdings schon kompilieren, um Syntaxfehler o.ä. auszuschliessen).

Ord: Stimmt, komisch dass mein Compiler nicht meckert. Muss ich mir nochmal ansehen.
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
Thomas
DOS-Kenner
Beiträge: 426
Registriert: Mi 22. Jun 2016, 12:29
Wohnort: Nähe von Limburg / Lahn

Re: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von Thomas »

Moin Zatzen.
Da habe ich mich wohl etwas schwammig ausgedrückt. Der Speicheraufbau im Realmode ist mir vollkommen klar, auch was ein Pointer ist und was er tut. Lediglich wie man ihn jetzt speziell in Pascal einsetzt noch nicht so ganz. In Basic bekommt man das ja alles abgenommen. Da arbeitet man ja nur absolut, wenn ich das richtig sehe. Mit DEF SEG das gewünschte Segment festlegen und mit POKE einen Wert an das Offset schreiben.
Aber ich denke ich komme so langsam dahinter.
Nur mit der Segmentgröße könntest Du mir nochmal erläutern: Du schreibst hier von 16K und Deine Rechnung stimmt ja auch aber ist ein Segment nicht 64K groß?
Direkte zugriffe auf den Grafikspeicher im $A000 Fenster habe ich ja selber schon gemacht, auch eigene PRINT (Write) Routinen mit direktem Zugriff auf B800 nichts neues.

Zu Deinen Beispielen unten, damit ich das richtig verstanden habe:
Zuerst lege ich einen Array Typ an mit den maximalen, oder von mir gewünschten Einträgen an. Dann einen typlosen Pointer und die verschiedenen Typvariablen entsprechend meiner Arraytypen und verknüpfe sie "absolute" mit dem Pointer.

Mit GetMem hole ich mir über meinen Pointer den benötigten Speicher. Auch klar soweit. Bei

Code: Alles auswählen

 tmpbyte^[123]  := 12; 
kommt der Index aus dem Arrypool von oben, also aus der Structure, könnte also maximal 65527 annehmen in der ByteStruct. Wobei es bei Byte ja noch recht simpel wäre, da Wäre der Index ohne TypeArray ja einfach die Menge an Bytes die ich reserviere und jeder Index ein Byte. By Integer das Doppelte, Long das Vierfache, etc... Aber ich denke so händisch verliert man da schnell den Überblick.
OK, und wie ist das dann bei mehrdimensionalen Arrays? Genauso? Wo beginnt da der zweite Index? Oder geht das gar nicht, müsste man sich mit zwei eindimensionalen Arrays und zwei Pointern behelfen?
Keine Angst, Ich will jetzt nicht gleich Bäume ausreißen, nur sicher sein dass ich es verstanden habe.
Die 8 weniger als 65536 rühren von der Pointergröße oder? Ach nee, der hat ja nur zwei Words, also 2x2 Byte...

Wegen der Musikstücke: Im gesamten Spiel ist es sicher nicht von Nöten ein großes Mod zu machen. Bei mir habe ich das so gelöst:
Intro: Initialisierung Player, MOD laden, MOD spielen, MOD stoppen, Player schließen. Genauso im Titel, Level und Schluß. Lediglich im Level habe ich drei verschiedene Stücke. Und das mache ich genau so wie du vorschlägst: Boss kommt, Patternsprung zur Bossmusik, Boss gekillt, Patternsprung zur Siegesfanfare. Und jeder der drei Teile hat natürlich einen entsprechenden Patternbreak zu seinem Loop Anfang.

Zum Benitracker: Da sit die Lage sonnenklar. Der läuft nicht in DosBox. Das Problem hatte ich zu Anfang. Weder Tracker noch Player liefen in DOS Box (Player hing sich einfach auf und Tracker schrieb "Something dreadful happened".)
Da aber der Player sich auch nicht mit DirectQB vertrug, und ich auf die EMS Routine tippte (Ja, ja, ich weis!) habe ich die im Player ja auf die Poke/Peek Funktionen von DQB umgeschrieben (easy) sodass direkt der von DQB bereitgestellte EMS genutzt wurde. Keine Probleme mehr mit der Musik und der Player lief auch in Dosbox! Mit dem Tracker habe ich das immer vorgehabt aber nie gemacht...

Wegen Ord: Ja, hat bei mir auch nicht gemeckert, vermutlich weil der Eintrag in den Klammern zufällig ein ordinaler Wert war. Ist mir beim Suchen zufällig aufgefallen. Dacht so, Ord... = Order? Moment, Ord ist doch eine Pascal Funktion und dann habe ich einfach Ordr draus gemacht :-D
Ein bisschen DOS kann oft mehr als ein Haufen Fenster.

Gigabyte GA-586HX, P54C 100@75MHz, 24MB RAM, AVGA3-22-1M ISA, RTL8029AS PCI, Goldstar Prime 2 ISA, MA5ASOUND, Dreambl. X2 DB, HD 4x2GB, 48x CD, 3,5" Floppy, 2xRS232, 1xPar., PS/2 Maus
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von zatzen »

Ersteinmal damit Du Dich nicht wunderst, warum ich hier so engagiert mitarbeite: Mir macht das Spaß, und wenn ich zusehe, möglichst optimale Lösungen zu finden lerne ich auch noch neues dazu, und Du lernst mehr über Pascal - also win-win sozusagen. So habe ich auch nochmal geprüft ob folgendes funktioniert:

Code: Alles auswählen

var
  a, b, c, d: byte;
  li: longint absolute a;
begin
  li := $04030201;
  writeln(a, b, c, d);
end.
Tatsächlich wird "1 2 3 4" ausgegeben, d..h. der Longint "sitzt" auf dem Speicherbereich der vier Byte-Variablen.
Gleiches passiert bei

Code: Alles auswählen

var
  a, b, c, d: byte;
  li: longint;
begin
  li := $04030201;
  move(li, a, 4); { Speicher von li nach a kopieren, und zwar 4 Bytes }
  writeln(a, b, c, d);
end.
Bei Absolute oder Move bzw. Fillchar ist es Pascal völlig egal von welchem Typ die Variablen sind, es wird einfach nur Speicher zugewiesen bzw. kopiert, sozusagen auf volle Verantwortung des Programmierers.
Bei QB hatte ich immer das Gefühl, die Variablen und Arrays wären Dateninseln. Erst durch Pascal habe ich verstanden, dass man auf den gesamten Speicher beliebig zugreifen kann, wenn man nur ein paar Anker setzt.

Die Größe eines Segments wird eher dadurch bestimmt, dass der Offset-Anteil aus 16 Bit besteht. Daher ist also sozusagen ein Segment 64 KB groß. Aber wenn man den Segmentanteil um 1 erhöht, erhöht man die Adresse linear um 16 Byte. Aus gewissen Gründen wurde das so etwas vermeintlich kompliziert gelöst, mit Segment und Offset eben auf diese Weise - im Protected Mode bzw. bei 32 Bit ist das nicht mehr nötig, ich kenne mich da zwar nicht aus, kann aber nur mutmaßen dass es dort etwas anders struktuiert ist, da man allein mit dem Offset-Anteil 4 GB adressieren könnte.
Thomas hat geschrieben: Wobei es bei Byte ja noch recht simpel wäre, da Wäre der Index ohne TypeArray ja einfach die Menge an Bytes die ich reserviere und jeder Index ein Byte. By Integer das Doppelte, Long das Vierfache, etc... Aber ich denke so händisch verliert man da schnell den Überblick.
Ja, man muss da etwas hardwarenaher mitdenken, was dann aber eigentlich zum besseren Verständnis der Programmabläufe beiträgt. Du kannst Dir aber auch Routinen schreiben:

Code: Alles auswählen

procedure get_tmparr(elements, bytes_p_el: word);
begin
  tmp_membytes := elements * bytes_p_el; { globale Word-Variable tmp_membytes }
  getmem(tmp_poi, tmp_membytes);
end;

procedure free_tmparr;
begin
  freemem(tmp_poi, tmp_membytes);
end;
Und um es noch "menschlicher" zu machen, kannst Du Dir Konstanten definieren, z.b. _longint = 4 und das als bytes_p_el übergeben, aber sollte ja klar sein.

Bei mehrdimensionalen Arrays ist das genauso. Ist alles genau wie mit normalen Arrays, im Schriftbild kommt zwischen Array-Name und der Klammer einfach nur ^. Ein Array mit [0..15, 0..3] legt sich linear so aus, dass erst immer der hintere Teil hochgezählt wird, also vier Elemente, bevor der vordere Teil eins weiter geht. Also Speicherauslegung:
[0]0,1,2,3[1]0,1,2,3...

Dass man in Pascal nur 65528 Bytes mit Getmem reservieren kann ist eigentlich blöd, ich glaube es hat damit zu tun dass man nicht immer davon ausgehen kann dass jeder freie Bereich mit Offset = 0 beginnt. Ich glaube DOSferatu hat hier mehr Ahnung.


Ich habe mir einmal die Play-Routine von BT angesehen. Der Player nutzt Puffer-Arrays, und zwar für ein gesamtes Pattern mit 9 Kanälen. Es sind 5 Arrays, das mal 64 und mal 9, und für Basic noch Faktor 2 wegen Integer, das ergibt 5760 Bytes, in Pascal 2880, schon eher ein ganz schöner Happen, würde ich auch erstmal auf den Heap legen - und später, wenn wir alles in Pascal stehen haben, das Programm umbauen dass die Patterns ohne Umweg des Puffers geladen werden. Wenn ich das richtig gesehen habe wird dieser Puffer nämlich immer schlagartig in einer Schleife gefüllt wenn ein neues Pattern anfängt. In einem 50 Hz Interrupt stellenweise mehr als 2 KB zusammenzufummeln ist nicht unbedingt so ideal, das könnte evtl. auf weniger potenten Systemen zu Hängern führen.

Aber wie gesagt, ersteinmal wäre ich dafür dass wir das mit Pattern-Puffer machen. Die Daten dort hineinzulesen, aus den 3-Byte-codierten Patterndaten heraus ist überhaupt kein Problem, dafür schreibe ich gerne eine Assembler-Routine.
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
Thomas
DOS-Kenner
Beiträge: 426
Registriert: Mi 22. Jun 2016, 12:29
Wohnort: Nähe von Limburg / Lahn

Re: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von Thomas »

Hi.
Naja, so "menschlich" dass ich die Typwerte als Konstanten speichern müsste brauch ich es dann auch nicht.
Aber dass erst die hinteren Array Indices geschrieben werden fällt mir jetzt auch wieder ein, genau wie Variablendeklarationen in einer Zeile. Pascal speichert von rechts nach links oder so ähnlich.

Deinen Ausführungenen zum Player kann ich nicht zu 100% folgen. Die Rechnung, klar. Integer doppelt, auch klar aber der Speicher wird doch Byteweise beschrieben
(POKE OFS = EH: OFS = OFS + 1) also belegt doch nur der letzte Patterneintrag, Zeile 64, Kanal 9, Low FX Byte, 2 Bytes. Das zweite Integer Byte wird doch direkt vom darauffolgenden mit dessen erstem Byte überschrieben? Kann auch sein dass ich da wieder verkehrt denke. Abstrakt eben.

Das mit dem Buffer und den Verzögerungen leuchtet mir ein. Erschwerend kommt hinzu dass EMS ja auch noch etwas langsamer ist. Konkret habe ich das bei meinem Spiel bemerkt auf dem P133 wenn ich statt EMM386 QuEMM benutzt habe und auf dem 486 merke ich das auch mit EMM386: Nach jedem Pattern gibt es einen Minihänger.
OK, legen wir es alles zunächst im Heap in einem Buffer ab. Ich denke ja genauso, das man Optimierungen und Erweiterungen dann ergänzen kann, wenn Mal alles läuft. Daher dachte ich ja auch, ich übersetze erstmal "einfach" nur den Basic Code und nehme dazu auch erstmal nur den Ur-Player und nicht gleich den mit meinen, geringen Verbesserungen. Die nachträglich zu implementieren ist ein Kinderspiel.
Erklär mir doch bitte Mal genauer das mit dem
...dass die Patterns ohne Umweg des Puffers geladen werden.
Meinst Du damit, das das gesamte MOD im Heap liegt und dann Note für Note in Echtzeit geladen und gespielt wird? Mir ist da noch nicht so ganz klar was er da mit dem Puffer macht. So wie ich den Code interpretiert habe, legt er den gesamten Song im EMS ab und holt sich Zeile für Zeile da raus was er braucht, in einer 9er For Schleife für alle Kanäle, wie Du schon sagtest.
Ein bisschen DOS kann oft mehr als ein Haufen Fenster.

Gigabyte GA-586HX, P54C 100@75MHz, 24MB RAM, AVGA3-22-1M ISA, RTL8029AS PCI, Goldstar Prime 2 ISA, MA5ASOUND, Dreambl. X2 DB, HD 4x2GB, 48x CD, 3,5" Floppy, 2xRS232, 1xPar., PS/2 Maus
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von zatzen »

Hallo!

Wie Daten im Speicher abgelegt werden hängt auch vom Endian-Typ ab, also little oder big. Bei Intel oder PCs eben ist es Little Endian, d.h. erst Lowbyte, dann Highbyte bzw. die niederwertigsten Bytes zuerst, danach folgend höherwertige.
Es verwirrt mich noch etwas im Zusammenhang mit Arrays, man muss die ASM Routinen (unten) noch prüfen ob sie so richtig funktionieren.

Ich habe mich vorhin näher mit dem Code auseinandergesetzt, und es ist so, dass der Player sich tatsächlich zwischendurch immer ein volles 64 Zeilen, 9 Kanal Pattern entsprechend Ord aus dem EMS holt und in die Puffer-Arrays schreibt. Deshalb auch der Mini-Hänger beim Patternwechsel. POKE kommt im Code nur 10x vor und schreibt nur in die EMS Page. Die Patterns liegen eben ungeordnet aber komplett im EMS. Zum Abspielen braucht er aber nach Ord geordnete Patterns, die ja Trackweise zusammengestellt werden. Um nicht zu viel in der Gegend herumzureferenzieren oder weil man nicht so einfach auf den EMS zugreifen kann, legt sich der Programmierer hier eben nach Ende jedes Patterns das nächste komplett zurecht in den Puffer und liest dann bequem aus diesem.

Ich wollte es auch erst so nachbauen, habe dann aber gesehen dass es ziemlich einfach ist, auch direkt aus den codierten Patterns nach Ord-Aufbau zu lesen.

Ersteinmal die Parameter-Leseroutinen in Assembler:

Code: Alles auswählen

{$G+}

  patn_pointer: pointer;
  { wir brauchen nur noch den Pointer für die Patterndaten, }
  { da nur über die ASM Routinen zugegriffen wird }

  patn_seg, patn_ofs: word;
  { werden beim Laden gesetzt nachdem der Speicher reserviert wurde }
  { macht in den ASM Routinen die Umgehung vom LES-Befehl möglich }


  patn_offset: word; { wird in der Abspielroutine anhand Ordr bestimmt }


function readnote(row: word): byte; assembler;
asm
  mov bx, patn_offset
  mov ax, row
  add bx, ax
  add bx, ax
  add bx, ax { 3x row-offset addieren da jede row 3 Bytes hat }
  mov es, patn_seg
  mov si, patn_ofs
  mov al, es:[si+bx]
  shr al, 4
end;

{ folgende kompakt gehalten, es ändert sich nur in der }
{ letzten Zeile jeweils etwas }
function readoct(row: word): byte; assembler;
asm
  mov bx, patn_offset; mov ax, row
  add bx, ax; add bx, ax; add bx, ax
  mov es, patn_seg; mov si, patn_ofs
  mov al, es:[si+bx]; shr al, 1; and al, 7
end;

function readins(row: word): byte; assembler;
asm
  mov bx, patn_offset; mov ax, row
  add bx, ax; add bx, ax; add bx, ax
  mov es, patn_seg; mov si, patn_ofs
  mov ax, es:[si+bx]; shr ax, 4; and al, 31
end;

function read_eh(row: word): byte; assembler;
asm
  mov bx, patn_offset; mov ax, row
  add bx, ax; add bx, ax; add bx, ax
  mov es, patn_seg; mov si, patn_ofs
  mov al, es:[si+bx+1]; and al, 15
end;

function read_el(row: word): byte; assembler;
asm
  mov bx, patn_offset; mov ax, row
  add bx, ax; add bx, ax; add bx, ax
  mov es, patn_seg; mov si, patn_ofs
  mov al, es:[si+bx+2]
end;
Nun einmal der Anfang der Abspiel-GOSUB in Pascal mit direktem Lesen aus den codierten Patterns vom Heap:

Code: Alles auswählen

{ benötigte Variablen/Arrays/Strukturen damit der Compiler sich nicht beschwert }
type ordr_struc = array[0..255, 0..8] of byte;
var
  ordr_pointer: pointer;
  ordr: ^ordr_struc absolute ordr_pointer;

  edrow, edpos: byte;
  count, speed: byte;

  patn_offset: word; { wird in der Abspielroutine anhand Ordr bestimmt }


procedure bt_play; { letztlich wohl eine Interrupt-Procedure }
  var
    v: byte;
    n, o, i, eh, el: byte;
begin
  inc(count);
  if count = speed then
  begin
    for v := 0 to 8 do
    begin
      patn_offset := ordr^[edpos, v] * 192;
      n := readnote(edrow);
      o := readoct(edrow);
      i := readins(edrow);
      eh := read_eh(edrow);
      el := read_el(edrow);

      {
      if eh = ...

      ...

      }
Mann kann sich die Übergabe von edrow auch sparen indem man in den ASM Routinen direkt edrow statt row schreibt, das dürfte die Sache nochmal ein wenig beschleunigen.

Es entfällt:

Code: Alles auswählen

	IF edpos <> opos THEN
		modptr(0) = 320& * ord(0, edpos)
		modptr(1) = 320& * ord(1, edpos)
		modptr(2) = 320& * ord(2, edpos)
		modptr(3) = 320& * ord(3, edpos)
		modptr(4) = 320& * ord(4, edpos)
		modptr(5) = 320& * ord(5, edpos)
		modptr(6) = 320& * ord(6, edpos)
		modptr(7) = 320& * ord(7, edpos)
		modptr(8) = 320& * ord(8, edpos)
		FOR v = 0 TO 8
			mptr = modptr(v)
			FOR r = 0 TO 63
				nbuf(r, v) = PEEK(mptr): mptr = mptr + 1
				obuf(r, v) = PEEK(mptr): mptr = mptr + 1
				ibuf(r, v) = PEEK(mptr): mptr = mptr + 1
				ehbuf(r, v) = PEEK(mptr): mptr = mptr + 1
				elbuf(r, v) = PEEK(mptr): mptr = mptr + 1
			NEXT r
		NEXT v
	END IF
Sowohl hier in der Abspielroutine als auch in der Laderoutine.
Und natürlich alles EMS-bezogene.
Zuletzt geändert von zatzen am Mo 20. Jan 2020, 01:13, insgesamt 2-mal geändert.
mov ax, 13h
int 10h

while vorne_frei do vor;
Benutzeravatar
Thomas
DOS-Kenner
Beiträge: 426
Registriert: Mi 22. Jun 2016, 12:29
Wohnort: Nähe von Limburg / Lahn

Re: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von Thomas »

Super! Also kommst Du der ganzen Sache schon ein bedeutendes Stück näher.

Ja, 10 x POKE und 5 oder 10 x PEEK.

Ja, BT_Play wird eine Interrupt Routine, bzw. Ist es schon in meinem Versuch.
Da bin ich jetzt auch dem Fehler beim Abspielen näher gekommen. Zum einen waren noch zwei IF Abfragen darin die noch auf = -1 standen anstelle von > 0 und zum anderen, und da denke ich liegt das Hauptproblem, ist das Begin der großen FOR V Schleife am Anfang nicht mit End abgeschlossen. Ergänze ich das End, meckert der Compiler "; erwartet" jetzt gehe ich Zeile für Zeile dazwischen durch auf der Suche nach doppelten Ends. Kann aber doch normal nicht sein...
Ah Zatzen, du schriebst ja ich könne statt dem BASIC ELSEIF einfach ELSE IF schreiben. Dazu eine Frage: reicht es dem folgenden IF Block Begin und End zu verpassen oder benötigt das ELSE davor ebenfalls Begin und End?

Ähm, wie gehen wir denn jetzt genau vor? Soll ich deine Listings schonmal bei mir in ein neues Programm tackern? Oder arbeitest Du bereits an einer kompletten Unit? Weil wie ich schon sagte, ich denke nicht dass ich deine Routinen kompatibel zu meinem Rest eingebunden bekomme....
Ein bisschen DOS kann oft mehr als ein Haufen Fenster.

Gigabyte GA-586HX, P54C 100@75MHz, 24MB RAM, AVGA3-22-1M ISA, RTL8029AS PCI, Goldstar Prime 2 ISA, MA5ASOUND, Dreambl. X2 DB, HD 4x2GB, 48x CD, 3,5" Floppy, 2xRS232, 1xPar., PS/2 Maus
Benutzeravatar
zatzen
DOS-Guru
Beiträge: 518
Registriert: Di 17. Apr 2012, 05:10
Wohnort: bei Köln
Kontaktdaten:

Re: Allgemeine Hilfestellung zum Programmieren unter DOS gesucht

Beitrag von zatzen »

Dieses Ding fasziniert mich im Moment ziemlich...
Die einzige komplizierte Routine ist die BT.Play Gosub, die Laderoutine habe ich ja schon, und der Rest ist ziemlich simpel, zumindest ohne EMS. Wäre es okay für Dich, wenn ich das komplett übernehme und Dich evtl. um Rat Frage wenn ich hier oder da nicht weiterkomme?

Ich habe mir PIS Dateien im Hex Editor angesehen, und da gibt es haufenweise Byte-Triplets der Form C0 00 00.
In der Formatbeschreibung heisst es, Note 12 ($C) bedeutet "blank". Also leere Zeile.
Ich würde nun hingehen, wenn ich den Code erstmal in Pascal habe mit den Patterns auf dem Heap, dass ich das ganze noch so modiifiziere dass jedes Pattern 8 Informations-Bytes, also 64 Bits bekommt, die verraten, welche Zeilen tatsächlich Informationen enthalten und welche nicht, und dann nur diese Zeilen nacheinander auf den Heap kopieren. Das würde ich beim Laden einrichten. Wird ein klein wenig kompliziert, aber absolut machbar, und die Größe der Patterns auf dem Heap dürfte dadurch beträchtlich schrumpfen. Nur wenn ein Pattern komplett gefüllt ist wäre es so 8 Byte größer als ohne diese Maßnahme, aber ab 3 leeren Zeilen pro Pattern wird gespart.

So würde die Portierung für mich umso mehr Sinn machen, kein EMS, und im Schnitt vielleicht nur ein viertel so viel Pattern-Speicher nötig wie in den Dateien selbst.
Zuletzt geändert von zatzen am Mo 20. Jan 2020, 20:15, insgesamt 1-mal geändert.
mov ax, 13h
int 10h

while vorne_frei do vor;
Antworten