DominScript Back to dominscript.com
Demo source

The script behind the cube. opengl_ffmpeg_script_driven_demo.dom

The complete source of the demo shown on the front page. Roughly 700 lines of DominScript that opens an OpenGL window, decodes a video stream with FFmpeg, and renders a spinning textured cube — with every geometric and rendering decision visible in this file.

The OpenGL plugin underneath is a thin glue layer over real GL entry points (gl.Begin, gl.Vertex3f, gl.Frustum, ...). There is no rendering engine hidden behind a high-level façade; what you read here is what runs.

Lines: ~700 Plugins used: opengl, ffmpeg, sdl2_image, worker, path, timer, os, struct, print Runtime: Linux (X11/Wayland) · Windows
opengl_ffmpeg_script_driven_demo.dom dominscript
/*
 * opengl_ffmpeg_script_driven_demo.dom — same scene as
 * opengl_ffmpeg_callback_demo, but every rendering decision lives in
 * the script.
 *
 * What it does:
 *     Identical scene to opengl_ffmpeg_callback_demo: 1280x720 window,
 *     spinning textured cube, four side faces show video frames, top
 *     and bottom faces show the project logo, mouse wheel zoom and
 *     left-drag rotate work the same way.
 *
 *     The difference is *where* the rendering logic lives. The original
 *     demo calls into a thick gl.* facade (DrawCube, DrawFace,
 *     DrawBackground, BeginFrame, ApplyRotation, ...) and the plugin
 *     hides the OpenGL state machine behind those names. Here, the
 *     plugin is reduced to a thin glue layer over real GL entry points
 *     (gl.Begin, gl.Vertex3f, gl.Color3f, gl.Frustum, gl.Rotatef, ...)
 *     and every drawing decision -- geometry, edges, gradients, matrix
 *     setup, rotation state -- is written out in this file.
 *
 *     If you want to learn how the cube is built, you read this file.
 *     There is nowhere else to hide it.
 *
 * State sharing between the main loop and the callbacks (rev31zzng):
 *     The interactive state (RenderState) lives in the CALLBACK world as a
 *     `callback global blob $State` -- the world where the mouse/resize
 *     callback (OnOpenGlEvent) and the frame callback (OnVideoFrame)
 *     run, so they read and write it directly. The main world never
 *     names it: once per frame main calls into the callback world with
 *     worker.Call("Tick", ref $Snapshot) -- the callback world advances
 *     the time-based rotation and writes the whole state into the lent
 *     blob, and main renders from that snapshot. At startup the same
 *     crossing runs the other way: worker.Call("Configure", ref $Init)
 *     seeds the callback-world state with the texture slots and the
 *     initial values. Two worlds, one call per frame, no shared
 *     pointer, no lock -- the language's own model (reference 5.2b and
 *     Examples/global_two_worlds_demo.dom).
 *     (Until rev31zzng this demo used the script_state plugin's raw
 *     buffer mapped by pointer from both sides.)
 *
 * Usage:
 *     ./domin Examples/opengl_ffmpeg_script_driven_demo.dom [flags]
 *
 *     --help    Show this help message and exit.
 *
 * Required runtime:
 *     A display server reachable from the process:
 *       - Linux: X11 or Wayland with a valid XDG_RUNTIME_DIR.
 *       - Windows: a desktop session; SDL2.dll in runtime/ (see
 *         runtime/README.txt).
 *     Built plugins: opengl, ffmpeg, sdl2_image, path, timer, worker.
 *
 * Graceful skip:
 *     Same two-layered SKIP behavior as the callback demo: env pre-check
 *     before gl.Open, plus gl.Open's return value as a backstop.
 *
 * Expected output:
 *     - Display available: opens a window, shows the spinning cube,
 *       returns 0 on close. (No stdout output in normal operation.)
 *     - No display: prints "opengl-ffmpeg-script-driven: SKIP -
 *       display not available" and exits 0.
 */

plugin "builtin:os";
plugin "builtin:struct";
plugin "builtin:print";
plugin "../plugins/worker/WorkerPlugin";
plugin "../plugins/opengl/GlPlugin";
plugin "../plugins/ffmpeg/FfmpegPlugin";
plugin "../plugins/sdl2_image/Sdl2ImagePlugin";   /* rev31zzuw: the logo through the SDL2_image adapter */
plugin "../plugins/path/PathPlugin";
plugin "../plugins/timer/TimerPlugin";

