2009-01-18:

Using OpenGL in .BAT scripts

bat:windows:easy:opengl:c++
Today's post will be, as promised, about OpenGL in .BAT scripts. At the very beginning, I would like to remind you (I was told that the correct form of 'you' is written with a lower 'y') that .BAT scripts have nothing to do with speed - they are just plain slow ;>

(as always, the sources and binaries are available at the end of the article)

As one may know, there is no native OpenGL support in .BAT scripts. But, as one may also know, almost no language had native OpenGL support in the beginning - the OpenGL support was added the moment somebody created an OpenGL interface for the given language. So, the first thing to do is obvious - we must create an OpenGL interface.

Let's talk a little about the architecture of such interface. Since .BAT itself has no mechanism for creating a window (btw, if it would, then cmd.exe would own the window technically), then we must create "something" that will create us a windows. Hmm, but there is a problem - when we run that "something", and it creates a window (OpenGL window that is, using CreateWindow and setting up OpenGL, or using libSDL/GLUT/whatever to do the same), and it would exit (so that the batch script regains control), then the system garbage collector will destroy the window. Uh. We need both the window to exist, and the batch script to regain control. So let's make a daemon-style application that will run in the background, and keep the OpenGL window alive. Let's call it... GLDaemon - teh daemon of OpenGL ;>

Now, how do we get .BAT to "draw" something 3D in that window? Well, we need to call some OpenGL functions (like glTranslatef, glBein, glVertex3f, etc) in the context of the GLDaemon. But, how do we tell the daemon that some function should be executed? Well, let's create another program, called GLOpcode, that will be used to send a single command to the daemon. The transport of messages between GLOpcode and GLDaemon can be done in several ways:
- we can use sockets and TCP/UDP
- named pipes are also cool
- messages (SendMessage, etc)
- and all other IPC mechanisms (DDE, shared memory, or even some driver)
Which one should we choose... I've chosen TCP - because I have a handy TCP library I can use ;>, but I admit, that using pipes, messages, or UDP would be better.

How should the GLDaemon work? Well, first of all, it should create an OpenGL window, and then wait for commands. In my case, I wanted the daemon not to execute the commands instantly, but instead, I wanted it to create a command list, and execute the list on demand. So, the first 3 GL opcodes will be used to work on the command list (a quick note: from the left, I will write the function name in the .BAT interface, then the opcode sent by the GLOpcode, and then a short description):

gl.LockRender, L - Locking the list execution and "opening" the list for appending new commands.
gl.ClearRender, C - Clearing the list.
gl.UnlockRender, U - Unlocking list execution (the list is executed continuously in a loop).

The following commands are well known from OpenGL:

gl.Translatef, AglTranslatef %1 %2 %3 - Calling the glTranslatef function (translation/displacement) with parameters X,Y,Z
gl.PushMatrix, AglPushMatrix - Pushing the matrix on a stack
gl.PopMatrix, AglPopMatrix - Poping the matrix from the stack
gl.Begin, AglBegin %1 - Start taking coordinates/other stuff for some 3D figure (give the figure name in the parameter, i.e. GL_TRIANGLE)
gl.End, AglEnd - End of coordinates
gl.Color3f, AglColor3f %1 %2 %3 - Setup a color
gl.Vertex3f, AglVertex3f %1 %2 %3 - Send a coordinate
gl.Rotatef, AglRotatef %1 %2 %3 %4 - Rotate according to the provided vector

Now, let's create a "static class" in .BAT (see my previous post about classes in .BAT), that will "contain" the OpenGL interface. First, the constructor (inter alia, it will fire up the GLDaemon):

:gl.init
start GLDaemon
rem Let's give the daemon 2 seconds to 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


Now to implement the static methods. I'll show gl.LockRender and gl.Rotate as examples - they all look the same:

:gl.LockRender
GLOpcode "L"
goto :EOF

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


So, we have the OpenGL interface for .BAT ready. How do we use this interface? Let's look upon an example that will draw a colorful triangle:

@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


As one can see, it's rather simple. We call the interface constructor, then we have a loop that clears the command list, sends some GL commands that draw a triangle rotated by r, orders to execute the list, and increases r by 20 degrees.

Here is a screen shot (one can see the GLDaemon window at the top):
trojkat


Now for some C++. I'll start with GLOpcode implementation (may I remind You, that everything is in the pack at the bottom):

#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;
}


It just checks if there is a parameter, then it connects to 127.0.0.1 port 31337 (what other port could I possible use ? ;D), it sends the opcode, and disconnects (thats why using UDP would be better, but nvm).

The daemon implementation is a bit longer, so I'll just show here a part of it. Below there is the back-end of the gl.LockRender (L) and gl.Rotatef (AglRotatef) functions:

First the Lock

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


Long story short: it sets the flag UserLock. If the flag is set, then the 'scene' function (that does the render stuff) is not being executed.

Now for 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);
   }


Nothing to comment, right? It takes the i list element, parses it (well it should be pre-parsed already at command addition, but I wanted the code to be more simple then optimized), and executed glRotatef with the given parameters ("%*s" is "well, there is a string there, but I don't care... ignore it" - thats why sscanf has 5 "formats", but only 4 target variables).

How much FPS do we have? Well, the question should be: How much SPF (Seconds Per Frame) do we have. However, we can do some optimizing. There are a few things we can do:

As I have written before, TCP could be changed to something else - pipes, UDP, etc. GLOpcode could execute faster.

Another thing is decreasing the number of times GLOpcode is called. We need to do some remakes in GLOpcode, so we can give it more than one command at once to transfer:

 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); // Let it send ;D
 }

 a.Disconnect();


Now, let's remake the interface to create a command list in the .BAT script, and send it when gl.UnlockRender is called. The main changes are shown below:

: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


This boosts things quire well (creating a process is slower then changing some environment variable). I've put this version in the "opt" directory.

Another thing would be to create a few separate command lists in the daemon, and tell him which one in what order should he execute (hmm, sound like CallLists in OpenGL... or even we could make something like VBO!).

There are many other possible ways to optimize it (another proof it's not optimal in any way ;>), but I leave them to be figured out by the readers, as home work ;>

Pack with source and binaries: batgl.zip (305 KB)

And thats all! You are welcomed to leave your optimization ideas in the comments ;>

Add a comment:

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