2009-01-14:

OpenGL w skryptach .BAT

bat:windows:easy:opengl:c++
Zgodnie z obietnicą, dzisiaj będzie o wykorzystaniu OpenGL w skryptach .BAT. Chciałbym jednak od razu przypomnieć, że .BAT z szybkością dużo wspólnego nie ma ;> (mimo to pod koniec co nie co o optymalizacji napiszę i tak).

(źródła + binarki są na końcu artykułu spakowane ZIPem i gotowe do ściągnięcia)

Jak wiadomo w skryptach .BAT obsługi OpenGL nie ma. Ale jak również wiadomo, obsługi OpenGL w żadnym języku nie było, do póki ktoś nie zrobił interfejsu OpenGL dla tego języka - sprawa jest więc prosta, musimy stworzyć interfejs OpenGL dla batcha.
Na początek rozważmy architekturę takiego interfejsu. Ponieważ .BAT nie ma możliwości tworzenia okna (tak btw, jak by miał taką możliwość, to właścicielem tego okna był by cmd.exe), to musimy stworzyć "coś" co stworzy nam okno. Hmm, i tu się pojawia problem - uruchamiamy "coś", "coś" stworzyło okno (w sensie - zrobiło CreateWindow + setup OpenGL, lub użyło libSDL/GLUT/cokolwiek do tego), i zrobiło exit - bo musiało zwrócić kontrolę batchowi - a garbage collector systemowy usunął okno. Uh. Ale nam przecież chodzi o to żeby okno nadal istniało, a aplikacja wyszła lub przynajmniej pozostała w tle. No dobra, w takim razie zostawimy aplikację w tle, niech trzyma otwarte okno. Nazwijmy to "coś" w takim razie uruchamianym na żądanie daemonem OpenGL.
Hmm, a teraz, jak coś "narysować" w tym "trójwymiarowym" oknie? Ano trzeba w kontekscie daemona wywołać jakieś funkcję OpenGL, typu glTranslatef, glBegin, glVertex3f, etc. Trzeba więc jakoś skomunikować się z daemonem. Jak? Tworząc kolejny program, nazwijmy go GLOpcode, który wyśle pojedyncze polecenie do daemona i zwróci kontrolę batchowi. Samo przesyłanie komend z GLOpcode do GLDaemon może odbywać się na kilka sposobów:
- sockety TCP/UDP
- named pipe'y
- komunikaty (SendMessage etc)
- wszelkie inne mechanizmy do IPC, typu DDE, dzielona pamięć, lub nawet jakiś driver
Co wybrać? Ja wybrałem sockety TCP, bo akurat pod ręką mam dość wygodny lib ;>, chociaż rozsądniejszym wyborem były by tutaj pipe'y, komunikaty, lub UDP.

OK, jak powinien działać GLDaemon? Przede wszystkim po uruchomieniu powinien stworzyć okno OpenGL, a następnie powinien oczekiwać na polecenia. W moim przypadku założyłem że GLDaemon nie wykonuje poleceń od razu po ich otrzymaniu, lecz zapisuje sobie otrzymywane polecenia na listę, i wykonuje je na żądanie. Tak więc pierwsze trzy polecenia GLDaemona będą poleceniami operującymi na liście poleceń (taka uwaga - będę podawał od lewej najpierw nazwę funkcji w interfejscie .BAT, następnie opcode przesyłany przez GLOpcode do GLDaemona, a potem krótki opis):

gl.LockRender, L - Zatrzymanie wykonywania listy, wszystkie otrzymane polecenia będą do niej dopisywane
gl.ClearRender, C - Wyczyszczenie listy poleceń OpenGL
gl.UnlockRender, U - Wznowienie wykonywania listy (lista jest wykonywana non stop - po zakończeniu listy, jest wykonywana od początku)

Reszta poleceń jest dobrze znana z OpenGL:

gl.Translatef, AglTranslatef %1 %2 %3 - Wywołanie glTranslatef (przesunięcie) z parametrami X,Y,Z
gl.PushMatrix, AglPushMatrix - Zapisanie macierzy na stosie
gl.PopMatrix, AglPopMatrix - Przywrócenie macierzy
gl.Begin, AglBegin %1 - Rozpoczęcie podawania koordynatów obiektu (parametr to typ obiektu, np. GL_TRIANGLE)
gl.End, AglEnd - Koniec podawania koordynatów
gl.Color3f, AglColor3f %1 %2 %3 - Ustawienie obowiązującego koloru
gl.Vertex3f, AglVertex3f %1 %2 %3 - Podanie koordynatu
gl.Rotatef, AglRotatef %1 %2 %3 %4 - Obrót wg podanego wektora

A teraz, stwórzmy statyczną klasę w .BAT, która zawiera interfejs. Najpierw konstruktor (który m.in. odpala w tle GLDaemona):