string ProjectAssetPath(string $RelativePath)
{
    string $ScriptDir[512];
    string $ProjectDir[512];

    $ScriptDir  = os.ScriptDir();
    $ProjectDir = path.Join($ScriptDir, "..");
    return(path.Join($ProjectDir, $RelativePath));
}


#define WINDOW_WIDTH  1280
#define WINDOW_HEIGHT  720
#define VIDEO_WIDTH    640
#define VIDEO_HEIGHT   360
#define LOGO_WIDTH     620
#define LOGO_HEIGHT    220

/* GL constants, copied verbatim from <GL/gl.h>. The plugin's gl.*
   wrappers take these by value, just like glBegin / glEnable / etc.
   in C. Picking them out by name here keeps the script self-explanatory
   instead of sprinkling magic hex numbers through gl.Begin / gl.Enable
   call sites. */
#define GL_COLOR_BUFFER_BIT     16384  /* 0x4000 */
#define GL_DEPTH_BUFFER_BIT     256    /* 0x0100 */
#define GL_TEXTURE_2D           3553   /* 0x0DE1 */
#define GL_QUADS                7      /* 0x0007 */
#define GL_LINES                1      /* 0x0001 */
#define GL_DEPTH_TEST           2929   /* 0x0B71 */
#define GL_BLEND                3042   /* 0x0BE2 */
#define GL_SRC_ALPHA            770    /* 0x0302 */
#define GL_ONE_MINUS_SRC_ALPHA  771    /* 0x0303 */
#define GL_PROJECTION           5889   /* 0x1701 */
#define GL_MODELVIEW            5888   /* 0x1700 */
#define GL_ALPHA_TEST           3008   /* 0x0BC0 */
#define GL_GREATER              516    /* 0x0204 */

/* Event packet layout -- same as in opengl_ffmpeg_callback_demo.dom. */
#define GL_EVENT_NONE        0
#define GL_EVENT_QUIT        1
#define GL_EVENT_RESIZE      2
#define GL_EVENT_MOUSE_DOWN  3
#define GL_EVENT_MOUSE_UP    4
#define GL_EVENT_MOUSE_MOVE  5
#define GL_EVENT_MOUSE_WHEEL 6
#define GL_MOUSE_LEFT        1

#define FACE_FRONT  0
#define FACE_BACK   1
#define FACE_RIGHT  2
#define FACE_LEFT   3
#define FACE_TOP    4
#define FACE_BOTTOM 5

#define AUTO_SPEED_X 0.23
#define AUTO_SPEED_Y 0.37
#define AUTO_SPEED_Z 0.11

#define CUBE_PANEL_HALF 0.78

/* ----- Shared state struct ------------------------------------------ */
struct RenderState packet
{
    i32     WindowWidth;
    i32     WindowHeight;
    i32     VideoSlot;
    i32     LogoSlot;
    double  CameraDistance;
    double  ViewScale;
    double  AngleX;
    double  AngleY;
    double  AngleZ;
    double  SpeedX;
    double  SpeedY;
    double  SpeedZ;
    double  LastAdvanceMs;
    double  CubeGap;
    double  EdgeR;
    double  EdgeG;
    double  EdgeB;
    double  EdgeWidth;
};

/* The interactive state, owned by the callback world (dispatcher thread).
   Only callback functions touch it; main sees it through Tick's snapshot. */
callback global blob $State[sizeof(RenderState)];

/* main -> callback world, once: seed the state from main's blob. */
callback i32 Configure(ref blob $Init)
{
    /* rev31zzvb: one whole-blob copy (reference 13.7) instead of 18 field
       assignments -- the two blobs share the RenderState layout. */
    $State = $Init;
    return 0;
}

/* main -> callback world, once per frame: advance the time-based rotation
   and hand back the whole state in the lent blob (the snapshot main
   renders from). Same arithmetic as before, same place in the loop. */
