Trust in Me!

Nehmen wir mal an, ein (Delphi-)object, welches eine Liste gleicher Objekte verwalten soll. Man könnte das einen Baum nennen.

  TValueList = array of TValue;
  TValue = object
    FList: TValueList;
    SomeOtherStuff: integer;
  end;

(Für später geborene: das ist die alte Syntax für „record mit Methoden“.)

Leider nimmt uns der Compiler das so nicht ab. Warum? Weil TValue natürlich vorher noch nicht bekannt ist. Und im Gegensatz zu Klassen kann man Records nicht vorwärtsdeklarieren. Wie also könnten wir dieses Problem lösen? Man könnte Pointer verwenden, und bei Verwendung entsprechend erzeugen. Aber freigeben? Es gibt ja keinen Destructor, also keine Option. Man könnte das gesamte Objekt in eine Klassen-Instanz verwandeln, die von IUnknown die Referenzzählung erbt. Das ist aber wesentlich mehr Aufwand als das bisherige „Deklarieren und Verwenden“.

Die Lösung hier zeigt sich in einem fiesen, aber einfachen Hack:

TValueListAlias = array of boolean;
TValue = object
  FList: TValueListAlias;
  SomeOtherStuff: integer;
end;
TValueList = array of TValue;

Dabei spielt es keine Rolle, welchen Basistypen man für das Alias verwendet. Es muss sich nur um ein dynamisches Array handeln. Jetzt kann man in jedem Zugriff auf das eigentliche TValueList casten und alles funktioniert. Dabei kann man die Hässlichkeiten wunderbar in Zugriffsmethoden verbergen.

SetLength(TValueList(FList), 42);

Das funktioniert auch, aber wenn TValue nun Felder die finialisiert werden müssen enthält, wird sich FastMM beschweren, dass eben diese nie freigegeben werden. Es stellt sich heraus, dass SetLength zwar den BaseType richtig speichert und auch in @DynArrayClear exakt diesen wieder beräumt, aber Felder im Gegensatz zu Variablen nicht damit, sondern mit FinalizeArray freigegeben werden. Und dieses richtet sich nach der Deklaration, nicht nach dem was wirklich passiert.

Hier kommt dann der eigentliche Grund für diesen Beitrag: wir sagen der RTL einfach: „Vertrau mir, das ist ein anderer Datentyp!“. Mit anderen Worten: wir patchen die Feld-Tabelle der betroffenen Klasse so, dass FinalizeArray dort ein TValueList (BaseType also TValue, nicht unser Dummy von weiter oben) sieht und dieses korrekt finalisiert.

Das ist zunächst einfacher als gedacht:

TI:= TypeInfo(TValue);
FT := Pointer(Integer(TI) + Byte(TI.Name[0]));
for I := FT.Count-1 downto 0 do begin
  if FT.Fields[I].TypeInfo^ = TypeInfo(TValueListAlias) then
    ppti:= FT.Fields[I].TypeInfo^:= TypeInfo(TValueList);
end;

Nur – das funktioniert so nicht 😉 Runtime Error 216 ist die Folge einer Exception beim Schreiben der neuen TypeInfo. Da hat nämlich mal jemand mitgedacht und die dazugehörige Seite als PAGE_EXECUTE_READ markiert. Was man zum Glück in seinem eigenen Prozess beliebig ändern kann, und so ergibt sich (mit allen Deklarationen) folgendes Meisterwerk:

procedure FixFieldTable(TheRecord, Find, Replace: PTypeInfo);
type
  TFieldInfo = packed record
    TypeInfo: PPTypeInfo;
    Offset: Cardinal;
  end;
 
  PFieldTable = ^TFieldTable;
  TFieldTable = packed record
    X: Word;
    Size: Cardinal;
    Count: Cardinal;
    Fields: array [0..0] of TFieldInfo;
  end;
var
  FT: PFieldTable;
  ppti: PPTypeInfo;
  I, old, dummy: cardinal;
begin
  FT := Pointer(Integer(TheRecord) + Byte(TheRecord.Name[0]));
  for I := FT.Count-1 downto 0 do begin
    if FT.Fields[I].TypeInfo^ = Find then begin
      ppti:= FT.Fields[I].TypeInfo;
      VirtualProtect(ppti,SizeOf(ppti), PAGE_READWRITE, old);
      try
        ppti^:= Replace;
      finally
        VirtualProtect(ppti, sizeof(ppti), old, dummy);
      end;
    end;
  end;
end;
{...}
 
  FixFieldTable(TypeInfo(TValue), TypeInfo(TValueListAlias), TypeInfo(TValueList));

