{
    This file is part of Chentrah,
    Copyright (C) 2004-2014 Anton Rzheshevski (chebmaster@mail.ru).

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see http://www.gnu.org/licenses/

 **********************************************************************}


unit mo_viewport;

{$include mo_globaldefs.h}

interface

uses
  SysUtils, Classes, math, chepersy, mo_hub, typinfo, mo_classes, mo_resources,
  mo_glsl, mo_fbo,
  mo_menu, mo_gmathbase;

type
  TViewPort = class (TRectControl)
  protected
    FBO: TFbo;
    MlsPresentProg: TGlslProgram;
    CurrentMultiSample: integer;
    prevW, prevH, frw, frh: float;
    renW, renH: integer;

  public
    procedure RegisterFields; override;
    procedure OnResize; override;
    procedure AfterLoading; override;


    procedure MouseToVector(mouseX, mouseY: ffloat; var origin, normal: TffVector); virtual;
    procedure RenderScene; virtual;

    procedure CheckRenderBuffersSizes; virtual;
    procedure ClearFbo; virtual;

    //because we use our own implementation of MSAA with blackjack and hookers!
    procedure CreateMultisamplePresentationProgram(newmls: integer); virtual;

    procedure Render; override;

    procedure RenderFboToScreen; virtual;
  end;

//  https://www.opengl.org/wiki/Framebuffer_Object_Examples
//  https://www.opengl.org/wiki/Rectangle_Texture
//  http://nehe.gamedev.net/article/glsl_an_introduction/25007/
//  http://www.gamedev.ru/code/articles/GLSL?page=4