:gl.init
start GLDaemon
rem Dajmy 2 sekundy daemonowi na start
sleep 2
set gl.Translatef=call :gl.Translatef
set gl.PushMatrix=call :gl.PushMatrix
set gl.PopMatrix=call :gl.PopMatrix
set gl.Begin=call :gl.Begin
set gl.End=call :gl.End
set gl.Color3f=call :gl.Color3f
set gl.Vertex3f=call :gl.Vertex3f
set gl.Rotatef=call :gl.Rotatef
set gl.LockRender=call :gl.LockRender
set gl.UnlockRender=call :gl.UnlockRender
set gl.ClearRender=call :gl.ClearRender
goto :EOF


A teraz, jak wygląda implementacja którejś ze statycznym metod? Weźmy np gl.LockRender oraz gl.Rotatef (one wszystkie są ultra podobne):

:gl.LockRender
GLOpcode "L"
goto :EOF

:gl.Rotatef
GLOpcode "AglRotatef %1 %2 %3 %4"
goto :EOF


Mamy więc interfejs OpenGL dla .BAT. Jak wygląda przykładowe użycie takowego interfejsu? Spójrzmy na przykład który rysuje obracający się kolorowy trójkąt (<hermetyczny_dowcip>w stylu XBOX 360</hermetyczny_dowcip>).

@echo off
call :gl.init

set r=0
:loop
 !gl.LockRender!
 !gl.ClearRender!
 !gl.Translatef! 0 0 -10
 !gl.PushMatrix!
 !gl.Rotatef! !r! 1 0.3 0.2
 !gl.Begin! GL_TRIANGLES
 !gl.Color3f! 1 0 0!
 !gl.Vertex3f! 0 1 0
 !gl.Color3f! 0 1 0
 !gl.Vertex3f! -1 -1 0
 !gl.Color3f! 0 0 1
 !gl.Vertex3f! 1 -1 0
 !gl.End!
 !gl.PopMatrix!
 !gl.UnlockRender!
 set /a r=!r!+20
goto loop
goto :EOF


Sprawa jak widać jest dość prosta. Na początku wywołujemy konstruktor interfejsu, a potem mamy pętelkę, która czyści listę renderingu, wrzuca trochę poleceń OpenGL (podając mu koordynaty trójkąta obróconego o r), każe wykonać listę, i zwiększa r o 20 stopni.

Na zachętę screen (w jego górnej części widać okno GLDaemona):
trojkat


OK, a teraz trochę C++. Zacznijmy od implementacji GLOpcode (przypominam że wszystko jest w paczce na dolę postu):


#include <cstdio>
#include <cstring>
#include <cstdlib>
#include "NetSock.h"

int
main(int argc, char **argv)
{
 if(argc == 1)
   return 1;

 NetSock a;
 a.Connect(0x7f000001, 31337);
 a.Write((unsigned char*)argv[1], strlen(argv[1]));
 a.Disconnect();
 
 return 0;
}


Noo jak widać trudne to nie jest. Sprawdza czy jest argument, a jeżeli jest to łączy się na 127.0.0.1 na port 31337 ;D, wysyła polecenie, się rozłącza (dlatego lepiej by było żeby to UDP było, ale nvm).

Implementacja daemona jest kapkę dłuższa, więc nie będę jej całej zamieszczał. Wrzucę tylko back-end funkcji które cytowałem w przypadku .BAT - gl.LockRender (L) i gl.Rotatef (AglRotatef):

Najpierw Lock:

[...]
   else if(Buffer[0] == 'L') // Lock
   {
     puts("Lock");
     UserLock = true;
   }