callback i32 Tick(ref blob $Snapshot)
{
    $State assign RenderState;

    /* Time-based rotation (rev31zzeh): the Speed values are degrees
       per 40 ms (one 25 fps frame), so the visual speed is identical
       at any render rate. The delta is clamped so window drags or
       stalls cannot make the cube jump. */
    double $NowMs = (double)timer.MonotonicMs();
    double $DtMs = 40.0;

    if ($State.LastAdvanceMs > 0.0)
    {
        $DtMs = $NowMs - $State.LastAdvanceMs;
        $DtMs = ($DtMs < 0.0) ? 0.0 : ($DtMs > 200.0) ? 200.0 : $DtMs;   /* clamp, as ClampCameraDistance */
    }
    $State.LastAdvanceMs = $NowMs;

    $State.AngleX += ($State.SpeedX * ($DtMs / 40.0));
    $State.AngleY += ($State.SpeedY * ($DtMs / 40.0));
    $State.AngleZ += ($State.SpeedZ * ($DtMs / 40.0));

    $Snapshot = $State;                     /* the whole state, one copy */
    return 0;
}

struct GlEvent packet
{
    u8  magic0;
    u8  magic1;
    u8  magic2;
    u8  magic3;
    u8  version;
    u8  type;
    u8  button;
    u8  pad0;
    i16 x;
    i16 y;
    i16 dx;
    i16 dy;
    i16 wheel;
    u16 width;
    u16 height;
};

void GetCubeFaceGeometry(i32 $Face,          double $CubeGap,
                         ref double $Ax, ref double $Ay     , ref double $Az,
                         ref double $Bx, ref double $By     , ref double $Bz,
                         ref double $Cx, ref double $Cy     , ref double $Cz,
                         ref double $Dx, ref double $Dy     , ref double $Dz)
{
    double $P = CUBE_PANEL_HALF;
    double $D = 1.05 + ($CubeGap * 0.40);

    if ($Face == FACE_FRONT)
    {
        $Ax = -$P; $Ay = -$P; $Az =  $D;
        $Bx =  $P; $By = -$P; $Bz =  $D;
        $Cx =  $P; $Cy =  $P; $Cz =  $D;
        $Dx = -$P; $Dy =  $P; $Dz =  $D;
    }
    else if ($Face == FACE_BACK)
    {
        $Ax =  $P; $Ay = -$P; $Az = -$D;
        $Bx = -$P; $By = -$P; $Bz = -$D;
        $Cx = -$P; $Cy =  $P; $Cz = -$D;
        $Dx =  $P; $Dy =  $P; $Dz = -$D;
    }
    else if ($Face == FACE_RIGHT)
    {
        $Ax =  $D; $Ay = -$P; $Az =  $P;
        $Bx =  $D; $By = -$P; $Bz = -$P;
        $Cx =  $D; $Cy =  $P; $Cz = -$P;
        $Dx =  $D; $Dy =  $P; $Dz =  $P;
    }
    else if ($Face == FACE_LEFT)
    {
        $Ax = -$D; $Ay = -$P; $Az = -$P;
        $Bx = -$D; $By = -$P; $Bz =  $P;
        $Cx = -$D; $Cy =  $P; $Cz =  $P;
        $Dx = -$D; $Dy =  $P; $Dz = -$P;
    }
    else if ($Face == FACE_TOP)
    {
        $Ax = -$P; $Ay =  $D; $Az =  $P;
        $Bx =  $P; $By =  $D; $Bz =  $P;
        $Cx =  $P; $Cy =  $D; $Cz = -$P;
        $Dx = -$P; $Dy =  $D; $Dz = -$P;
    }
    else
    {
        $Ax = -$P; $Ay = -$D; $Az = -$P;
        $Bx =  $P; $By = -$D; $Bz = -$P;
        $Cx =  $P; $Cy = -$D; $Cz =  $P;
        $Dx = -$P; $Dy = -$D; $Dz =  $P;
    }
}