implementation

  procedure TViewPort.RegisterFields;
  begin
    RegClass(TFbo);
    inherited;
    ListFields([
      'fbo', @Fbo, typeinfo(TFbo),
      '-MlsPresentProg', @MlsPresentProg, CPS_POINTER,
      '-cmsm', @CurrentMultiSample, typeinfo(integer),//skipped
      '-prevW', @prevW, '-prevH', @prevH, '-frw', @frw, '-frh', @frh, typeinfo(float),// skipped
      '-renW', @renW, '-renH', @renH, typeinfo(integer)
    ])
  end;


  procedure TViewPort.OnResize;
  begin
    inherited;
    CheckRenderBuffersSizes;
  end;

  procedure TViewPort.RenderScene;
  begin
    //NOT IMPLEMENTED YET -- see TScene, TCamera
  end;

  procedure TViewPort.MouseToVector(mouseX, mouseY: ffloat; var origin, normal: TffVector);
  begin
    //NOT IMPLEMENTED YET -- see TScene, TCamera
  end;


  procedure TViewPort.CheckRenderBuffersSizes;
  var
    needW, needH, roundmask: integer;
    qff, desiredW, desiredH: float;
  begin
    qff:= sqrt(MotherState^.QF / 100); //quality factor aka global detalization multiplier
    //QF is capped at 999, which gives max qff = 3.16
    // which is also the reasonable upper limit of such a crude method of FSAA.
    if (MotherState^.QF >= 100) and (MotherState^.QF < 120) then begin
      renW:= trunc(Width.Current);
      renH:= trunc(Height.Current);
      desiredW:= Width.current;
      desiredH:= Height.current;
    end else begin
      desiredW:= Width.current * qff;
      desiredH:= Height.current * qff;
      renW:= trunc(desiredW);
      renH:= trunc(desiredH);
    end;

    if not MotherState^.OGLNpotSupported then begin
      desiredW:= math.min(Width.current, desiredW);
      desiredH:= math.min(Height.current, desiredH);
    end;
    roundmask:= math.Max(256, GetHigherPowerOf2(round(desiredW / 4))) - 1;
    needW:= Math.min(MotherState^.OGLFboMax, Math.max(256, ValidateTextureDimension(((round(desiredW) - 1) or roundmask) + 1))); //round up to nearest 256
    roundmask:= math.Max(256, GetHigherPowerOf2(round(desiredH / 4))) - 1;
    needH:= Math.min(MotherState^.OGLFboMax, Math.max(256, ValidateTextureDimension(((round(desiredH) - 1) or roundmask) + 1))); //round up to nearest 256

    if  not Assigned(Fbo) or Fbo.GetWasLost
    or (Fbo.width < needW) or (Fbo.height < needH)
    or (Fbo.width > needW * 2) or (Fbo.height > needH * 2)
    then begin
      if Assigned(Fbo) then Fbo.Free;
      Fbo:= TFbo.Create(needW, needH);
    end;

    renW:= min(renW, Fbo.Width);
    renH:= min(renH, Fbo.Height);
    frW:= renW / Fbo.Width;
    frH:= renH / Fbo.Height;
  end;

  procedure TViewPort.CreateMultisamplePresentationProgram(newmls: integer);
  var
    sampler_code: AnsiString = '';
    sampler_rx, sampler_ry, a, adelta, dx, dy, sr, srdelta: float;
    i, steps, step, ssum, perstep: integer;
    function csign(f: float): AnsiString;
    begin
      if f < 0 then Result:= '' else Result:= '+';
    end;
  begin
    if Assigned(MlsPresentProg) then begin
      MlsPresentProg.DeleteShaders;
      MlsPresentProg.Free;
    end;
    steps:= 1 + ((newmls - 1) div 8);
    ssum:= 1;
    for step:=1 to steps do begin
      perstep:= math.min(8, newmls - ssum);
      adelta:= 3.1416 * 2 / perstep;
      a:= 3.1416 * 1.0; //0.75; //45 degrees
      sampler_rx:= 0.7 * ((1 + steps - step)/steps) * FrW / math.max(1.0, Width.Current);
      sampler_ry:= 0.7 * ((1 + steps - step)/steps) * FrH / math.max(1.0, Height.Current);
      for i:= 1 to perstep do begin
        dx:= sin(a) * sampler_rx;
        dy:= cos(a) * sampler_ry;
        sampler_code+= 'sum += texture2D(u_texture, vec2(gl_TexCoord[0].x '
           + csign(dx) + ' ' + FloatToStr(dx) + ', gl_TexCoord[0].y '
           + csign(dy) + ' ' + FloatToStr(dy) + ')) * '
           + FloatToStr(1 / newmls) + '; ';
        a+= adelta;
      end;
      ssum+= perstep;
    end;
    MlsPresentProg:= TGlslProgram.Create(
      TVertexShader.Create( //emulate FFP
        'void main() {'
         + 'gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;'
         + 'gl_FrontColor = gl_Color;'
         + 'gl_TexCoord[0] = gl_MultiTexCoord0;'
       +'}'
      ),
      TFragmentShader.Create(
         'uniform sampler2D u_texture;'
       + 'void main(void) {'
         + 'vec4 sum = vec4(0.0);'
         + 'sum += texture2D(u_texture, vec2(gl_TexCoord[0].x, gl_TexCoord[0].y)) * ' + FloatToStr(1 / newmls) + ';'
         + sampler_code
       //  + 'gl_FragColor = texture2D(u_texture, vec2(gl_TexCoord[0]));'
         + 'gl_FragColor = sum * gl_Color;'
       + '}'
      )
    );
  end;

  procedure TViewPort.AfterLoading;
  begin

  end;

  var
    SQNumCycles: double = 0.0;
    SQMicrosecondsTotal: double = 0.0;

  procedure TViewPort.ClearFbo;
  var
    q: qword;
    d: double;
  begin
    if glqScissorFailed in MotherState^.OGLQuirks then begin
      //the quirk is confirmed. Clear the FBO by rendering a quad
      glUseProgram(0);
      glViewport(0, 0, Math.min(renW + (renW div 16), Fbo.Width), Math.min(renH + (renH div 16), Fbo.Height));
      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();
      glOrtho(0, 1, 0, 1, 1 , 0);
      glMatrixMode(GL_MODELVIEW);
      glLoadIdentity();
      glDisable(GL_BLEND);
      glDisable(GL_ALPHA_TEST);
      glDepthMask( GL_TRUE );
      glEnable(GL_DEPTH_TEST); glDepthFunc(GL_ALWAYS);
      glDisable (GL_TEXTURE_2D);
      glcolor4f(0, 0, 0, 0);
      glBegin(GL_QUADS);
        glVertex3f(0, 0, 0);
        glVertex3f(0, 1.05, 0);
        glVertex3f(1.05, 1.05, 0);
        glVertex3f(1.05, 0, 0);
      glEnd;
    end else begin
      UsecByTsc(@q);
      glScissor(0, 0, Math.min(renW + (renW div 16), Fbo.Width), Math.min(renH + (renH div 16), Fbo.Height));
      glEnable(GL_SCISSOR_TEST);
      glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
      glDisable(GL_SCISSOR_TEST);
      SQMicrosecondsTotal+= Math.min(20000.0, UsecByTsc(@q));
        //clipping, bitches. Because shit happens, like system slowdown
        //or hibernation at the wrong moment
      SQNumCycles+= 1.0;
      if (SQNumCycles > 100.0) //enough recurrences to confirm it
      and (SQMicrosecondsTotal / SQNumCycles > 10000.0) // there's no middle
        //ground. It is either negligible when everything is all right
        //  or horrific when the driver falls to software
      then begin
        MotherState^.OGLQuirks += [glqScissorFailed]; //gets saved in the config
        AddLog(RuEn(
          'Драйвер OpenGL свалился в программную прорисовку при использовании glScissor + glClear.'
            + #10#13'  Вывих учтён, использую очистку FBO прорисовкой прямоугольника.',
          'OpenGL driver fell back to software while using glScissor + glClear.'
            + #10#13'  Quirk registered, using full-screen quad to clear FBO.'));
      end;
    end;
  end;




  procedure TViewPort.Render;
  var
    msm: integer;
  begin
    CheckRenderBuffersSizes;

    if (MotherState^.QF < 120) //or not MotherState^.OGLNpotSupported
      then msm:= 1
      else msm:= 3 + trunc (MotherState^.QF / 50); //5 minimum
      // Fuck, these are nor related! I can have 20 samples with 8 texture coords max.
      //min(MotherState^.OGLMaxTextureCoords, 1 + trunc (MotherState^.QF / 100));
                               //round(sqrt(MotherState^.QF * sqr(2) / 120)));
    if (msm <> CurrentMultiSample)
    or ((msm > 1) and (
      {lazy shader recreating (cuz passing uniform parameters is, you know,
         _hard_, so I hardcode them and rebuild the shader as necessary) }
      (Width.Current > prevW * 1.1) or (Width.Current * 1.1 < prevW)
      or (Height.Current > prevH * 1.1) or (Height.Current * 1.1 < prevH)
    ))
    then begin
      if msm > 1
        then CreateMultisamplePresentationProgram(msm)
        else
          if Assigned(MlsPresentProg) then begin
            MlsPresentProg.Free;
            pointer(MlsPresentProg):= nil;
          end;
      CurrentMultiSample:= msm;
      prevW:= Width.Current;
      prevH:= Height.Current;
    end;


    Fbo.Bind;

    RenderScene;

    glBindFramebuffer(GL_FRAMEBUFFER_EXT, 0);

    RenderFboToScreen;
  end;


  procedure TViewPort.RenderFboToScreen;
  begin
    SetGlStatesFor2dRender;

    glEnable(GL_ALPHA_TEST);
    glAlphaFunc(GL_GREATER, 0.01);
    glEnable(GL_BLEND);
    glDisable(GL_DEPTH_TEST);
    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_TEXTURE_2D);

    FBO.texture.Bind;

    if (CurrentMultiSample > 1) and Assigned(MlsPresentProg) then MlsPresentProg.Use;

    glColor4f(MotherState^.FadeIn, MotherState^.FadeIn, MotherState^.FadeIn, MotherState^.FadeIn);

    glBegin(GL_QUADS);
      glTexCoord2f(0, 0);
      glVertex2f(x, y); //bottom left corner
      glTexCoord2f(0, frH);
      glVertex2f(x, y + Height.Current);
      glTexCoord2f(frW, frH);
      glVertex2f(x + Width.Current, y + Height.Current);
      glTexCoord2f(frW, 0);
      glVertex2f(x + Width.Current, y);
    glEnd;

    glBindTexture(GL_TEXTURE_2D, 0);

    glUseProgram(0);
  end;

end.