[...]
void
static scene()
{
 if(Connection || UserLock)
   return;
[...]


W skrócie, Lock ustawia flagę UserLock. Jeżeli ta flaga jest ustawiona, to funkcja scene (zawierająca procedury wykonujące rendering) się nie wykonuje.

A teraz glRotatef:

   else if(strcmp(Cmd, "glRotatef") == 0)
   {
     float a, b, c, d;
     sscanf(*i, "%*s %f %f %f %f", &a, &b, &c, &d);
     glRotatef(a,b,c,d);
   }


Za dużo nie ma co komentować nie? Bierze i-ty element listy, parsuje go (tak, wiem, w liście powinny być sparsowane elementy, żeby szybciej się wykonywała... ale kod miał być łatwy i w miarę krótki >), i wykonuje glRotatef z podanymi parametrami ("%*s" btw oznacza "jest tam string, ale go zignoruj", dlatego w sscanf jest 5 formatów podanych, ale tylko 4 zmienne docelowe dla nich).

Jak wygląda FPS tego cuda? Niestety, powinno się raczej pytać o SPF (Seconds Per Frame). Można natomiast trochę to pooptymalizować. Rozwiązań jest kilka.

Pierwsze rozwiązanie dotyczy przejścia z TCP na coś szybszego w przypadku lokalnego komputera. Tak jak pisałem już ze dwa razy wcześniej, dobrym pomysłem byłyby komunikaty, named pipe'y lub UDP - sprawi to że GLOpcode będzie wykonywał się szybciej.

Drugą sprawą jest zmniejszenie ilości wywołań GLOpcode. W takim przypadku robimy drobną przeróbkę GLOpcode tak aby umiało naraz wysyłać więcej niż jedno polecenie:

 int i;

 NetSock a;
 a.Connect(0x7f000001, 31337);

 for(i = 1; i < argc; i++)
 {
   a.Write((unsigned char*)argv[i], strlen(argv[i]));
   Sleep(0); // Niech sie wysle ;D
 }

 a.Disconnect();


A następnie przerabiamy interfejs tak aby tworzyło listę parametrów, i wysyłało ją w momencie wywołania gl.UnlockRender. Kilka głównych zmian wygląda tak:

:gl.init
[...]
set gl.CommandList=
goto :EOF

:gl.LockRender
set gl.CommandList=%gl.CommandList% "L"
goto :EOF

:gl.UnlockRender
set gl.CommandList=%gl.CommandList% "U"
GLOpcode %gl.CommandList%
goto :EOF

:gl.ClearRender
GLOpcode "L" "C" "U"
set gl.CommandList=
goto :EOF

:gl.Translatef
set gl.CommandList=%gl.CommandList% "AglTranslatef %1 %2 %3"
goto :EOF


To rozwiązanie całkiem ładnie przyspiesza sprawę (stworzenie procesu jest dużo wolniejsze niż operacje na zmiennych środowiskowych). Wrzuciłem je do katalogu "opt".

Kolejnym pomysłem byłoby stworzenie kilku oddzielnych list poleceń, i mówienie daemonowi które listy, i w jakiej kolejności ma wykonywać (hehe, jeżeli ktoś zna OpenGL to za pewne widzi analogię do optymalizacji przy użyciu CallList, czy choćby, idąc o krok dalej, VBO).

Za pewne jeszcze kilka pomysłów by się znalazło, ale zostawiam je do wymyślenia samodzielnie w ramach zadania domowego ;>

Paczka ze źródłami i binarkami: batgl.zip (305 KB)

OK tyle na dzisiaj. Zapraszam do wrzucania pomysłów optymalizacyjnych w komentarzach ;>

Comments:

2009-01-14 05:42:22 = Obi-San
{
Pełen respekt za ten artykuł, przyznam że bardzo mnie zaskoczyłeś. Co jeszcze w temacie batch masz w zanadrzu?
}
2009-01-14 06:08:21 = kicaj
{
straszny hak z tym openGL ale rzuca całkiem nowe spojerzenie na tworzenie skryptów batch, aż łezka się kręci w oku
}
2009-01-14 07:45:32 = oshogbo
{
Jedno słowo "miazga"... GW
}
2009-01-14 10:11:33 = mt3o
{
Domyślam się, że wąskim gardłem jest komunikacja na linii bat<->daemon.
Czy gdyby daemon komunikował się z batem za pomocą powiedzmy obszaru współdzielonego pamięci, nie byłoby to szybsze?
Ponadto należałoby ograniczyć do minimum czas uruchamiania się warstwy pośredniczącej czyli GLOpcode. Tylko pytanie - jak? Tnąc potrzebne biblioteki? Pisząc to na niższym poziomie? (to ostatnie tylko w desperacji...)

Należałoby zbadać czasy odpaleń i zakończeń GLOpcode'a, przejścia całej pętli pliku BAT i przetwarzania tych poleceń w daemonie.

Przydatne powinna być też możliwość definiowania własnych zbiorów poleceń, analogicznych do procedur z pascala. W końcu szybciej zadeklarować polecenie obrócenia trójkąta i wywołać ją trzykrotnie w daemonie, niż 3x wywołać każde atomowe polecenie osobno.

Ostatecznie rozwiązanie, które zastosowałeś w kodzie przestawia trójkąt o stałą odległość z każdym wywołaniem. Czyli na 800mhz będzie się obracać wolniej niż na maszynie czterordzeniowej. Gdyby liczyć deltę czasu (w obrębie zadeklarowanej wewnątrz daemona procedury, na przykład) i w zależności od jej wielkości obracać trójkąt - uzyskalibyśmy podobną ilość obrotów na każdej maszynie.

W każdym razie - genialny pomysł na bezużyteczny projekt :)
}
2009-01-14 11:45:08 = Patrykuss
{
@Gyn. Kurde, nie spodziewałem się, że napiszesz o tym. Faktem jest, że wyczerpałeś ciekawe tematy dot. batcha. O czym ja teraz będę pisał :)?