void FitTextureQuad(double $Ax, double $Ay, double $Az,
                    double $Bx, double $By, double $Bz,
                    double $Cx, double $Cy, double $Cz,
                    double $Dx, double $Dy, double $Dz,
                    i32    $TexW,
                    i32    $TexH,
                    ref double $Q0x, ref double $Q0y, ref double $Q0z,
                    ref double $Q1x, ref double $Q1y, ref double $Q1z,
                    ref double $Q2x, ref double $Q2y, ref double $Q2z,
                    ref double $Q3x, ref double $Q3y, ref double $Q3z)
{
    double $Aspect;
    double $ScaleX = 1.0;
    double $ScaleY = 1.0;
    double $StartX;
    double $StartY;
    double $EndX;
    double $EndY;
    double $L0x; double $L0y; double $L0z;
    double $R0x; double $R0y; double $R0z;
    double $L1x; double $L1y; double $L1z;
    double $R1x; double $R1y; double $R1z;

    if (($TexW <= 0) || ($TexH <= 0))
    {
        $Q0x = $Ax; $Q0y = $Ay; $Q0z = $Az;
        $Q1x = $Bx; $Q1y = $By; $Q1z = $Bz;
        $Q2x = $Cx; $Q2y = $Cy; $Q2z = $Cz;
        $Q3x = $Dx; $Q3y = $Dy; $Q3z = $Dz;
        return;
    }

    $Aspect = (double)$TexW / (double)$TexH;
    if ($Aspect > 1.0)
    {
        $ScaleY = 1.0 / $Aspect;
    }
    else
    {
        $ScaleX = $Aspect;
    }

    $StartX = (1.0 - $ScaleX) * 0.5;
    $EndX   = 1.0 - $StartX;
    $StartY = (1.0 - $ScaleY) * 0.5;
    $EndY   = 1.0 - $StartY;

    $L0x = $Ax + (($Dx - $Ax) * $StartY); $L0y = $Ay + (($Dy - $Ay) * $StartY); $L0z = $Az + (($Dz - $Az) * $StartY);
    $R0x = $Bx + (($Cx - $Bx) * $StartY); $R0y = $By + (($Cy - $By) * $StartY); $R0z = $Bz + (($Cz - $Bz) * $StartY);
    $L1x = $Ax + (($Dx - $Ax) * $EndY);   $L1y = $Ay + (($Dy - $Ay) * $EndY);   $L1z = $Az + (($Dz - $Az) * $EndY);
    $R1x = $Bx + (($Cx - $Bx) * $EndY);   $R1y = $By + (($Cy - $By) * $EndY);   $R1z = $Bz + (($Cz - $Bz) * $EndY);

    $Q0x = $L0x + (($R0x - $L0x) * $StartX); $Q0y = $L0y + (($R0y - $L0y) * $StartX); $Q0z = $L0z + (($R0z - $L0z) * $StartX);
    $Q1x = $L0x + (($R0x - $L0x) * $EndX);   $Q1y = $L0y + (($R0y - $L0y) * $EndX);   $Q1z = $L0z + (($R0z - $L0z) * $EndX);
    $Q2x = $L1x + (($R1x - $L1x) * $EndX);   $Q2y = $L1y + (($R1y - $L1y) * $EndX);   $Q2z = $L1z + (($R1z - $L1z) * $EndX);
    $Q3x = $L1x + (($R1x - $L1x) * $StartX); $Q3y = $L1y + (($R1y - $L1y) * $StartX); $Q3z = $L1z + (($R1z - $L1z) * $StartX);
}

void DrawTexturedQuad(i32 $TextureSlot,
                      double $Ax, double $Ay, double $Az,
                      double $Bx, double $By, double $Bz,
                      double $Cx, double $Cy, double $Cz,
                      double $Dx, double $Dy, double $Dz)
{
    gl.Enable(GL_TEXTURE_2D);
    gl.Enable(GL_BLEND);
    gl.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    /* Alpha test: transparent pixels (alpha <= 0.003) get DISCARDED
       before the depth-buffer write, so a logo's transparent corners
       don't block the opposite face of the cube from showing through. */
    gl.Enable(GL_ALPHA_TEST);
    gl.AlphaFunc(GL_GREATER, 0.003);
    gl.BindTexture(GL_TEXTURE_2D, $TextureSlot);
    gl.Color3f(1.0, 1.0, 1.0);
    gl.Begin(GL_QUADS);
    gl.TexCoord2f(0.0, 1.0); gl.Vertex3f($Ax, $Ay, $Az);
    gl.TexCoord2f(1.0, 1.0); gl.Vertex3f($Bx, $By, $Bz);
    gl.TexCoord2f(1.0, 0.0); gl.Vertex3f($Cx, $Cy, $Cz);
    gl.TexCoord2f(0.0, 0.0); gl.Vertex3f($Dx, $Dy, $Dz);
    gl.End();
    gl.Disable(GL_ALPHA_TEST);
}

