Function is ambiguos in c++ - c++

I'm trying to use a library I found for triangulations, and I'm getting a strange error. When I compile everything, I get the following errors:
'_trytoadd' is ambiguous ' Candidates are: void _trytoadd(Se<SeDcdtVertex,SeDcdtEdge,SeDcdtFace>
*, Se<SeDcdtVertex,SeDcdtEdge,SeDcdtFace> *, int, const GsVec2 &, const GsVec2 &, const GsVec2
&) '
I'm getting this error for _cantpass, _try to add and _ptreeaddent functions, but they are all defined and called properly in this .cpp file. Any idea what's wrong? Here's the code for the class in question:
/*=======================================================================
Copyright 2010 Marcelo Kallmann. All Rights Reserved.
This software is distributed for noncommercial use only, without
any warranties, and provided that all copies contain the full copyright
notice licence.txt located at the base folder of the distribution.
=======================================================================*/
# include <math.h>
# include <stdlib.h>
# include "gs_geo2.h"
# include "se_lct.h"
# include "se_triangulator_internal.h"
# define GS_TRACE_ONLY_LINES
//# define GS_USE_TRACE1 // main search method
//# define GS_USE_TRACE2 // search expansion
//# define GS_USE_TRACE3
# include "gs_trace.h"
//================================================================================
//========================== search path tree ====================================
//================================================================================
// nen/nex are the entrance/exit edges of the node being expanded, which is already in the search tree
// en/ex are the entrance/exit edge of the current traversal being evaluated for expansion
// (p1,p2) are en coordinates
bool SeLct::_canpass ( SeDcdtSymEdge* nen, SeDcdtSymEdge* nex, SeDcdtSymEdge* en, SeDcdtSymEdge* ex,
const GsPnt2& p1, const GsPnt2& p2, const GsPnt2& p3, float r, float d2 )
{
// check if can traverse en/ex traversal:
if ( _man->is_constrained(ex->edg()) ) return false;
// test if next triangle being tested has been already visited:
if ( _mesh->marked(en->fac()) ) return false;
if ( en->nxt()==ex ) // bot
{
if ( _pre_clearance )
{ float cl = ex->edg()->cl(ex);
if ( cl<d2 ) return false;
}
else
{ if ( dist2(p2,p3)<d2 ) return false;
if ( !_sector_clear(ex->nxt(),d2,p2,p3,p1) ) return false;
}
if ( nex->fac()==_fi && nex->nxt()==nen ) // top->bot departure transition
{ if ( !_local_transition_free(ex,en,d2,_xi,_yi) ) return false;
}
}
else // top
{
if ( _pre_clearance )
{ float cl = en->edg()->cl(en);
if ( cl<d2 ) return false;
}
else
{ if ( dist2(p3,p1)<d2 ) return false;
if ( !_sector_clear(en->nxt(),d2,p1,p2,p3) ) return false;
}
if ( nex->fac()==_fi && nen->nxt()==nex ) // bot->top departure transition
{ if ( !_local_transition_free(ex,en,d2,_xi,_yi) ) return false;
}
}
return true;
}
// en is the entrance edge, ex the exit edge, (p1,p2) are en coordinates
void SeLct::_trytoadd ( SeDcdtSymEdge* en, SeDcdtSymEdge* ex, int mi, const GsPnt2& p1, const GsPnt2& p2, const GsPnt2& p3 )
{
// verify if it is passable:
PathNode& n = _ptree->nodes[mi];
if ( !_canpass ( (SeDcdtSymEdge*)n.en, (SeDcdtSymEdge*)n.ex, en, ex, p1, p2, p3, _ptree->radius, _ptree->diam2 ) ) return;
// ok it is passable, compute cost:
double x, y;
if ( en->nxt()==ex )
_getcostpoint ( &n, n.x, n.y, p2.x, p2.y, p3.x, p3.y, x, y, _ptree->radius ); // bot
else
_getcostpoint ( &n, n.x, n.y, p3.x, p3.y, p1.x, p1.y, x, y, _ptree->radius ); // top
// insert:
# define PTDIST(a,b,c,d) float(sqrt(gs_dist2(a,b,c,d)))
_ptree->add_child ( mi, en, ex, n.ncost+PTDIST(n.x,n.y,x,y), PTDIST(x,y,_xg,_yg), x,y ); // A* heuristic
# undef PTDIST
}
# define ExpansionNotFinished -1
# define ExpansionBlocked -2
int SeLct::_expand_lowest_cost_leaf ()
{
int min_i;
if ( _ptree->leafs.size()>_maxfronts ) _maxfronts=_ptree->leafs.size();
min_i = _ptree->lowest_cost_leaf ();
GS_TRACE2 ( "Expanding leaf: "<<min_i );
if ( min_i<0 ) return ExpansionBlocked; // no more leafs: path could not be found!
// attention: array references may be invalidated due array reallocation during insertion
SeDcdtSymEdge* s = (SeDcdtSymEdge*) _ptree->nodes[min_i].ex->sym();
SeDcdtSymEdge* sn = s->nxt();
SeDcdtSymEdge* sp = sn->nxt();
const GsPnt2& p1 = s->vtx()->p; // note: s is in the triangle to expand
const GsPnt2& p2 = sn->vtx()->p;
const GsPnt2& p3 = sp->vtx()->p;
float d2 = _ptree->diam2;
// test if next triangle contains goal point:
if ( gs_in_triangle(p1.x,p1.y,p2.x,p2.y,p3.x,p3.y,_xg,_yg) ) // reached goal triangle !
{ GS_TRACE1 ( "Goal triangle reached..." );
double r = _ptree->radius;
if ( !pt2circfree(this,s,_xg,_yg,r) ) // we do not know if the goal location is valid, so test it now
{ GS_TRACE1 ( "Goal location is invalid." );
return ExpansionBlocked;
}
GS_TRACE1 ( "Goal location valid." );
GS_TRACE1 ( "Analyzing arrival..." );
SeDcdtSymEdge* nen = (SeDcdtSymEdge*)_ptree->nodes[min_i].en;
SeDcdtSymEdge* nex = (SeDcdtSymEdge*)_ptree->nodes[min_i].ex;
_analyze_arrival ( s, 3, r, d2, nen, nex );
if ( _ent[3].type==EntBlocked )
{ if ( (sn->edg()->is_constrained()||dist2(p2,p3)<d2) &&
(sp->edg()->is_constrained()||dist2(p3,p1)<d2) )
{ GS_TRACE1 ( "Arrival blocked from all possible entries." );
return ExpansionBlocked;
}
else
{ GS_TRACE1 ( "Arrival is blocked, but search can continue..." );
// at this point the arrival is not valid but the search will continue, and
// note that the arrival triangle may still be used as passage so we let
// the expansion tests in _trytoadd() proceed.
}
}
else
{ GS_TRACE1 ( "Arrival tests passed." );
GS_TRACE1 ( "Arrival is valid " << (_ent[3].type==EntTrivial?"and trivial.":"but non trivial.") );
return min_i; // FOUND!
}
}
int nsize = _ptree->nodes.size();
_trytoadd ( s, sn, min_i, p1, p2, p3 ); // bot
_trytoadd ( s, sp, min_i, p1, p2, p3 ); // top
if ( _ptree->nodes.size()>nsize ) _mesh->mark ( s->fac() ); // only mark traversed faces
if (_searchcb) _searchcb(_sudata);
return ExpansionNotFinished; // continue the expansion
}
void SeLct::_ptreeaddent ( SeDcdtSymEdge* s, bool top, bool edge )
{
const GsPnt2& p1 = s->vtx()->p;
const GsPnt2& p2 = s->nvtx()->p;
double x, y;
if ( edge )
{ x=_xi; y=_yi; }
else
{ _getcostpoint ( 0, _xi, _yi, p1.x, p1.y, p2.x, p2.y, x, y, _ptree->radius ); }
// insert:
# define PTDIST(a,b,c,d) (float)sqrt(gs_dist2(a,b,c,d))
_ptree->add_child ( -1, top? s->nxt():s->nxn(), s, PTDIST(_xi,_yi,x,y), PTDIST(x,y,_xg,_yg), x, y );
# undef PTDIST
}
//================================================================================
//============================== search path =====================================
//================================================================================
/* - This is the A* algorithm that takes O(nf), f is the faces in the "expansion frontier". */
bool SeLct::_search_channel ( double x1, double y1, double x2, double y2, float radius, const SeFace* iniface )
{
GS_TRACE1 ( "Starting Search Path..." );
if ( !_ptree ) _ptree = new PathTree;
_clear_path(); // clear data from previous query and set _path_result to NoPath
_channel.size(0);
_xi=x1; _yi=y1; _xg=x2; _yg=y2;
if ( !iniface ) return false;
// Even if p1 is on an edge, locate_point will return in s a face that
// can be considered to contain p1 (p1 would be invalid if in a vertex)
SeBase *s;
LocateResult res=locate_point ( iniface, x1, y1, s );
if ( res==NotFound )
{ GS_TRACE1 ( "Could not locate first point!" );
_path_result=NoPath;
return false;
}
_fi = s->fac(); // save initial face
if ( !pt1circfree(this,s,x1,y1,radius) ) { _path_result=NoPath; return false; }
// Check if we are to solve trivial or local paths, testing if both points are in the same triangle:
if ( _man->in_triangle(s->vtx(),s->nxt()->vtx(),s->nxn()->vtx(),x2,y2) )
{ GS_TRACE1 ( "Both points are in the same triangle..." );
if ( radius==0 )
{ GS_TRACE1 ( "Trivial path returned." );
_path_result=TrivialPath; return true; // this is it
}
if ( !pt2circfree(this,s,x2,y2,radius) )
{ GS_TRACE1 ( "Goal point in same triangle invalid. No path returned." );
_path_result=NoPath; return false;
}
_path_result = _analyze_local_path ( s, radius );
if ( _path_result==TrivialPath )
{ GS_TRACE1 ( "Capsule free. Trivial path returned." );
return true; // path exists
}
else if ( _path_result==LocalPath )
{ GS_TRACE1 ( "Deformable capsule is passable. Local path returned." );
return true; // path exists
}
// at this point the result may be a GlobalPath or a NoPath
GS_TRACE1 ( "Deformable capsule is not passable." );
// we then let the normal entrance analysis and search to proceed.
// the entrance that blocked the capsule will also be blocked but
// there may be a global path to get there so we just do not
// mark the initial face as visited, allowing it to be found by the global search.
}
GS_TRACE1 ( "Searching for a global path..." );
GS_TRACE1 ( "Analyzing entrances..." );
_analyze_entrances ( s, _xi, _yi, radius );
GS_TRACE1 ( "Entrance 0: "<<(_ent[0].type==EntBlocked?"blocked":_ent[0].type==EntTrivial?"trivial":"not trivial") );
GS_TRACE1 ( "Entrance 1: "<<(_ent[1].type==EntBlocked?"blocked":_ent[1].type==EntTrivial?"trivial":"not trivial") );
GS_TRACE1 ( "Entrance 2: "<<(_ent[2].type==EntBlocked?"blocked":_ent[2].type==EntTrivial?"trivial":"not trivial") );
GS_TRACE1 ( "Initializing A* search..." );
_mesh->begin_marking ();
_ptree->init ( radius );
if ( _ent[0].type!=EntBlocked ) _ptreeaddent ( _ent[0].s, _ent[0].top, res==EdgeFound? true:false );
if ( _ent[1].type!=EntBlocked ) _ptreeaddent ( _ent[1].s, _ent[1].top, false );
if ( _ent[2].type!=EntBlocked ) _ptreeaddent ( _ent[2].s, _ent[2].top, false );
if (_searchcb) _searchcb(_sudata);
GS_TRACE1 ( "Expanding leafs..." );
int found = ExpansionNotFinished;
while ( found==ExpansionNotFinished )
found = _expand_lowest_cost_leaf();
_mesh->end_marking ();
if ( found==ExpansionBlocked )
{ GS_TRACE1 ( "Points are not connectable!" );
_path_result = NoPath;
return false;
}
_finalsearchnode = found;
int n = found; // the starting leaf
s = _ptree->nodes[n].ex->sym();
do { _channel.push() = _ptree->nodes[n].ex;
n = _ptree->nodes[n].parent;
} while ( n!=-1 );
_channel.revert();
GS_TRACE1 ( "Path crosses "<<_channel.size()<<" edges." );
_path_result = GlobalPath;
return true;
}
void SeLct::get_search_nodes ( GsArray<SeBase*>& e )
{
e.size ( 0 );
for ( int i=0; i<_ptree->nodes.size(); i++ )
{ e.push() = _ptree->nodes[i].en;
e.push() = _ptree->nodes[i].ex;
}
}
int SeLct::get_search_nodes () const
{
return _ptree? _ptree->nodes.size() : 0;
}
void SeLct::get_search_metric ( GsArray<GsPnt2>& pnts )
{
pnts.size ( 0 );
if ( !_ptree ) return;
for ( int i=0; i<_ptree->nodes.size(); i++ )
{
PathNode& n = _ptree->nodes[i];
if ( n.parent<0 )
{ pnts.push().set ( _xi, _yi ); }
else
{ PathNode& np = _ptree->nodes[n.parent];
pnts.push().set ( np.x, np.y );
}
pnts.push().set ( n.x, n.y );
}
}
void SeLct::get_search_front ( GsArray<SeBase*>& e )
{
e.size ( 0 );
for ( int i=0; i<_ptree->leafs.size(); i++ )
{ e.push() = _ptree->nodes[ _ptree->leafs.elem(i) ].ex;
}
}
//============================ End of File =================================