Swoją drogą. Pomysł ciekawy jednak wydajność nie zachwyca. OpenGL w batchu zapewne zostanie tylko w formie "ciekawostki" ;). W każdym razie jednak wielki + za art.
}
2009-01-14 13:40:27 = Gynvael Coldwind
{
@Obi-San
Cieszę się że się podobało ;> Może jeszcze coś o .BAT wrzucę. Na pewno wrzucę hack do cmd.exe który miałem "dwa blogi temu", a który pozwalał na używanie ANSI Escape Codes w prompt i echo.. a czy coś nowego, to się okaże ;>

@kicaj
Ano hak hak, ale można zawsze jeszcze bardziej hacknąć ;> Poczynając od loadera, po skorzystanie z kilku buffer overflow (które w cmd.exe do dzisiaj istnieją) jako z interfejsu pluginów (swojego czasu tak w StarCrafcie zrobiono: dodano [przez scene ofc] na krótką chwilę [pare dni] świetne nowe możliwości skryptowania map.. ofc jako że to był typowy security vuln, to nowy patch szybko wyłączył "interfejs pluginów") i wrzucenie obsługi OpenGL bezpośrednio w cmd.exe ;>

@osho
Ależ dziękuje ;>

@mt3o
Noo! Widzę że komuś się chciało do końca arta przeczytać ;>
Co do wąskiego gardła, to masz częściową rację. Komunikacja bat<->daemon jest wąskim gardłem. Niestety nie jedynym, i nie najciaśniejszym. Pomysł ze współdzieloną pamięcią jest całkiem niezły, pod warunkiem że z .BAT można by w pamięć CMD w jakiś sposób ingerować. Sądzę że jeżeli współdzielona pamięć była by możliwa, to było by też możliwe bezpośrednie odwołanie do interfejsu OpenGL jako do lokalnych natywnych funkcji ;>
Uruchamianie GLOpcode faktycznie jest bardzo dużym obciążeniem, i imho jednym z dwóch najciaśniejszych miejsc. Natomiast nie widzę zbytnio możliwości dużych optymalizacji. Zejście jak najniżej z poziomem jest pewnym rozwiązaniem - ograniczenie się do kernel32.dll (które i tak jest w pamięci zawsze, no i jest wymagane przez loader procesów) mogło by kapkę przyspieszyć (w sensie wycięcie wycięcie msvcrt i winsock). Natomiast hmm, tworzenie procesu i tak jest kosztowne, więc trzeba by jakoś tego się pozbyć... Natomiast z tego co mi obecnie wiadomo, bez patchowania cmd.exe, nie ma zbytnio takiej możliwości.

Co do czasów, czas odpalenia jest wolny, dlatego jedną z moich propozycji optymalek były własnie batch'e poleceń wysyłanych jednym GLOpcode ;>
Zresztą trudno się z Tobą nie zgodzić, można to całkiem nieźle rozwinąć ;>

Co do delty ofc masz rację, nie chciałem jednak komplikować kodu ;> Zgadzam się że mógłby to daemon robić ;>

Hehehe puenta trafia w sedno ;> Ktoś mi kiedyś powiedział że się "nie da pisać dla OpenGL w .BAT" ;p Stąd ten post ;D Dałem się podpuścić hehehe.. I faktycznie jest to całkowicie bezużyteczne ;> Ale imho ciekawe ;>

@Patrykuss
Hehehe ;> Dzisiaj przez głupi przypadek kupiłem za 18,99zł pewną zabawkę którą będę chciał przesłać dane na Amstrada z PC.. sądzę że o tym będzie następny post ;> Zresztą, może i o .BAT coś się jeszcze znajdzie ;>

Co do wydajności, ano. U mnie wersja zoptymalizowanej wyciąga 1.2 FPS ;D Jak pisałem, taki tam Proof of Concept, i tyle ;>




}
2011-02-18 15:04:59 = Rafalon
{
Wiesz co jest problem ponieważ: "http://img191.imageshack.us/img191/3024/beztytuusifw.png" jak to naprawić?
}
2011-02-18 21:31:04 = Gynvael Coldwind
{
@Rafalon
Wygląda na to że delayed expansion nie masz globalnie włączone.
Możesz to włączyć dla skryptu tylko - dopisz na górze "setlocal enabledelayedexpansion".
Wspominałem o tym w poście o programowaniu obiektowym w .bat - http://gynvael.coldwind.pl/?id=123
}
2014-07-29 13:55:38 = Bogo89
{
na słabszych maszynkach zmienić linijkę:
start GLDaemon
na:
start /belownormal GLDaemon
lub na:
start /low GLDaemon

oraz żeby r nie rosło w nieskończoność przed goto loop dodajemy np:
if %r% gtr 360 set /a r-=360
}

Add a comment:

Nick:
URL (optional):
Math captcha: 4 ∗ 3 + 3 =