void DrawQuadEdges(double $EdgeR, double $EdgeG, double $EdgeB,
                   double $Ax, double $Ay, double $Az,
                   double $Bx, double $By, double $Bz,
                   double $Cx, double $Cy, double $Cz,
                   double $Dx, double $Dy, double $Dz)
{
    gl.Disable(GL_TEXTURE_2D);
    gl.Color3f($EdgeR, $EdgeG, $EdgeB);
    gl.Begin(GL_LINES);
    gl.Vertex3f($Ax, $Ay, $Az); gl.Vertex3f($Bx, $By, $Bz);
    gl.Vertex3f($Bx, $By, $Bz); gl.Vertex3f($Cx, $Cy, $Cz);
    gl.Vertex3f($Cx, $Cy, $Cz); gl.Vertex3f($Dx, $Dy, $Dz);
    gl.Vertex3f($Dx, $Dy, $Dz); gl.Vertex3f($Ax, $Ay, $Az);
    gl.End();
}

void DrawCubeFace(i32 $Face, i32 $TextureSlot, i32 $TexW, i32 $TexH,
                  double $CubeGap,
                  double $EdgeR, double $EdgeG, double $EdgeB)
{
    double $Ax  = 0.0; double $Ay  = 0.0; double $Az  = 0.0;
    double $Bx  = 0.0; double $By  = 0.0; double $Bz  = 0.0;
    double $Cx  = 0.0; double $Cy  = 0.0; double $Cz  = 0.0;
    double $Dx  = 0.0; double $Dy  = 0.0; double $Dz  = 0.0;
    double $Q0x = 0.0; double $Q0y = 0.0; double $Q0z = 0.0;
    double $Q1x = 0.0; double $Q1y = 0.0; double $Q1z = 0.0;
    double $Q2x = 0.0; double $Q2y = 0.0; double $Q2z = 0.0;
    double $Q3x = 0.0; double $Q3y = 0.0; double $Q3z = 0.0;

    GetCubeFaceGeometry($Face, $CubeGap,
                        &$Ax, &$Ay, &$Az,
                        &$Bx, &$By, &$Bz,
                        &$Cx, &$Cy, &$Cz,
                        &$Dx, &$Dy, &$Dz);

    FitTextureQuad($Ax, $Ay, $Az,
                   $Bx, $By, $Bz,
                   $Cx, $Cy, $Cz,
                   $Dx, $Dy, $Dz,
                   $TexW, $TexH,
                   &$Q0x, &$Q0y, &$Q0z,
                   &$Q1x, &$Q1y, &$Q1z,
                   &$Q2x, &$Q2y, &$Q2z,
                   &$Q3x, &$Q3y, &$Q3z);

    DrawTexturedQuad($TextureSlot,
                     $Q0x, $Q0y, $Q0z,
                     $Q1x, $Q1y, $Q1z,
                     $Q2x, $Q2y, $Q2z,
                     $Q3x, $Q3y, $Q3z);

    DrawQuadEdges($EdgeR, $EdgeG, $EdgeB,
                  $Ax, $Ay, $Az,
                  $Bx, $By, $Bz,
                  $Cx, $Cy, $Cz,
                  $Dx, $Dy, $Dz);
}

void DrawBackgroundGradient(double $TopR,    double $TopG,    double $TopB,
                            double $BottomR, double $BottomG, double $BottomB)
{
    double $FarZ        = -18.0;
    double $SkyTop      =   7.0;
    double $SkyBottom   =  -2.0;
    double $SkyHalfW    =  18.0;

    gl.Disable(GL_TEXTURE_2D);
    gl.Begin(GL_QUADS);
    gl.Color3f($TopR, $TopG, $TopB);
    gl.Vertex3f(-$SkyHalfW, $SkyTop,    $FarZ);
    gl.Vertex3f(      $SkyHalfW, $SkyTop,    $FarZ);
    gl.Color3f($BottomR, $BottomG, $BottomB);
    gl.Vertex3f(      $SkyHalfW, $SkyBottom, $FarZ);
    gl.Vertex3f(-$SkyHalfW, $SkyBottom, $FarZ);
    gl.End();
}