Related

MQL4 How to alert when a price reaches certain levels?

I have this free indicator is the name is FiboPiv_v2.mq4.
It's a tool I use with good result in trading scalping. I set normally the alert by hand, but I see that the code is open and so I'd like to do a modification but I'm quite newbie in programming MQL4.
I would like to add a pop-up and sound that says something like "AUDUSD has reached S1".
Code of the indicator:
#property indicator_chart_window
//+------------------------------------------------------------------+
//| Custom indicator initialization function |
//+------------------------------------------------------------------+
int init()
{
//---- indicators
return(0);
}
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function |
//+------------------------------------------------------------------+
int deinit()
{
//----
ObjectDelete("S1");
ObjectDelete("S2");
ObjectDelete("S3");
ObjectDelete("R1");
ObjectDelete("R2");
ObjectDelete("R3");
ObjectDelete("PIVIOT");
ObjectDelete("Support 1");
ObjectDelete("Support 2");
ObjectDelete("Support 3");
ObjectDelete("Piviot level");
ObjectDelete("Resistance 1");
ObjectDelete("Resistance 2");
ObjectDelete("Resistance 3");
Comment(" ");
//----
return(0);
}
//+------------------------------------------------------------------+
//| Custom indicator iteration function |
//+------------------------------------------------------------------+
int start()
{
double rates[1][6],yesterday_close,yesterday_high,yesterday_low;
ArrayCopyRates(rates,Symbol(),PERIOD_D1);
//----
if(DayOfWeek()==1)
{
if(TimeDayOfWeek(iTime(Symbol(),PERIOD_D1,1))==5)
{
yesterday_close= rates[1][4];
yesterday_high = rates[1][3];
yesterday_low=rates[1][2];
}
else
{
for(int d=5;d>=0;d--)
{
if(TimeDayOfWeek(iTime(Symbol(),PERIOD_D1,d))==5)
{
yesterday_close= rates[d][4];
yesterday_high = rates[d][3];
yesterday_low=rates[d][2];
}
}
}
}
else
{
yesterday_close= rates[1][4];
yesterday_high = rates[1][3];
yesterday_low=rates[1][2];
}
//---- Calculate Pivots
Comment("\nYesterday quotations:\nH ",yesterday_high,"\nL
",yesterday_low,"\nC ",yesterday_close);
double R=yesterday_high-yesterday_low;//range
double p=(yesterday_high+yesterday_low+yesterday_close)/3;// Standard Pivot
double r3 = p + (R * 1.000);
double r2 = p + (R * 0.618);
double r1 = p + (R * 0.382);
double s1 = p - (R * 0.382);
double s2 = p - (R * 0.618);
double s3 = p - (R * 1.000);
//----
drawLine(r3,"R3",Lime,0);
drawLabel("Resistance 3",r3,Lime);
drawLine(r2,"R2",Green,0);
drawLabel("Resistance 2",r2,Green);
drawLine(r1,"R1",DarkGreen,0);
drawLabel("Resistance 1",r1,DarkGreen);
drawLine(p,"PIVIOT",Blue,1);
drawLabel("Piviot level",p,Blue);
drawLine(s1,"S1",Maroon,0);
drawLabel("Support 1",s1,Maroon);
drawLine(s2,"S2",Crimson,0);
drawLabel("Support 2",s2,Crimson);
drawLine(s3,"S3",Red,0);
drawLabel("Support 3",s3,Red);
//----
return(0);
}
//+------------------------------------------------------------------+
void drawLabel(string name,double lvl,color Color)
{
if(ObjectFind(name)!=0)
{
if(Bars<10) return;
ObjectCreate(name,OBJ_TEXT,0,Time[10],lvl);
ObjectSetText(name,name,8,"Arial",EMPTY);
ObjectSet(name,OBJPROP_COLOR,Color);
}
else
{
if(Bars<10) return;
ObjectMove(name,0,Time[10],lvl);
}
}
//----
void drawLine(double lvl,string name,color Col,int type)
{
if(ObjectFind(name)!=0)
{
ObjectCreate(name,OBJ_HLINE,0,Time[0],lvl,Time[0],lvl);
if(type==1)
ObjectSet(name,OBJPROP_STYLE,STYLE_SOLID);
else
ObjectSet(name,OBJPROP_STYLE,STYLE_DOT);
ObjectSet(name,OBJPROP_COLOR,Col);
ObjectSet(name,OBJPROP_WIDTH,1);
}
else
{
ObjectDelete(name);
ObjectCreate(name,OBJ_HLINE,0,Time[0],lvl,Time[0],lvl);
if(type==1)
ObjectSet(name,OBJPROP_STYLE,STYLE_SOLID);
else
ObjectSet(name,OBJPROP_STYLE,STYLE_DOT);
ObjectSet(name,OBJPROP_COLOR,Col);
ObjectSet(name,OBJPROP_WIDTH,1);
}
}
//+--------------------------------------------------------------------+
First:
Forget to have an Alert() call inside an Indicator type of MQL4-code. For details ref. below + MQL4 Documentation on such and similarly forbidden blocking calls.
Next :The original code was awfully inefficient
( see details in documentation on New-MQL4.56789, in 2018/Q2 still runs all ( indeed ALL platform's instances of concurrently operated CustomIndicator + Indicators inside one, single, solo, blocking-vulnerable, SPOF-thread (!!) )
A sort of efficiency-repaired code that does not "shaking all trees" bottom up per each QUOTE-arrival, that yet performs all the necessary updates and shows the beeping-alarm scheme:
#property indicator_chart_window
double R, r3, r2, r1, p, s1, s2, s3; // SEMI-GLOBALs for easy runPriceCHECKs()
//+------------------------------------------------------------------+
//| Custom indicator iteration function |
//+------------------------------------------------------------------+
int start()
{
static int aLastVisitedBAR = EMPTY; // INIT
if ( aLastVisitedBAR == iBars( _Symbol, PERIOD_D1 ) ) // TEST:
{ runPriceCHECKs(); // Check Price v/s PIVOT levels
return( 0 ); // but PIVOTs never change during the same day ... so JIT/RET--^
}
else aLastVisitedBAR == iBars( _Symbol, PERIOD_D1 ); // SYNC & RE-CALC:
double rates[][6], yesterday_close,
yesterday_high,
yesterday_low;
ArrayCopyRates( rates,
_Symbol,
PERIOD_D1
);
//----
if ( DayOfWeek() == 1 )
{
if ( TimeDayOfWeek( iTime( _Symbol, PERIOD_D1, 1 ) ) == 5 )
{
yesterday_close = rates[1][4];
yesterday_high = rates[1][3];
yesterday_low = rates[1][2];
}
else
{
for ( int d = 5; d >= 0; d-- )
{
if ( TimeDayOfWeek( iTime( _Symbol, PERIOD_D1, d ) ) == 5 )
{
yesterday_close = rates[d][4];
yesterday_high = rates[d][3];
yesterday_low = rates[d][2];
}
}
}
}
else
{
yesterday_close = rates[1][4];
yesterday_high = rates[1][3];
yesterday_low = rates[1][2];
}
//---- Calculate Pivots
Comment( "\nYesterday quotations:\nH ",
yesterday_high, "\nL ",
yesterday_low, "\nC ",
yesterday_close
);
R = yesterday_high - yesterday_low; // a Day range
p = ( yesterday_high + yesterday_low + yesterday_close ) /3;// a Standard Pivot
r3 = p + ( R * 1.000 ); drawLine( r3, "R3", clrLime, 0 ); drawLabel( "Resistance 3", r3, clrLime );
r2 = p + ( R * 0.618 ); drawLine( r2, "R2", clrGreen, 0 ); drawLabel( "Resistance 2", r2, clrGreen );
r1 = p + ( R * 0.382 ); drawLine( r1, "R1", clrDarkGreen, 0 ); drawLabel( "Resistance 1", r1, clrDarkGreen );
drawLine( p, "PIVOT", clrBlue, 1 ); drawLabel( "Pivot level", p, clrBlue );
s1 = p - ( R * 0.382 ); drawLine( s1, "S1", clrMaroon, 0 ); drawLabel( "Support 1", s1, clrMaroon );
s2 = p - ( R * 0.618 ); drawLine( s2, "S2", clrCrimson, 0 ); drawLabel( "Support 2", s2, clrCrimson );
s3 = p - ( R * 1.000 ); drawLine( s3, "S3", clrRed, 0 ); drawLabel( "Support 3", s3, clrRed );
//----
runPriceCHECKs(); // Check Price v/s PIVOT levels
//----
return( 0 );
}
//+------------------------------------------------------------------+
//| drawLabel( string name, double lvl, color Color ) |
//+------------------------------------------------------------------+
void drawLabel( string name, double lvl, color Color )
{
if ( Bars < 10 ) return;
if ( ObjectFind( name ) == 0 ) ObjectMove( name, 0, Time[10], lvl );
else
{ ObjectCreate( name, OBJ_TEXT, 0, Time[10], lvl );
ObjectSet( name, OBJPROP_COLOR, Color );
ObjectSetText( name, name, 8, "Arial", EMPTY );
}
}
//+------------------------------------------------------------------+
//| drawLine( double lvl, string name, color Col, int type ) |
//+------------------------------------------------------------------+
void drawLine( double lvl, string name, color Col, int type )
{
if ( ObjectFind( name ) == 0 )
ObjectDelete( name );
ObjectCreate( name, OBJ_HLINE, 0, Time[0], lvl,
Time[0], lvl );
ObjectSet( name, OBJPROP_COLOR, Col );
ObjectSet( name, OBJPROP_WIDTH, 1 );
ObjectSet( name, OBJPROP_STYLE, ( type == 1 ) ? STYLE_SOLID : STYLE_DOT );
}
//+------------------------------------------------------------------+
//| runPriceCHECKs() |
//+------------------------------------------------------------------+
void runPriceCHECKs()
{
#define DEF_PROXIMITY_TRESHOLD_R3 ( 15 * Point )
#define DEF_PROXIMITY_TRESHOLD_R2 ( 10 * Point )
#define DEF_PROXIMITY_TRESHOLD_R1 ( 5 * Point )
#define DEF_PROXIMITY_TRESHOLD_P ( 2 * Point )
#define DEF_PROXIMITY_TRESHOLD_S1 ( 5 * Point )
#define DEF_PROXIMITY_TRESHOLD_S2 ( 10 * Point )
#define DEF_PROXIMITY_TRESHOLD_S3 ( 15 * Point )
if ( MathAbs( r3 - Close ) <= DEF_PROXIMITY_TRESHOLD_R3 )
...
PlaySound( aFileNAME_WAV_R3 ); // The file must be located in terminal_directory\Sounds or its sub-directory. Only WAV files are played.
// Sleep( 250 ); // 250 [ms] AS A LAST RESORT, BETTER MAKE .WAV CUT-SHORT NOT TO LAST LONGER BY ITSELF
// PlaySound( NULL );
return;
if ( MathAbs( r2 - Close ) <= DEF_PROXIMITY_TRESHOLD_R2 )
...
return;
if ( MathAbs( r1 - Close ) <= DEF_PROXIMITY_TRESHOLD_R1 )
...
return;
if ( MathAbs( p - Close ) <= DEF_PROXIMITY_TRESHOLD_P )
...
return;
if ( MathAbs( s1 - Close ) <= DEF_PROXIMITY_TRESHOLD_S1 )
...
return;
if ( MathAbs( s2 - Close ) <= DEF_PROXIMITY_TRESHOLD_S2 )
...
return;
if ( MathAbs( s3 - Close ) <= DEF_PROXIMITY_TRESHOLD_S3 )
...
return;
// nop
}
//+------------------------------------------------------------------+
//| Custom indicator initialization function |
//+------------------------------------------------------------------+
int init()
{ return( 0 );
}
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function |
//+------------------------------------------------------------------+
int deinit()
{
//----
ObjectDelete( "S1" );
ObjectDelete( "S2" );
ObjectDelete( "S3" );
ObjectDelete( "R1" );
ObjectDelete( "R2" );
ObjectDelete( "R3" );
ObjectDelete( "PIVOT" );
ObjectDelete( "Support 1" );
ObjectDelete( "Support 2" );
ObjectDelete( "Support 3" );
ObjectDelete( "Piviot level" );
ObjectDelete( "Resistance 1" );
ObjectDelete( "Resistance 2" );
ObjectDelete( "Resistance 3" );
Comment( ":o)" );
//----
return( 0 );
}
//+------------------------------------------------------------------+

Segmentation fault when performing bundle adjustment with g2o

void bundleAdjustment ( const vector< cv::Point3f > points_3d,
const vector< cv::Point2f > points_2d,
const Mat& K, Mat& R, Mat& t )
{
typedef g2o::BlockSolver< g2o::BlockSolverTraits<6,3> > Block;
Block::LinearSolverType* linearSolver = new g2o::LinearSolverCSparseBlock::PoseMatrixType();
Block* solver_ptr = new Block ( linearSolver );
g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg ( solver_ptr );
g2o::SparseOptimizer optimizer;
optimizer.setAlgorithm ( solver );
g2o::VertexSE3Expmap* pose = new g2o::VertexSE3Expmap(); // camera pose
Eigen::Matrix3d R_mat;
R_mat <<
R.at<double> ( 0,0 ), R.at<double> ( 0,1 ), R.at<double> ( 0,2 ),
R.at<double> ( 1,0 ), R.at<double> ( 1,1 ), R.at<double> ( 1,2 ),
R.at<double> ( 2,0 ), R.at<double> ( 2,1 ), R.at<double> ( 2,2 );
pose->setId ( 0 );
pose->setEstimate ( g2o::SE3Quat (R_mat,
Eigen::Vector3d ( t.at ( 0,0 ), t.at ( 1,0 ), t.at ( 2,0 ) ) ) );
optimizer.addVertex ( pose );
int index = 1;
for (const Point3f p:points_3d ) // landmarks, world coordinates.
{
g2o::VertexSBAPointXYZ* point = new g2o::VertexSBAPointXYZ();
point->setId ( index++ );
point->setEstimate ( Eigen::Vector3d ( p.x, p.y, p.z ) );
point->setMarginalized ( true );
optimizer.addVertex ( point );
}
g2o::CameraParameters* camera = new g2o::CameraParameters (K.at<double> ( 0,0 ), Eigen::Vector2d ( K.at<double> ( 0,2 ), K.at<double> ( 1,2 ) ), 0);
camera->setId ( 0 );
optimizer.addParameter ( camera ); //Add camera parameters
index = 1;
for ( const Point2f p:points_2d ) //Add edges(observed data)
{
g2o::EdgeProjectXYZ2UV* edge = new g2o::EdgeProjectXYZ2UV();
edge->setId ( index );
edge->setVertex ( 0, dynamic_cast<g2o::VertexSBAPointXYZ*> ( optimizer.vertex ( index ) ) );
edge->setVertex ( 1, pose );
edge->setMeasurement ( Eigen::Vector2d ( p.x, p.y ) );
edge->setParameterId ( 0,0 ); //cameraparameter
edge->setInformation ( Eigen::Matrix2d::Identity() );
optimizer.addEdge ( edge );
index++;
}
chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
optimizer.setVerbose ( true );
optimizer.initializeOptimization();
optimizer.optimize ( 100 );
chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
chrono::duration<double> time_used = chrono::duration_cast<chrono::duration<double>> ( t2-t1 );
cout<<"optimization costs time: "<<time_used.count() <<" seconds."<<endl;
cout<<endl<<"after optimization:"<<endl;
cout<<"T="<<endl<<Eigen::Isometry3d ( pose->estimate() ).matrix() <<endl;
}
I have defined a function to perform bundle adjustment with g2o, but terminal asserted Segmentation Fault when I ran the executable. I am sure that problem comes from this function because the whole program can run properly after commenting out this function.
My platform is Ubuntu16.04 LTS.
EDIT1: I tried to add set( CMAKE_BUILD_TYPE "Release" ) in my CMakeLists.txt. Then terminal asserted double free or corruption (out) after running the executable. However, it ran properly after I commenting out following code block:
g2o::CameraParameters* camera = new g2o::CameraParameters (K.at<double> ( 0,0 ), Eigen::Vector2d ( K.at<double> ( 0,2 ), K.at<double> ( 1,2 ) ), 0);
camera->setId ( 0 );
optimizer.addParameter ( camera );
EDIT2: The whole program could run properly once I employed default constructor of CameraParameter:
g2o::CameraParameters* camera = new g2o::CameraParameters ();
camera->setId ( 0 );
optimizer.addParameter ( camera );
However, segmentation fault existed still.
EDIT3: No segmentation faults occur any more after commenting out following codes:
optimizer.addVertex ( pose );
optimizer.addVertex ( point );
optimizer.addEdge ( edge );
optimizer.addParameter ( camera )

How to achieve efficient 2d batching?

I am trying to implement sprite batching but I am not quite sure how I should do it.
Texture batching is not very hard, I just group everything by texture id but I am not sure how I should handle the vertex data.
I could do it like this
texture.bind();
gl_quad.bind();
for(auto& quad: quads){
send(quad.matrix);
draw();
}
I would just upload 1 quad to the GPU and then send the matrix as a uniform variable and draw the quad but then I would have 1 draw call for every sprite that I want to draw which is probably not very clever.
Alternatively I could let every sprite have 4 vertices and then I would update them on the CPU, then I would gather all sprites and upload all vertices into one big buffer and bind it.
texture.bind();
auto big_buffer = create_vertex_buffers(quads).bind();
draw();
big_buffer.delete();
I could also use instanced rendering. Upload only one quad, every sprite would have a matrix and then upload all matrices into one buffer and call drawIndirect. I would have to send 9 floats instead of 8 (with the big_buffer version) and I think that drawIndirect is much more expensive than a simple drawcommand.
Are there any other ways that I have missed? What would you recommend?
I can show you a few classes that works with batches and their implementations; but they do rely on other classes. This work is protected by copyright found in the header section of each file.
CommonStructs.h
// Version: 1.0
// Copyright (c) 2012 by Marek A. Krzeminski, MASc
// http://www.MarkeKnows.com
#ifndef COMMON_STRUCTS_H
#define COMMON_STRUCTS_H
namespace vmk {
// GuiVertex ------------------------------------------------------------------
struct GuiVertex {
glm::vec2 position;
glm::vec4 color;
glm::vec2 texture;
GuiVertex( glm::vec2 positionIn, glm::vec4 colorIn, glm::vec2 textureIn = glm::vec2() ) :
position( positionIn ),
color( colorIn ),
texture( textureIn )
{}
}; // GuiVertex
// BatchConfig ----------------------------------------------------------------
struct BatchConfig {
unsigned uRenderType;
int iPriority;
unsigned uTextureId;
float fAlpha;
BatchConfig( unsigned uRenderTypeIn, int iPriorityIn, unsigned uTextureIdIn, float fAlphaIn ) :
uRenderType( uRenderTypeIn ),
iPriority( iPriorityIn ),
uTextureId( uTextureIdIn ),
fAlpha( fAlphaIn )
{}
bool operator==( const BatchConfig& other ) const {
if ( uRenderType != other.uRenderType ||
iPriority != other.iPriority ||
uTextureId != other.uTextureId ||
glm::abs( fAlpha - other.fAlpha ) > 0.004f )
{
return false;
}
return true;
}
bool operator!=( const BatchConfig& other ) const {
return !( *this == other );
}
}; // BatchConfig
} // namespace vmk
#endif // COMMON_STRUCTS_H
Batch.h
// Version: 1.0
// Copyright (c) 2012 by Marek A. Krzeminski, MASc
// http://www.MarkeKnows.com
#ifndef BATCH_H
#define BATCH_H
#include "CommonStructs.h"
namespace vmk {
class ShaderManager;
class Settings;
class Batch sealed {
private:
static Settings* m_pSettings;
static ShaderManager* m_pShaderManager;
unsigned m_uMaxNumVertices;
unsigned m_uNumUsedVertices;
unsigned m_vao;
unsigned m_vbo;
BatchConfig m_config;
GuiVertex m_lastVertex;
// For Debugging Only
unsigned m_uId; // Batch Id
std::vector<std::string> m_vIds; // Id's Of What Is Contained In This Batch
public:
Batch( unsigned uId, unsigned uMaxNumVertices );
~Batch();
bool isBatchConfig( const BatchConfig& config ) const;
bool isEmpty() const;
bool isEnoughRoom( unsigned uNumVertices ) const;
Batch* getFullest( Batch* pBatch );
int getPriority() const;
void add( const std::vector<GuiVertex>& vVertices, const BatchConfig& config );
void add( const std::vector<GuiVertex>& vVertices );
void addId( const std::string& strId );
void render();
private:
Batch( const Batch& c ); // Not Implemented
Batch& operator=( const Batch& c ); // Not Implemented
void cleanUp();
}; // Batch
} // namespace vmk
#endif // BATCH_H
Batch.cpp
// Version: 1.0
// Copyright (c) 2012 by Marek A. Krzeminski, MASc
// http://www.MarkeKnows.com
#include "stdafx.h"
#include "Batch.h"
#include "Logger.h"
#include "Property.h"
#include "Settings.h"
#include "ShaderManager.h"
namespace vmk {
Settings* Batch::m_pSettings = nullptr;
ShaderManager* Batch::m_pShaderManager = nullptr;
// ----------------------------------------------------------------------------
// Batch()
Batch::Batch( unsigned uId, unsigned uMaxNumVertices ) :
m_uMaxNumVertices( uMaxNumVertices ),
m_uNumUsedVertices( 0 ),
m_vao( 0 ),
m_vbo( 0 ),
m_config(GL_TRIANGLE_STRIP, 0, 0, 1.0f ),
m_lastVertex( glm::vec2(), glm::vec4() ),
m_uId( uId ) {
if ( nullptr == m_pSettings ) {
m_pSettings = Settings::get();
}
if ( nullptr == m_pShaderManager ) {
m_pShaderManager = ShaderManager::get();
}
// Optimal Size For A Batch Is Between 1-4MB In Size. Number Of Elements That Can Be Stored In A
// Batch Is Determined By Calculating #Bytes Used By Each Vertex
if ( uMaxNumVertices < 1000 ) {
std::ostringstream strStream;
strStream << __FUNCTION__ << " uMaxNumVertices{" << uMaxNumVertices << "} is too small. Choose a number >= 1000 ";
throw ExceptionHandler( strStream );
}
// Clear Error Codes
glGetError();
if ( m_pSettings->getOpenglVersion().x >= 3 ) {
glGenVertexArrays( 1, &m_vao );
glBindVertexArray( m_vao );
}
// Create Batch Buffer
glGenBuffers( 1, &m_vbo );
glBindBuffer( GL_ARRAY_BUFFER, m_vbo );
glBufferData( GL_ARRAY_BUFFER, uMaxNumVertices * sizeof( GuiVertex ), nullptr, GL_STREAM_DRAW );
if ( m_pSettings->getOpenglVersion().x >= 3 ) {
unsigned uOffset = 0;
m_pShaderManager->enableAttribute( A_POSITION, sizeof( GuiVertex ), uOffset );
uOffset += sizeof( glm::vec2 );
m_pShaderManager->enableAttribute( A_COLOR, sizeof( GuiVertex ), uOffset );
uOffset += sizeof( glm::vec4 );
m_pShaderManager->enableAttribute( A_TEXTURE_COORD0, sizeof( GuiVertex ), uOffset );
glBindVertexArray( 0 );
m_pShaderManager->disableAttribute( A_POSITION );
m_pShaderManager->disableAttribute( A_COLOR );
m_pShaderManager->disableAttribute( A_TEXTURE_COORD0 );
}
glBindBuffer( GL_ARRAY_BUFFER, 0 );
if ( GL_NO_ERROR != glGetError() ) {
cleanUp();
throw ExceptionHandler( __FUNCTION__ + std::string( " failed to create batch" ) );
}
} // Batch
// ----------------------------------------------------------------------------
// ~Batch()
Batch::~Batch() {
cleanUp();
} // ~Batch
// ----------------------------------------------------------------------------
// cleanUp()
void Batch::cleanUp() {
if ( m_vbo != 0 ) {
glBindBuffer( GL_ARRAY_BUFFER, 0 );
glDeleteBuffers( 1, &m_vbo );
m_vbo = 0;
}
if ( m_vao != 0 ) {
glBindVertexArray( 0 );
glDeleteVertexArrays( 1, &m_vao );
m_vao = 0;
}
} // cleanUp
// ----------------------------------------------------------------------------
// isBatchConfig()
bool Batch::isBatchConfig( const BatchConfig& config ) const {
return ( config == m_config );
} // isBatchConfigh
// ----------------------------------------------------------------------------
// isEmpty()
bool Batch::isEmpty() const {
return ( 0 == m_uNumUsedVertices );
} // isEmpty
// ----------------------------------------------------------------------------
// isEnoughRoom()
// Returns True If The Number Of Vertices Passed In Can Be Stored In This Batch
// Without Reaching The Limit Of How Many Vertices Can Fit In The Batch
bool Batch::isEnoughRoom( unsigned uNumVertices ) const {
// 2 Extra Vertices Are Needed For Degenerate Triangles Between Each Strip
unsigned uNumExtraVertices = ( GL_TRIANGLE_STRIP == m_config.uRenderType && m_uNumUsedVertices > 0 ? 2 : 0 );
return ( m_uNumUsedVertices + uNumExtraVertices + uNumVertices <= m_uMaxNumVertices );
} // isEnoughRoom
// ----------------------------------------------------------------------------
// getFullest()
// Returns The Batch That Contains The Most Number Of Stored Vertices Between
// This Batch And The One Passed In
Batch* Batch::getFullest( Batch* pBatch ) {
return ( m_uNumUsedVertices > pBatch->m_uNumUsedVertices ? this : pBatch );
} // getFullest
// ----------------------------------------------------------------------------
// getPriority()
int Batch::getPriority() const {
return m_config.iPriority;
} // getPriority
// ----------------------------------------------------------------------------
// add()
// Adds Vertices To Batch And Also Sets The Batch Config Options
void Batch::add( const std::vector<GuiVertex>& vVertices, const BatchConfig& config ) {
m_config = config;
add( vVertices );
} // add
// ----------------------------------------------------------------------------
// add()
void Batch::add( const std::vector<GuiVertex>& vVertices ) {
// 2 Extra Vertices Are Needed For Degenerate Triangles Between Each Strip
unsigned uNumExtraVertices = ( GL_TRIANGLE_STRIP == m_config.uRenderType && m_uNumUsedVertices > 0 ? 2 : 0 );
if ( uNumExtraVertices + vVertices.size() > m_uMaxNumVertices - m_uNumUsedVertices ) {
std::ostringstream strStream;
strStream << __FUNCTION__ << " not enough room for {" << vVertices.size() << "} vertices in this batch. Maximum number of vertices allowed in a batch is {" << m_uMaxNumVertices << "} and {" << m_uNumUsedVertices << "} are already used";
if ( uNumExtraVertices > 0 ) {
strStream << " plus you need room for {" << uNumExtraVertices << "} extra vertices too";
}
throw ExceptionHandler( strStream );
}
if ( vVertices.size() > m_uMaxNumVertices ) {
std::ostringstream strStream;
strStream << __FUNCTION__ << " can not add {" << vVertices.size() << "} vertices to batch. Maximum number of vertices allowed in a batch is {" << m_uMaxNumVertices << "}";
throw ExceptionHandler( strStream );
}
if ( vVertices.empty() ) {
std::ostringstream strStream;
strStream << __FUNCTION__ << " can not add {" << vVertices.size() << "} vertices to batch.";
throw ExceptionHandler( strStream );
}
// Add Vertices To Buffer
if ( m_pSettings->getOpenglVersion().x >= 3 ) {
glBindVertexArray( m_vao );
}
glBindBuffer( GL_ARRAY_BUFFER, m_vbo );
if ( uNumExtraVertices > 0 ) {
// Need To Add 2 Vertex Copies To Create Degenerate Triangles Between This Strip
// And The Last Strip That Was Stored In The Batch
glBufferSubData( GL_ARRAY_BUFFER, m_uNumUsedVertices * sizeof( GuiVertex ), sizeof( GuiVertex ), &m_lastVertex );
glBufferSubData( GL_ARRAY_BUFFER, ( m_uNumUsedVertices + 1 ) * sizeof( GuiVertex ), sizeof( GuiVertex ), &vVertices[0] );
}
// TODO: Use glMapBuffer If Moving Large Chunks Of Data > 1MB
glBufferSubData( GL_ARRAY_BUFFER, ( m_uNumUsedVertices + uNumExtraVertices ) * sizeof( GuiVertex ), vVertices.size() * sizeof( GuiVertex ), &vVertices[0] );
if ( m_pSettings->getOpenglVersion().x >= 3 ) {
glBindVertexArray( 0 );
}
glBindBuffer( GL_ARRAY_BUFFER, 0 );
m_uNumUsedVertices += vVertices.size() + uNumExtraVertices;
m_lastVertex = vVertices[vVertices.size() - 1];
} // add
// ----------------------------------------------------------------------------
// addId()
void Batch::addId( const std::string& strId ) {
m_vIds.push_back( strId );
} // addId
// ----------------------------------------------------------------------------
// render()
void Batch::render() {
if ( m_uNumUsedVertices == 0 ) {
// Nothing In This Buffer To Render
return;
}
bool usingTexture = INVALID_UNSIGNED != m_config.uTextureId;
m_pShaderManager->setUniform( U_USING_TEXTURE, usingTexture );
if ( usingTexture ) {
m_pShaderManager->setTexture( 0, U_TEXTURE0_SAMPLER_2D, m_config.uTextureId );
}
m_pShaderManager->setUniform( U_ALPHA, m_config.fAlpha );
// Draw Contents To Buffer
if ( m_pSettings->getOpenglVersion().x >= 3 ) {
glBindVertexArray( m_vao );
glDrawArrays( m_config.uRenderType, 0, m_uNumUsedVertices );
glBindVertexArray( 0 );
} else { // OpenGL v2.x
glBindBuffer( GL_ARRAY_BUFFER, m_vbo );
unsigned uOffset = 0;
m_pShaderManager->enableAttribute( A_POSITION, sizeof( GuiVertex ), uOffset );
uOffset += sizeof( glm::vec2 );
m_pShaderManager->enableAttribute( A_COLOR, sizeof( GuiVertex ), uOffset );
uOffset += sizeof( glm::vec4 );
m_pShaderManager->enableAttribute( A_TEXTURE_COORD0, sizeof( GuiVertex ), uOffset );
glDrawArrays( m_config.uRenderType, 0, m_uNumUsedVertices );
m_pShaderManager->disableAttribute( A_POSITION );
m_pShaderManager->disableAttribute( A_COLOR );
m_pShaderManager->disableAttribute( A_TEXTURE_COORD0 );
glBindBuffer( GL_ARRAY_BUFFER, 0 );
}
if ( m_pSettings->isDebugLoggingEnabled( Settings::DEBUG_RENDER ) ) {
std::ostringstream strStream;
strStream << std::setw( 2 ) << m_uId << " | "
<< std::left << std::setw( 10 );
if ( GL_LINES == m_config.uRenderType ) {
strStream << "Lines";
} else if ( GL_TRIANGLES == m_config.uRenderType ) {
strStream << "Triangles";
} else if ( GL_TRIANGLE_STRIP == m_config.uRenderType ) {
strStream << "Tri Strips";
} else if ( GL_TRIANGLE_FAN == m_config.uRenderType ) {
strStream << "Tri Fan";
} else {
strStream << "Unknown";
}
strStream << " | " << std::right
<< std::setw( 6 ) << m_config.iPriority << " | "
<< std::setw( 7 ) << m_uNumUsedVertices << " | "
<< std::setw( 5 );
if ( INVALID_UNSIGNED != m_config.uTextureId ) {
strStream << m_config.uTextureId;
} else {
strStream << "None";
}
strStream << " |";
for each( const std::string& strId in m_vIds ) {
strStream << " " << strId;
}
m_vIds.clear();
Logger::log( strStream );
}
// Reset Buffer
m_uNumUsedVertices = 0;
m_config.iPriority = 0;
} // render
} // namespace vmk
BatchManager.h
// Version: 1.0
// Copyright (c) 2012 by Marek A. Krzeminski, MASc
// http://www.MarekKnows.com
#ifndef BATCH_MANAGER_H
#define BATCH_MANAGER_H
#include "Singleton.h"
#include "CommonStructs.h"
namespace vmk {
class Batch;
class BatchManager sealed : public Singleton {
private:
std::vector<std::shared_ptr<Batch>> m_vBatches;
unsigned m_uNumBatches;
unsigned m_maxNumVerticesPerBatch;
public:
BatchManager( unsigned uNumBatches, unsigned numVerticesPerBatch );
virtual ~BatchManager();
static BatchManager* const get();
void render( const std::vector<GuiVertex>& vVertices, const BatchConfig& config, const std::string& strId );
void emptyAll();
protected:
private:
BatchManager( const BatchManager& c ); // Not Implemented
BatchManager& operator=( const BatchManager& c); // Not Implemented
void emptyBatch( bool emptyAll, Batch* pBatchToEmpty );
//void renderBatch( const std::vector<GuiVertex>& vVertices, const BatchConfig& config );
}; // BatchManager
} // namespace vmk
#endif // BATCH_MANAGER_H
BatchManager.cpp
// Version: 1.0
// Copyright (c) 2012 by Marek A. Krzeminski, MASc
// http://www.MarekKnows.com
#include "stdafx.h"
#include "BatchManager.h"
#include "Batch.h"
#include "Logger.h"
#include "Settings.h"
namespace vmk {
static BatchManager* s_pBatchManager = nullptr;
static Settings* s_pSettings = nullptr;
// ----------------------------------------------------------------------------
// BatchManager()
BatchManager::BatchManager( unsigned uNumBatches, unsigned numVerticesPerBatch ) :
Singleton( TYPE_BATCH_MANAGER ),
m_uNumBatches( uNumBatches ),
m_maxNumVerticesPerBatch( numVerticesPerBatch ) {
// Test Input Parameters
if ( uNumBatches < 10 ) {
std::ostringstream strStream;
strStream << __FUNCTION__ << " uNumBatches{" << uNumBatches << "} is too small. Choose a number >= 10 ";
throw ExceptionHandler( strStream );
}
// A Good Size For Each Batch Is Between 1-4MB In Size. Number Of Elements That Can Be Stored In A
// Batch Is Determined By Calculating #Bytes Used By Each Vertex
if ( numVerticesPerBatch < 1000 ) {
std::ostringstream strStream;
strStream << __FUNCTION__ << " numVerticesPerBatch{" << numVerticesPerBatch << "} is too small. Choose A Number >= 1000 ";
throw ExceptionHandler( strStream );
}
// Create Desired Number Of Batches
m_vBatches.reserve( uNumBatches );
for ( unsigned u = 0; u < uNumBatches; ++u ) {
m_vBatches.push_back( std::shared_ptr<Batch>( new Batch( u, numVerticesPerBatch ) ) );
}
s_pSettings = Settings::get();
s_pBatchManager = this;
} // BatchManager
// ----------------------------------------------------------------------------
// ~BatchManager()
BatchManager::~BatchManager() {
s_pBatchManager = nullptr;
m_vBatches.clear();
} // ~BatchManager
// ----------------------------------------------------------------------------
// get()
BatchManager* const BatchManager::get() {
if ( nullptr == s_pBatchManager ) {
throw ExceptionHandler( __FUNCTION__ + std::string( " failed, BatchManager has not been constructed yet" ) );
}
return s_pBatchManager;
} // get
// ----------------------------------------------------------------------------
// render()
void BatchManager::render( const std::vector<GuiVertex>& vVertices, const BatchConfig& config, const std::string& strId ) {
Batch* pEmptyBatch = nullptr;
Batch* pFullestBatch = m_vBatches[0].get();
// Determine Which Batch To Put The Vertices Into
for ( unsigned u = 0; u < m_uNumBatches; ++u ) {
Batch* pBatch = m_vBatches[u].get();
if ( pBatch->isBatchConfig( config ) ) {
if ( !pBatch->isEnoughRoom( vVertices.size() ) ) {
// First Need To Empty This Batch Before Adding Anything To It
emptyBatch( false, pBatch );
if ( s_pSettings->isDebugLoggingEnabled( Settings::DEBUG_RENDER ) ) {
Logger::log( "Forced batch to empty to make room for vertices" );
}
}
if ( s_pSettings->isDebugLoggingEnabled( Settings::DEBUG_RENDER ) ) {
pBatch->addId( strId );
}
pBatch->add( vVertices );
return;
}
// Store Pointer To First Empty Batch
if ( nullptr == pEmptyBatch && pBatch->isEmpty() ) {
pEmptyBatch = pBatch;
}
// Store Pointer To Fullest Batch
pFullestBatch = pBatch->getFullest( pFullestBatch );
}
// If We Get Here Then We Didn't Find An Appropriate Batch To Put The Vertices Into
// If We Have An Empty Batch, Put Vertices There
if ( nullptr != pEmptyBatch ) {
if ( s_pSettings->isDebugLoggingEnabled( Settings::DEBUG_RENDER ) ) {
pEmptyBatch->addId( strId );
}
pEmptyBatch->add( vVertices, config );
return;
}
// No Empty Batches Were Found Therefore We Must Empty One First And Then We Can Use It
emptyBatch( false, pFullestBatch );
if ( s_pSettings->isDebugLoggingEnabled( Settings::DEBUG_RENDER ) ) {
Logger::log( "Forced fullest batch to empty to make room for vertices" );
pFullestBatch->addId( strId );
}
pFullestBatch->add( vVertices, config );
} // render
// ----------------------------------------------------------------------------
// emptyAll()
void BatchManager::emptyAll() {
emptyBatch( true, m_vBatches[0].get() );
if ( s_pSettings->isDebugLoggingEnabled( Settings::DEBUG_RENDER ) ) {
Logger::log( "Forced all batches to empty" );
}
} // emptyAll
// ----------------------------------------------------------------------------
// CompareBatch
struct CompareBatch : public std::binary_function<Batch*, Batch*, bool> {
bool operator()( const Batch* pBatchA, const Batch* pBatchB ) const {
return ( pBatchA->getPriority() > pBatchB->getPriority() );
} // operator()
}; // CompareFunctor
// ----------------------------------------------------------------------------
// emptyBatch()
// Empties The Batches According To Priority. If emptyAll() Is False Then
// Only Empty The Batches That Are Lower Priority Than The One Specified
// AND Also Empty The One That Is Passed In
void BatchManager::emptyBatch( bool emptyAll, Batch* pBatchToEmpty ) {
// Sort Bathes By Priority
std::priority_queue<Batch*, std::vector<Batch*>, CompareBatch> queue;
for ( unsigned u = 0; u < m_uNumBatches; ++u ) {
// Add All Non-Empty Batches To Queue Which Will Be Sorted By Order
// From Lowest To Highest Priority
if ( !m_vBatches[u]->isEmpty() ) {
if ( emptyAll ) {
queue.push( m_vBatches[u].get() );
} else if ( m_vBatches[u]->getPriority() < pBatchToEmpty->getPriority() ) {
// Only Add Batches That Are Lower In Priority
queue.push( m_vBatches[u].get() );
}
}
}
// Render All Desired Batches
while ( !queue.empty() ) {
Batch* pBatch = queue.top();
pBatch->render();
queue.pop();
}
if ( !emptyAll ) {
// When Not Emptying All The Batches, We Still Want To Empty
// The Batch That Is Passed In, In Addition To All Batches
// That Have Lower Priority Than It
pBatchToEmpty->render();
}
} // emptyBatch
} // namespace vmk
Now these classes will not compile directly for they depend and rely on other class objects: Settings, Properties, ShaderManager, Logger, And those objects depend on other objects as well. This is coming from a large scale working OpenGL Graphics Rendering & Game Engine using OpenGL Shaders. This is working source code, optimally bug free.
This may serve as a guide as to how one would design a batch process. And may give insight into the things to consider for example: The types of vertices being rendering { Lines, Triangles, TriangleStrip, TriangleFan etc. }, Priority of where to draw an object based on if it has transparencies or not, Handling Degenerate Triangles with the vertices when creating a batch object.
The way that this is designed is that only matching batch types will fit in the same bucket, and the bucket will try to fill itself, if it is too full to hold the vertices it will then look for another bucket to see if one is available, if no buckets are available it will then search to see which is the fullest and it will empty them from the priority queue to send the vertices to the video card to be rendered.
This is tied into a ShaderManager that manages how OpenGL defines and sets up shader programs and linking them to a program, it is also tied in to an AssetStorage class which is not found here but found in the ShaderManager. This system handles a complete custom GUI, Sprites, Fonts, Textures etc.
If you would like to learn more I would highly suggest visiting www.MarekKnows.com and checking out his Video Tutorial Series on OpenGL; for this specific application you would need to follow his Shader Engine series!
It's worth noting that sprite rendering is only really expensive from the standpoint of the context switches in between each sprite rendered. Rendering a quad for each sprite tends to be a trivial expense in comparison.
Instancing here of the geometry data is likely to hinder more than help, since the cost of using a separate transformation matrix per quad rendered tends to outweigh the expense of just uploading a fresh set of vertex attributes per quad. Instancing works best when you have at least moderately complex geometry, like hundreds to thousands of vertices.
Typically if speed is your primary goal, the top priority here is to coalesce your texture data into "sprite sheet"-style texture atlases. The first goal is to have as few texture context switches as possible, and typically far beyond a separate texture image per sprite/frame. This would also make instancing further impractical because each quad or pair of triangles you render would then tend to vary wildly in terms of their texture coordinates.
If you actually reach this point where you have as few texture context switches as possible and want more speed for a bunch of dynamic sprites, then the next practical step (but with diminishing returns) might be to use streaming VBOs. You can fill a streaming VBO with the vertex attributes required to render the tris/quads for the current frame (with different vertex positions and texture coordinates) and then draw the VBO. For the best performance, it might help to chunk the VBOs and not fill them with all the geometry data of your entire scene per frame with a strategy where you fill and draw, fill and draw, fill and draw multiple times per frame.
Nevertheless, since you asked about instancing (which implies to me that you're using a separate image per sprite), your first and biggest gain will probably come from using texture atlases and reducing the texture context switches even further. The geometry-side optimization is a totally separate process and you might do fine for quite a while even using immediate mode here. It would be towards a finishing touch for optimization where you start optimizing that towards streaming VBOs.
This is all with the assumption of dynamic sprites that either move around on the screen or change images. For static tile-style images that never change, you can store their vertex attributes into a static VBO and potentially benefit from instancing (but there we're instancing a boatload of tiles per static VBO, and therefore each static VBO might have hundreds to thousands of vertices each).

Spherical coordinate representation (using Ogre maths constructs) fails tests

In my project I need to position things around other things in a spherical way, so I figured I needed a spherical coordinate representation. I'm using Ogre3D graphic rendering engine which provide some maths constructs which at the moment I use everywhere in my project.
The source code of Ogre is available there, in the 1.9 branch as I'm using an old version of it. I checked yesterday that the maths types didn't change in a long time so my code is relying on the code you can see there, in particular Maths, Vector3 and Quaternion. Here I'm assuming that Ogre's code is correct, but you never know, which is why I'm pointing to it as my code is relying heavily on it. Also know that the referential used in Ogre is right handed, with -Z being the default orientation of any entity, +Y being up of the screen, +X being the right side of the screen, and +Z being the direction toward you, -Z being the direction toward the screen.
Now, here is the spherical coordinate code I have a problem with:
#ifndef HGUARD_NETRUSH_ZONEVIEW_SPHERICAL_HPP__
#define HGUARD_NETRUSH_ZONEVIEW_SPHERICAL_HPP__
#include <iosfwd>
#include <OgreMath.h>
#include <OgreVector3.h>
#include <OgreQuaternion.h>
#include "api.hpp"
namespace netrush {
namespace zoneview {
/** Spherical coordinates vector, used for spherical coordinates and transformations.
Some example values:
( radius = 1.0, theta = 0.0deg , phi = 0.0deg ) <=> Y unit vector in cartesian space
( radius = 1.0, theta = 90.0deg, phi = 0.0deg ) <=> Z unit vector in cartesian space
( radius = 1.0, theta = 90.0deg , phi = 90.0deg ) <=> X unit vector in cartesian space
*/
struct SphereVector
{
Ogre::Real radius; ///< Rho or Radius is the distance from the center of the sphere.
Ogre::Radian theta; ///< Theta is the angle around the x axis (latitude angle counterclockwise), values range from 0 to PI.
Ogre::Radian phi; ///< Phi is the angle around the y axis (longitude angle counterclockwise), values range from 0 to 2PI.
NETRUSH_ZONEVIEW_API static const SphereVector ZERO;
NETRUSH_ZONEVIEW_API static const SphereVector UNIT_X;
NETRUSH_ZONEVIEW_API static const SphereVector UNIT_Y;
NETRUSH_ZONEVIEW_API static const SphereVector UNIT_Z;
NETRUSH_ZONEVIEW_API static const SphereVector NEGATIVE_UNIT_X;
NETRUSH_ZONEVIEW_API static const SphereVector NEGATIVE_UNIT_Y;
NETRUSH_ZONEVIEW_API static const SphereVector NEGATIVE_UNIT_Z;
SphereVector() = default;
SphereVector( Ogre::Real radius, Ogre::Radian theta, Ogre::Radian phi )
: radius( std::move(radius) ), theta( std::move(theta) ), phi( std::move(phi) )
{}
explicit SphereVector( const Ogre::Vector3& cartesian_vec )
{
*this = from_cartesian( cartesian_vec );
}
void normalize()
{
using namespace Ogre;
while( phi > Degree(360.f) ) phi -= Degree(360.f);
while( theta > Degree(180.f) ) theta -= Degree(180.f);
while( phi < Radian(0) ) phi += Degree(360.f);
while( theta < Radian(0) ) theta += Degree(180.f);
}
SphereVector normalized() const
{
SphereVector svec{*this};
svec.normalize();
return svec;
}
/** #return a relative Cartesian vector coordinate from this relative spherical coordinate. */
Ogre::Vector3 to_cartesian() const
{
using namespace Ogre;
const auto svec = normalized();
Vector3 result;
result.x = radius * Math::Sin( svec.phi ) * Math::Sin( svec.theta );
result.z = radius * Math::Cos( svec.phi ) * Math::Sin( svec.theta );
result.y = radius * Math::Cos( svec.theta );
return result;
}
/** #return a relative spherical coordinate from a cartesian vector. */
static SphereVector from_cartesian( const Ogre::Vector3& cartesian )
{
using namespace Ogre;
SphereVector result = SphereVector::ZERO;
result.radius = cartesian.length();
if( result.radius == 0 )
return result;
result.phi = Math::ATan2( cartesian.x, cartesian.z );
result.theta = Math::ATan2( Math::Sqrt( Math::Sqr( cartesian.x ) + Math::Sqr( cartesian.z ) ), cartesian.y );
result.normalize();
return result;
}
friend SphereVector operator-( const SphereVector& value )
{
SphereVector result;
result.radius = -value.radius;
result.theta = -value.theta;
result.phi = -value.phi;
return result;
}
friend SphereVector operator+( const SphereVector& left, const SphereVector& right )
{
SphereVector result;
result.radius = left.radius + right.radius;
result.theta = left.theta + right.theta;
result.phi = left.phi + right.phi;
return result;
}
friend SphereVector operator-( const SphereVector& left, const SphereVector& right )
{
return left + (-right);
}
SphereVector& operator+=( const SphereVector& other )
{
*this = *this + other;
return *this;
}
SphereVector& operator-=( const SphereVector& other )
{
*this = *this - other;
return *this;
}
/// Rotation of the position around the relative center of the sphere.
friend SphereVector operator*( const SphereVector& sv, const Ogre::Quaternion& rotation )
{
const auto cartesian_vec = sv.to_cartesian();
const auto rotated_vec = rotation * cartesian_vec;
SphereVector result { rotated_vec };
result.normalize();
return result;
}
/// Rotation of the position around the relative center of the sphere.
friend SphereVector operator*( const Ogre::Quaternion& rotation, const SphereVector& sv ) { return sv * rotation; }
/// Rotation of the position around the relative center of the sphere.
SphereVector& operator*=( const Ogre::Quaternion& rotation )
{
*this = *this * rotation;
return *this;
}
friend bool operator==( const SphereVector& left, const SphereVector& right )
{
return Ogre::Math::RealEqual( left.radius, right.radius )
&& left.phi == right.phi
&& left.theta == right.theta
;
}
friend bool operator!=( const SphereVector& left, const SphereVector& right )
{
return !( left == right );
}
};
inline std::ostream& operator<<( std::ostream& out, const SphereVector& svec )
{
out << "{ radius = " << svec.radius
<< ", theta = " << svec.theta
<< ", phi = " << svec.phi
<< " }";
return out;
}
inline bool real_equals( const SphereVector& left, const SphereVector& right, Ogre::Real tolerance = 1e-03 )
{
using namespace Ogre;
return Math::RealEqual( left.radius, right.radius, tolerance )
&& Math::RealEqual( left.theta.valueAngleUnits(), right.theta.valueAngleUnits(), tolerance )
&& Math::RealEqual( left.phi.valueAngleUnits(), right.phi.valueAngleUnits(), tolerance )
;
}
}}
#endif
The constants are defined in the cpp:
#include "spherical.hpp"
namespace netrush {
namespace zoneview {
const SphereVector SphereVector::ZERO( 0.f, Ogre::Radian( 0.f ), Ogre::Radian( 0.f ) );
const SphereVector SphereVector::UNIT_X( Ogre::Vector3::UNIT_X );
const SphereVector SphereVector::UNIT_Y( Ogre::Vector3::UNIT_Y );
const SphereVector SphereVector::UNIT_Z( Ogre::Vector3::UNIT_Z );
const SphereVector SphereVector::NEGATIVE_UNIT_X( Ogre::Vector3::NEGATIVE_UNIT_X );
const SphereVector SphereVector::NEGATIVE_UNIT_Y( Ogre::Vector3::NEGATIVE_UNIT_Y );
const SphereVector SphereVector::NEGATIVE_UNIT_Z( Ogre::Vector3::NEGATIVE_UNIT_Z );
}}
The failing test (the lines marked ast FAILURE):
inline bool check_compare( SphereVector left, SphereVector right )
{
std::cout << "----"
<< "\nComparing "
<< "\n Left: " << left
<< "\n Right: " << right
<< std::endl;
return real_equals( left, right );
}
// ...
TEST( Test_SphereVector, axe_rotation_quaternion )
{
using namespace Ogre;
const auto init_svec = SphereVector::NEGATIVE_UNIT_Z;
static const auto ROTATION_TO_X = Vector3::NEGATIVE_UNIT_Z.getRotationTo( Vector3::UNIT_X );
static const auto ROTATION_TO_Y = Vector3::NEGATIVE_UNIT_Z.getRotationTo( Vector3::UNIT_Y );
static const auto ROTATION_TO_Z = Vector3::NEGATIVE_UNIT_Z.getRotationTo( Vector3::UNIT_Z );
static const auto ROTATION_TO_NEGATIVE_X = Vector3::NEGATIVE_UNIT_Z.getRotationTo( Vector3::NEGATIVE_UNIT_X );
static const auto ROTATION_TO_NEGATIVE_Y = Vector3::NEGATIVE_UNIT_Z.getRotationTo( Vector3::NEGATIVE_UNIT_Y );
static const auto ROTATION_TO_NEGATIVE_Z = Vector3::NEGATIVE_UNIT_Z.getRotationTo( Vector3::NEGATIVE_UNIT_Z );
static const auto ROTATION_360 = ROTATION_TO_Z * 2;
const auto svec_x = init_svec * ROTATION_TO_X;
const auto svec_y = init_svec * ROTATION_TO_Y;
const auto svec_z = init_svec * ROTATION_TO_Z;
const auto svec_nx = init_svec * ROTATION_TO_NEGATIVE_X;
const auto svec_ny = init_svec * ROTATION_TO_NEGATIVE_Y;
const auto svec_nz = init_svec * ROTATION_TO_NEGATIVE_Z;
const auto svec_360 = init_svec * ROTATION_360;
EXPECT_TRUE( check_compare( svec_x.to_cartesian() , Vector3::UNIT_X ) );
EXPECT_TRUE( check_compare( svec_y.to_cartesian() , Vector3::UNIT_Y ) );
EXPECT_TRUE( check_compare( svec_z.to_cartesian() , Vector3::UNIT_Z ) );
EXPECT_TRUE( check_compare( svec_nx.to_cartesian() , Vector3::NEGATIVE_UNIT_X ) );
EXPECT_TRUE( check_compare( svec_ny.to_cartesian() , Vector3::NEGATIVE_UNIT_Y ) );
EXPECT_TRUE( check_compare( svec_nz.to_cartesian() , Vector3::NEGATIVE_UNIT_Z ) );
EXPECT_TRUE( check_compare( svec_360.to_cartesian(), Vector3::NEGATIVE_UNIT_Z ) ); // FAILURE 1
EXPECT_TRUE( check_compare( svec_x , SphereVector::UNIT_X ) );
EXPECT_TRUE( check_compare( svec_y , SphereVector::UNIT_Y ) ); // FAILURE 2
EXPECT_TRUE( check_compare( svec_z , SphereVector::UNIT_Z ) );
EXPECT_TRUE( check_compare( svec_nx , SphereVector::NEGATIVE_UNIT_X ) );
EXPECT_TRUE( check_compare( svec_ny , SphereVector::NEGATIVE_UNIT_Y ) ); // FAILURE 3
EXPECT_TRUE( check_compare( svec_nz , SphereVector::NEGATIVE_UNIT_Z ) );
EXPECT_TRUE( check_compare( svec_360, SphereVector::NEGATIVE_UNIT_Z ) ); // FAILURE 4
}
Excerpt from the test report:
Failure 1:
4> ----
4> Comparing
4> Left: Vector3(9.61651e-007, -3.0598e-007, 7)
4> Right: Vector3(0, 0, -1)
4>e:\projects\games\netrush\netrush_projects\projects\netrush\zoneview\tests\spherevector.cpp(210): error : Value of: check_compare( svec_360.to_cartesian(), Vector3::NEGATIVE_UNIT_Z )
4> Actual: false
4> Expected: true
Failure 2:
4> ----
4> Comparing
4> Left: { radius = 1, theta = Radian(1.4783e-007), phi = Radian(5.65042) }
4> Right: { radius = 1, theta = Radian(0), phi = Radian(0) }
4>e:\projects\games\netrush\netrush_projects\projects\netrush\zoneview\tests\spherevector.cpp(213): error : Value of: check_compare( svec_y , SphereVector::UNIT_Y )
4> Actual: false
4> Expected: true
Failure 3:
4> ----
4> Comparing
4> Left: { radius = 1, theta = Radian(3.14159), phi = Radian(5.82845) }
4> Right: { radius = 1, theta = Radian(3.14159), phi = Radian(0) }
4>e:\projects\games\netrush\netrush_projects\projects\netrush\zoneview\tests\spherevector.cpp(216): error : Value of: check_compare( svec_ny , SphereVector::NEGATIVE_UNIT_Y )
4> Actual: false
4> Expected: true
Failure 4:
4> Comparing
4> Left: { radius = 7, theta = Radian(1.5708), phi = Radian(1.37379e-007) }
4> Right: { radius = 1, theta = Radian(1.5708), phi = Radian(3.14159) }
4>e:\projects\games\netrush\netrush_projects\projects\netrush\zoneview\tests\spherevector.cpp(218): error : Value of: check_compare( svec_360, SphereVector::NEGATIVE_UNIT_Z )
4> Actual: false
4> Expected: true
The same code on gist: https://gist.github.com/Klaim/8633895 with constants defined there: https://gist.github.com/Klaim/8634224
Full tests (using GTest): https://gist.github.com/Klaim/8633917
Full test report: https://gist.github.com/Klaim/8633937
(I can't put it here because of the text size limitation)
As you can see in the error report, there is 4 errors. I just can't find a solution for these, so maybe someone here could point me to what I'm doing wrong. I believe the problem could be from the test itself, but I'm not sure at all. Also, note that there are tests missing that I plan to add. The
The api.hpp include only expose the macros for shared library symbol export/import, used for constants.
This code is supposed to be extracted to be provided as a separate small open source library.
What I'm asking is: is this code incorrect? Or is my test incorrect?

Ramer-Douglas-Peucker path simplification algorithm

I implemented a path simplification algorithm after reading the article here:
http://losingfight.com/blog/2011/05/30/how-to-implement-a-vector-brush/
It's worked for me pretty well for generating optimized level geometry for my game. But, I'm using it now to clean up a* pathfinding paths and it's got a weird edge case that fails miserably.
Here's a screenshot of it working - optimizing the path from red circle to the blue circle. The faint green line is the a* output, and the lighter whiteish line is the optimized path.
And here's a screenshot of it failing:
Here's my code. I adapted the ObjC code from the article to c++
Note: vec2fvec is a std::vector< vec2<float> >, and 'real' is just a typedef'd float.
void rdpSimplify( const vec2fvec &in, vec2fvec &out, real threshold )
{
if ( in.size() <= 2 )
{
out = in;
return;
}
//
// Find the vertex farthest from the line defined by the start and and of the path
//
real maxDist = 0;
size_t maxDistIndex = 0;
LineSegment line( in.front(), in.back() );
for ( vec2fvec::const_iterator it(in.begin()),end(in.end()); it != end; ++it )
{
real dist = line.distance( *it );
if ( dist > maxDist )
{
maxDist = dist;
maxDistIndex = it - in.begin();
}
}
//
// If the farhtest vertex is greater than our threshold, we need to
// partition and optimize left and right separately
//
if ( maxDist > threshold )
{
//
// Partition 'in' into left and right subvectors, and optimize them
//
vec2fvec left( maxDistIndex+1 ),
right( in.size() - maxDistIndex ),
leftSimplified,
rightSimplified;
std::copy( in.begin(), in.begin() + maxDistIndex + 1, left.begin() );
std::copy( in.begin() + maxDistIndex, in.end(), right.begin() );
rdpSimplify(left, leftSimplified, threshold );
rdpSimplify(right, rightSimplified, threshold );
//
// Stitch optimized left and right into 'out'
//
out.resize( leftSimplified.size() + rightSimplified.size() - 1 );
std::copy( leftSimplified.begin(), leftSimplified.end(), out.begin());
std::copy( rightSimplified.begin() + 1, rightSimplified.end(), out.begin() + leftSimplified.size() );
}
else
{
out.push_back( line.a );
out.push_back( line.b );
}
}
I'm really at a loss as to what's going wrong. My spidey sense says it's in the std::copy calls... I must be copying garbage in some circumstances.
EDIT:
I've rewritten the algorithm dropping any use of iterators and std::copy, and the like. It still fails in the exact same way.
void rdpSimplify( const vec2fvec &in, vec2fvec &out, real threshold )
{
if ( in.size() <= 2 )
{
out = in;
return;
}
//
// Find the vertex farthest from the line defined by the start and and of the path
//
real maxDist = 0;
size_t maxDistIndex = 0;
LineSegment line( in.front(), in.back() );
for ( size_t i = 0, N = in.size(); i < N; i++ )
{
real dist = line.distance( in[i] );
if ( dist > maxDist )
{
maxDist = dist;
maxDistIndex = i;
}
}
//
// If the farthest vertex is greater than our threshold, we need to
// partition and optimize left and right separately
//
if ( maxDist > threshold )
{
//
// Partition 'in' into left and right subvectors, and optimize them
//
vec2fvec left, right, leftSimplified, rightSimplified;
for ( size_t i = 0; i < maxDistIndex + 1; i++ ) left.push_back( in[i] );
for ( size_t i = maxDistIndex; i < in.size(); i++ ) right.push_back( in[i] );
rdpSimplify(left, leftSimplified, threshold );
rdpSimplify(right, rightSimplified, threshold );
//
// Stitch optimized left and right into 'out'
//
out.clear();
for ( size_t i = 0, N = leftSimplified.size(); i < N; i++ ) out.push_back(leftSimplified[i]);
for ( size_t i = 1, N = rightSimplified.size(); i < N; i++ ) out.push_back( rightSimplified[i] );
}
else
{
out.push_back( line.a );
out.push_back( line.b );
}
}
I can't find any faults in your code.
Some things to try:
Add some debug print statements to check what maxDist is in the failing case. It should be really low, but if it comes out high then you know there's a problem with your line segment distance code.
Check that the path you are seeing actually matches the path that your algorithm returns. If not then perhaps there is something wrong with your path rendering? Maybe a bug when the path only has two points?
Check that your input path is what you expect it to be by printing out all its coordinates at the start of the algorithm.
It shouldn't take too long to find the cause of the problem if you just investigate a little. After a few minutes, staring at code is a very poor way to debug.