Wer sich die Datenstruktur ansieht, wird feststellen dass TFieldTable ein Feld namens „Size“ enthält. Sollten wir das nicht anpassen? Die einfache Antwort ist: nein! In TValue selbst wird vom dynamischen Array nur ein Pointer auf den Anfang gespeichert, und der ist unabhängig vom genauen Aussehen des Arrays immer gleich groß: nämlich 32bit oder 4 Byte. Es ändert sich also nichts.
Außerdem praktisch: da SetLength den Array-Typ anlegt, den man ihm übergibt (und wir ja auf TValueList casten), werden auch untergeordnete Arrays als das freigegeben was in ihnen steckt.

Das funktioniert so bis mindestens BDS2006, ich kann mir aber vorstellen dass die neuen RTTI-Strukturen in XE hier einige Änderungen notwendig machen. Das müssen dann aber andere testen 😉

Oh, übrigens, wer es nicht erkannt hat: der Titel ist natürlich eine Referenz auf Disney’s Dschungelbuch.

Punkteverteilung

Manchmal, nur ganz selten, verhält sich ein Code anders als man denkt, weil man was anderes programmiert hat als man denkt.

Man nehme z.B. eine KI mit Negamax+AlphaBeta, die immer irgendwelche völlig behämmerten Züge ausführt. Irgendwann ist mir dann mal gedämmert, dass man sich ja mal die Score-Werte ausgeben lassen könnte.

next move
  h1 to g1 score: 9
  h1 to g2 score: 9
  h1 to i2 score: 9
  h1 to h2 score: 9
  h1 to f1 score: 9
  h1 to f3 score: 9
  h1 to h3 score: 9
  i1 to h2 score: 9
  i1 to i2 score: 9
  i1 to g1 score: 9
  i1 to g3 score: 9
  i1 to i3 score: 9
  a9 to b8 score: 9
  a9 to b9 score: 9
  a9 to a8 score: 9
  a9 to c7 score: 9
  a9 to c9 score: 9
  a9 to a7 score: 9

Huch! Alles das gleiche. Wie kann das sein?

    function Score(ABoard: TBoard; Perspective: TField): TScore;
{...}
          if Board[c,r]=Perspective then
{...}