void DrawGroundGradient(double $NearR, double $NearG, double $NearB,
                        double $FarR,  double $FarG,  double $FarB)
{
    double $GroundY  = -2.3;
    double $GridNear = -3.0;
    double $GridFar  = -18.0;

    gl.Disable(GL_TEXTURE_2D);
    gl.Begin(GL_QUADS);
    gl.Color3f($NearR, $NearG, $NearB);
    gl.Vertex3f(-12.0, $GroundY, $GridNear);
    gl.Vertex3f( 12.0, $GroundY, $GridNear);
    gl.Color3f ($FarR, $FarG, $FarB);
    gl.Vertex3f( 20.0, $GroundY, $GridFar);
    gl.Vertex3f(-20.0, $GroundY, $GridFar);
    gl.End();
}

void SetupPerspective(i32 $WindowWidth, i32 $WindowHeight,
                      double $ViewScale, double $CameraDistance)
{
    double $Aspect;
    double $Top;
    double $Right;
    i32    $H;

    /* rev31zzhg: the guard reads best as the C conditional idiom. */
    $H = ($WindowHeight <= 0) ? 1 : $WindowHeight;

    $Aspect = (double)$WindowWidth / (double)$H;
    $Top    = $ViewScale;
    $Right  = $Top * $Aspect;

    gl.MatrixMode(GL_PROJECTION);
    gl.LoadIdentity();
    gl.Frustum(-$Right, $Right, -$Top, $Top, 1.0, 20.0);
    gl.MatrixMode(GL_MODELVIEW);
    gl.LoadIdentity();
    gl.Translatef(0.0, 0.0, -$CameraDistance);
}

void RenderScriptFrame(ref blob $S)
{
    $S assign RenderState;

    if (gl.MakeCurrent() == 0)
    {
        return;
    }

    /* The rotation was advanced by Tick (callback world) just before
       this call; $S is that snapshot. */

    gl.Viewport  (0, 0, $S.WindowWidth, $S.WindowHeight);
    gl.ClearColor(0.015, 0.020, 0.035, 1.0);
    gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    gl.Enable(GL_DEPTH_TEST);
    gl.Enable(GL_BLEND);
    gl.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    SetupPerspective($S.WindowWidth, $S.WindowHeight,
                     $S.ViewScale, $S.CameraDistance);

    DrawBackgroundGradient(0.01, 0.02, 0.08, 0.18, 0.07, 0.18);
    DrawGroundGradient    (0.16, 0.16, 0.16, 0.08, 0.08, 0.08);

    gl.Rotatef($S.AngleX, 1.0, 0.0, 0.0);
    gl.Rotatef($S.AngleY, 0.0, 1.0, 0.0);
    gl.Rotatef($S.AngleZ, 0.0, 0.0, 1.0);
    gl.LineWidth($S.EdgeWidth);

    DrawCubeFace(FACE_FRONT , $S.VideoSlot, VIDEO_WIDTH, VIDEO_HEIGHT,$S.CubeGap, $S.EdgeR, $S.EdgeG, $S.EdgeB);
    DrawCubeFace(FACE_BACK  , $S.VideoSlot, VIDEO_WIDTH, VIDEO_HEIGHT,$S.CubeGap, $S.EdgeR, $S.EdgeG, $S.EdgeB);
    DrawCubeFace(FACE_RIGHT , $S.VideoSlot, VIDEO_WIDTH, VIDEO_HEIGHT,$S.CubeGap, $S.EdgeR, $S.EdgeG, $S.EdgeB);
    DrawCubeFace(FACE_LEFT  , $S.VideoSlot, VIDEO_WIDTH, VIDEO_HEIGHT,$S.CubeGap, $S.EdgeR, $S.EdgeG, $S.EdgeB);
    DrawCubeFace(FACE_TOP   , $S.LogoSlot , LOGO_WIDTH , LOGO_HEIGHT ,$S.CubeGap, $S.EdgeR, $S.EdgeG, $S.EdgeB);
    DrawCubeFace(FACE_BOTTOM, $S.LogoSlot , LOGO_WIDTH , LOGO_HEIGHT ,$S.CubeGap, $S.EdgeR, $S.EdgeG, $S.EdgeB);

    gl.SwapBuffers();
    gl.UnbindCurrent();
}

callback i32 OnVideoFrame(ref blob $FramePacket)
{
    $State assign RenderState;
    gl.UpdateTexture($State.VideoSlot, $FramePacket);
    return 0;
}

double ClampCameraDistance(double $Distance)
{
    /* rev31zzhg: the classic C clamp idiom — the right-associative
       '?:' chain (rev31zzgy) reads top-to-bottom like the old ifs. */
    return ($Distance < 1.5)  ? 1.5
         : ($Distance > 20.0) ? 20.0
         :                      $Distance;
}

callback i32 OnOpenGlEvent(ref blob $EventPacket)
{
    $State assign RenderState;
    $EventPacket assign GlEvent;

    if ($EventPacket.type == GL_EVENT_RESIZE)
    {
        $State.WindowWidth  = (i32)$EventPacket.width;
        $State.WindowHeight = (i32)$EventPacket.height;
    }
    else if ($EventPacket.type == GL_EVENT_MOUSE_WHEEL)
    {
        if ($EventPacket.wheel > 0)
        {
            $State.CameraDistance -= (0.25 * (double)$EventPacket.wheel);
        }
        else if ($EventPacket.wheel < 0)
        {
            $State.CameraDistance += (0.25 * (double)(-$EventPacket.wheel));
        }
        $State.CameraDistance = ClampCameraDistance($State.CameraDistance);
    }
    else if ($EventPacket.type == GL_EVENT_MOUSE_DOWN)
    {
        if ($EventPacket.button == GL_MOUSE_LEFT)
        {
            $State.SpeedX = 0.0;
            $State.SpeedY = 0.0;
            $State.SpeedZ = 0.0;
        }
    }
    else if ($EventPacket.type == GL_EVENT_MOUSE_UP)
    {
        if ($EventPacket.button == GL_MOUSE_LEFT)
        {
            $State.SpeedX = AUTO_SPEED_X;
            $State.SpeedY = AUTO_SPEED_Y;
            $State.SpeedZ = AUTO_SPEED_Z;
        }
    }
    else if ($EventPacket.type == GL_EVENT_MOUSE_MOVE)
    {
        if ($EventPacket.button == GL_MOUSE_LEFT)
        {
            $State.AngleY += ((double)$EventPacket.dx * 0.50);
            $State.AngleX += ((double)$EventPacket.dy * 0.50);
        }
    }

    return 0;
}