M( M( M( M( M( M( M(

Board ist ein Member von TGame, und damit an dieser Stelle durchaus ein gültiger Bezeichner. Nur leider nicht der den ich will, denn damit habe ich immer das Brett vom Startpunkt bewertet, niemals das nach einem Zug. Sehr schön, es sollte mal einer intelligente Compiler entwickeln, die mir sowas direkt sagen…

Kleine Hacks erhalten die Freundschaft

Heute mal wieder was aus der Bastelkiste:

Unser Institut für Mechanik hat für Belege und Praktika ein schickes kleines Delphi-Programm, welches die Aufgabenblätter generieren kann. Das ganze hat den Zweck, dass die Aufgaben sich mit der Matrikelnummer personalisieren lassen (RandSeed) und man seine berechneten Werte mittels dieses Programms direkt auf Korrektheit prüfen lassen kann.

Nun sind die Aufgaben nicht immer so ganz ohne, und manchmal hilft es ungemein, nicht nur Eingabefelder zu haben, die einem sagen ob man richtig oder falsch lag, sondern auch mal spicken zu können, wo denn der Fehler liegt.

Aber erstmal eine kleine Anekdote.
Man sitzt also in einer Vorlesung, und macht nebenbei mit einem Kommilitonen ein solches Übungsbelegblatt. Die Gleichungen sehen auch erstmal alle ganz schön aus. Nun hat man ja einen Laptop dabei, kann also die Ergebnisse kontrollieren. Und siehe da: nix passt.
Was tut man also? Man versucht herauszubekommen, was das Programm denn als Zahlenwert erwartet. Irgendwie dachte ich mir (warum, weiß ich auch nicht; vielleicht wars ja Schicksal 😛 ) dass mir ein Hex-Editor weiterhelfen könnte. Konnte er natürlich nicht, hat aber soweit geholfen, als dass ich festgestllt habe, dass es zu jedem Aufgabenblatt 2 Definitionen gibt. Eine generiert die Aufgabenstellung, und eine generiert… Moment. Was ist das? Ein Lösungsweg!
Es gibt also nicht nur 2 Programmversionen (wie schon das _student im Dateinamen vermuten lässt), sondern zu allem Überfluss enthält die selbe Echse offenbar beide Teile. Irgendwo muss also umgeschaltet werden, was gemacht werden soll.

Nachdem man seinen IDA Pro wiedergefunden und gestartet hat, wird also mal das Programm hineingeworfen. Etwas warten, und die Programmstelle wieder finden. Das macht sich recht einfach, immerhin ist der Titel des Belegs ein sehr eindeutiges Merkmal (im Code-Segment jedenfalls). Nun muss man nur noch die XRefs zurückverfolgen, um rauszubekommen wo der wichtige Aufruf erfolgt. Der ist nämlich da, wo basierend auf einem Vergleich der eine oder andere Zweig (=Ausgabeanweisungen) aufgerufen wird. It always boils down to a simple comparision.
Der letzte Schritt (wo allerdings leider die Vorlesung zuende war; der Teil mit dem „IDA wiederfinden“ hatte etwas lange gedauert) ist es nun nur noch, die Zuweisung an die überprüfte Variable zu finden. Auch hier hilft wieder die XRefs-Funktion. Es gibt nämlich nur eine, und die schreibt eine $00, also den Ordinalwert für false. Man könnte also sagen, die betreffende Variable ist sowas wie IsTutorenVersion.
Kurzer Patch in der Exe der (wir wissen ja: es ist Delphi) eine $01 (für true) stattdessen schreibt, und fertig.

Wer das liest und ein berechtigtes Interesse hat, der hat auch Wege mich nach dem Teil zu fragen. Das ist nun grade ein Hack, bei dem ich garantiert kein Responsible Disclosure machen werde. Das wäre nämlich sehr verantwortungslos. Mir selbst gegenüber 😀

Das wars erstmal von meiner Seite,
Martok

Demo1 – Release

Hallo!

Meine angekündigte Demo ist trotz ersten Problemen doch noch irgendwie fertig geworden. In Konkurrenz zu Farbrausch trete ich hier nicht gerade, aber immerhin zeigt sie, dass man mit Delphi auch recht kleine Demos schreiben kann, auch ohne wie blöd zu optimieren. Interessant finde ich ja, dass die ganzen Funktionen die ich in den letzten paar Tagen eingebaut habe sich kaum in der Dateigröße niederschlugen: immer noch 47KByte, nach UPX allerdings.

Musik hab ich allerdings keine drin, was aber nur da dran liegt, dass ich keine Lust hab noch was zu komponieren. Den Tracker gibt es ja in Form von BlitzTracker bereits.

Download hier (42 KByte ZIP)
Einfach auspacken und starten. Wer sie im Vollbild-Modus genießen will, kann treeD.exe mit dem Parameter -fullscreen aufrufen.
Ach ja, und die Wartezeit am Anfang ist kein Crash, sondern da werden die Texturen & das Terrain generiert.

Für die, die es interessiert: der ganze Spaß basiert auf einem Timeline-System, dass Kamerafahrten, Scene-Renderer und Overlay-Renderer entsprechend der aktuellen Zeit (angezeigt als DT im Window-Mode) auswählt und ggf. interpoliert.
Der Inhalt ist übrigens an keiner stelle statisch, jedes mal wenn man die Demo startet, sieht man eine komplett andere Welt 😉

Tja. Dann sagt mal was dazu. Aber bitte nicht nur die künstlerischen Aspekte bewerten, ich weiß dass das nicht sooo der Renner ist… 🙄

mfg
Martok

Prozedurale Texturen….

Mal wieder was von mir 😉

In der letzten Zeit habe ich mich mal wieder einem Projekt aus dem bereich der 3D-Grafik gewidmet. Und zwar bastle ich an einer Demo. Beziehungsweise an einem Testprojekt für ein Delphi-Demo-Framework, so wirklich Demo nennen kann man das heutzutage fast nicht mehr.

Sieht auch alles schon ganz nett aus, aber mit den Texturen hab ich noch so meine Probleme. Prozedurale Texturen sind ja eine ganz tolle Erfindung, aber so gut bin ich dann auch nicht…

Und zwar benutzt mein Framework Callback-Routinen à la

procedure _GroundTexGen(X,Y:single; var Value: TRGBA);

X und Y erstrecken sich von 0 bis 1, sind also gleichwertig den OpenGL-Texturkoordinaten. TRGBA ist ein array[0..3] of single, dessen Komponenten die einzelnen Farben Rot, Grün, Blau und Alpha (hier nicht benutzt) wieder im Bereich von 0 bis 1 enthalten.

Was ich bräuchte wären Texturen bzw. die Prozeduren für Himmel (Y>0.5 -> Oberirdisch), Holz/Baumrinde und „Boden“, also Gras, Erde usw. Wären alles prima Kandidaten für Perlin Noise, aber da blick ich nicht so ganz durch… 😕

Falls jemand Langeweile haben sollte und gern in die Credits möchte… sagt Bescheid!

mfg
Martok