i32 main()
{
    i32  $Video             = 0;
    i32  $Opened            = 0;
    i32  $Delivered         = 0;

    blob $LogoPacket[0];

    string $EnvDisplay [256];
    string $EnvXdg     [256];
    string $EnvSession [256];

    if ((os.HasFlag("--help") != 0) || (os.HasFlag("-help") != 0))
    {
        printf("Usage: %s [options]\n", os.ScriptPath());
        print.Line("");
        print.Line("Options:");
        print.Line("  --help    Show this help message and exit.");
        print.Line("");
        print.Line("Same scene as opengl_ffmpeg_callback_demo, but every drawing");
        print.Line("decision lives in this script (gl.Begin/Vertex3f/Frustum/...).");
        print.Line("Compare side-by-side to see what the thick gl.* facade was");
        print.Line("hiding.");
        print.Line("");
        print.Line("Mouse wheel zooms the camera; left-drag rotates the cube.");
        print.Line("Close the window to exit.");
        print.Line("");
        print.Line("If no display is available, the demo will SKIP gracefully");
        print.Line("(prints a single SKIP line and exits 0).");
        return(0);
    }

    $EnvDisplay = os.GetEnv("DISPLAY");
    $EnvXdg     = os.GetEnv("XDG_RUNTIME_DIR");
    $EnvSession = os.GetEnv("SESSIONNAME");
    if (($EnvDisplay.Length == 0) && ($EnvXdg.Length == 0) && ($EnvSession.Length == 0))
    {
        print.Line("opengl-ffmpeg-script-driven: SKIP - display not available (need X11/Wayland on Linux, or a desktop session on Windows).");
        return(0);
    }

    $Opened = gl.Open(WINDOW_WIDTH, WINDOW_HEIGHT, "Domin OpenGL FFmpeg script-driven demo");
    if ($Opened == 0)
    {
        print.Line("opengl-ffmpeg-script-driven: SKIP - gl.Open returned 0 (SDL2 init or window creation failed).");
        return(0);
    }

    /* main's own copy of the state: seeded here, then refreshed by Tick
       every frame (the snapshot the render code reads). */
    blob $S[sizeof(RenderState)];
    {
        $S assign RenderState;

        $S.WindowWidth    = WINDOW_WIDTH;
        $S.WindowHeight   = WINDOW_HEIGHT;
        $S.CameraDistance = 4.2;
        $S.ViewScale      = 0.45;
        $S.AngleX         =       18.0;
        $S.AngleY         = -24.0;
        $S.AngleZ         = 0.0;
        $S.SpeedX         = AUTO_SPEED_X;
        $S.SpeedY         = AUTO_SPEED_Y;
        $S.SpeedZ         = AUTO_SPEED_Z;
        $S.LastAdvanceMs  = 0.0;
        $S.CubeGap        = 0.45;
        $S.EdgeR          = 0.95;
        $S.EdgeG          = 0.95;
        $S.EdgeB          = 0.80;
        $S.EdgeWidth      = 3.0;

        $S.VideoSlot = gl.CreateTextureSlot();
        $S.LogoSlot  = gl.CreateTextureSlot();
    }
    /* seed the callback-world state (one synchronous crossing) */
    if (worker.Call("Configure", ref $S, 1000) < 0)
    {
        print.Line("opengl-ffmpeg-script-driven: Configure call failed.");
        gl.Close();
        return 1;
    }

    /* the logo PNG as an RGBA pixel packet from the sdl2_image adapter -- the same
       packet image.Load gives; gl.UpdateTexture eats either (the blob is the interface) */
    $LogoPacket = sdl2image.Load(ProjectAssetPath("Assets/Images/domin_logo.png"));

    gl.UpdateTexture($S.LogoSlot, $LogoPacket);

    ffmpeg.SetOutputSize(VIDEO_WIDTH, VIDEO_HEIGHT);
    ffmpeg.SetRealtime(1);
    ffmpeg.SetVideoOnly(1);
    if (ffmpeg.FileExists(ProjectAssetPath("Assets/Videos/demo.mp4")) != 0)
    {
        $Video = ffmpeg.Open(ProjectAssetPath("Assets/Videos/demo.mp4"));
        ffmpeg.StartAudio($Video);
    }
    else
    {
        ffmpeg.SetRealtime(0);
        $Video = ffmpeg.Open("synthetic:640x360");
    }

    while (gl.IsOpen() != 0)
    {
        gl.PumpEvents("OnOpenGlEvent");

        if (gl.IsOpen() == 0)
        {
            break;
        }

        /* rev31zzej: the render loop is clocked by vsync (SwapBuffers,
           ~60 fps), not by the video. PollNext is non-blocking: 1 = a
           new frame arrived (the callback updated the texture), 0 = no
           new frame yet (we still render — the cube keeps moving on
           its time-based rotation), -1 = end of stream -> loop the
           video. The video paints at its own rate underneath. */
        $Delivered = ffmpeg.PollNext($Video, "OnVideoFrame");
        if ($Delivered == -1)
        {
            ffmpeg.Rewind($Video);
        }

        if (gl.IsOpen() != 0)
        {
            /* one crossing per frame: advance in the callback world,
               snapshot into $S, render from $S */
            if (worker.Call("Tick", ref $S, 1000) >= 0)
            {
                RenderScriptFrame(ref $S);
            }
        }
    }

    ffmpeg.Close($Video);
    gl.Close();
    return 0;
}