Threaded tile paging:
- model loading deferred to primary thread - tile removal deferred to paging thread - other tweaks and rearrangments. Airport signs - first stab at some support for adding taxiway and runway signs. This is non-optimal, but I'm under the gun for a demo.
This commit is contained in:
parent
865fb56c5a
commit
34854ab2af
11 changed files with 508 additions and 92 deletions
|
@ -56,6 +56,7 @@
|
||||||
#include <Cockpit/panel.hxx>
|
#include <Cockpit/panel.hxx>
|
||||||
#include <Cockpit/panel_io.hxx>
|
#include <Cockpit/panel_io.hxx>
|
||||||
#include <GUI/gui.h>
|
#include <GUI/gui.h>
|
||||||
|
#include <Scenery/scenery.hxx>
|
||||||
#include <Scenery/tilemgr.hxx>
|
#include <Scenery/tilemgr.hxx>
|
||||||
#include <Objects/matlib.hxx>
|
#include <Objects/matlib.hxx>
|
||||||
#include <Time/light.hxx>
|
#include <Time/light.hxx>
|
||||||
|
@ -484,6 +485,17 @@ void GLUTkey(unsigned char k, int x, int y) {
|
||||||
tile_path.append( p.gen_index_str() );
|
tile_path.append( p.gen_index_str() );
|
||||||
|
|
||||||
// printf position and attitude information
|
// printf position and attitude information
|
||||||
|
printf( "Lon = %.6f Lat = %.6f Ground = %.2f Alt = %.2f\n",
|
||||||
|
f->get_Longitude() * SGD_RADIANS_TO_DEGREES,
|
||||||
|
f->get_Latitude() * SGD_RADIANS_TO_DEGREES,
|
||||||
|
scenery.cur_elev,
|
||||||
|
f->get_Altitude() * SG_FEET_TO_METER );
|
||||||
|
printf( "Heading = %.2f Roll = %.2f Pitch = %.2f\n",
|
||||||
|
f->get_Psi() * SGD_RADIANS_TO_DEGREES,
|
||||||
|
f->get_Phi() * SGD_RADIANS_TO_DEGREES,
|
||||||
|
f->get_Theta() * SGD_RADIANS_TO_DEGREES );
|
||||||
|
|
||||||
|
#if 0
|
||||||
SG_LOG( SG_INPUT, SG_INFO,
|
SG_LOG( SG_INPUT, SG_INFO,
|
||||||
"Lon = " << f->get_Longitude() * SGD_RADIANS_TO_DEGREES
|
"Lon = " << f->get_Longitude() * SGD_RADIANS_TO_DEGREES
|
||||||
<< " Lat = " << f->get_Latitude() * SGD_RADIANS_TO_DEGREES
|
<< " Lat = " << f->get_Latitude() * SGD_RADIANS_TO_DEGREES
|
||||||
|
@ -493,6 +505,8 @@ void GLUTkey(unsigned char k, int x, int y) {
|
||||||
"Heading = " << f->get_Psi() * SGD_RADIANS_TO_DEGREES
|
"Heading = " << f->get_Psi() * SGD_RADIANS_TO_DEGREES
|
||||||
<< " Roll = " << f->get_Phi() * SGD_RADIANS_TO_DEGREES
|
<< " Roll = " << f->get_Phi() * SGD_RADIANS_TO_DEGREES
|
||||||
<< " Pitch = " << f->get_Theta() * SGD_RADIANS_TO_DEGREES );
|
<< " Pitch = " << f->get_Theta() * SGD_RADIANS_TO_DEGREES );
|
||||||
|
#endif
|
||||||
|
|
||||||
SG_LOG( SG_INPUT, SG_INFO, tile_path.c_str());
|
SG_LOG( SG_INPUT, SG_INFO, tile_path.c_str());
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -823,7 +823,7 @@ static ssgLeaf *gen_leaf( const string& path,
|
||||||
newmat = material_lib.find( material );
|
newmat = material_lib.find( material );
|
||||||
if ( newmat == NULL ) {
|
if ( newmat == NULL ) {
|
||||||
SG_LOG( SG_TERRAIN, SG_ALERT,
|
SG_LOG( SG_TERRAIN, SG_ALERT,
|
||||||
"Ack! bad on the fly materia create = "
|
"Ack! bad on the fly material create = "
|
||||||
<< material << " in " << path );
|
<< material << " in " << path );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -984,3 +984,131 @@ ssgBranch *fgBinObjLoad( const string& path, FGTileEntry *t,
|
||||||
|
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ssgBranch *gen_taxi_sign( const string path, const string content ) {
|
||||||
|
// for demo purposes we assume each element (letter) is 1x1 meter.
|
||||||
|
// Sign is placed 0.25 meters above the ground
|
||||||
|
|
||||||
|
ssgBranch *object = new ssgBranch();
|
||||||
|
object->setName( (char *)content.c_str() );
|
||||||
|
|
||||||
|
double offset = content.length() / 2.0;
|
||||||
|
|
||||||
|
for ( unsigned int i = 0; i < content.length(); ++i ) {
|
||||||
|
string material;
|
||||||
|
|
||||||
|
char item = content[i];
|
||||||
|
if ( item == '<' ) {
|
||||||
|
material = "ArrowL.rgb";
|
||||||
|
} else if ( item == '>' ) {
|
||||||
|
material = "ArrowR.rgb";
|
||||||
|
} else if ( item >= 'A' && item <= 'Z' ) {
|
||||||
|
material = "Letter";
|
||||||
|
material += item;
|
||||||
|
material += ".rgb";
|
||||||
|
} else if ( item >= 'a' && item <= 'z' ) {
|
||||||
|
int tmp = item - 'a';
|
||||||
|
char c = 'A' + tmp;
|
||||||
|
material = "Black";
|
||||||
|
material += c;
|
||||||
|
material += ".rgb";
|
||||||
|
} else {
|
||||||
|
cout << "Unknown taxi sign code = '" << item << "' !!!!" << endl;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
point_list nodes; nodes.clear();
|
||||||
|
point_list normals; normals.clear();
|
||||||
|
point_list texcoords; texcoords.clear();
|
||||||
|
int_list vertex_index; vertex_index.clear();
|
||||||
|
int_list tex_index; tex_index.clear();
|
||||||
|
|
||||||
|
nodes.push_back( Point3D( -offset + i, 0, 0.25 ) );
|
||||||
|
nodes.push_back( Point3D( -offset + i + 1, 0, 0.25 ) );
|
||||||
|
nodes.push_back( Point3D( -offset + i, 0, 1.25 ) );
|
||||||
|
nodes.push_back( Point3D( -offset + i + 1, 0, 1.25 ) );
|
||||||
|
|
||||||
|
normals.push_back( Point3D( 0, -1, 0 ) );
|
||||||
|
normals.push_back( Point3D( 0, -1, 0 ) );
|
||||||
|
normals.push_back( Point3D( 0, -1, 0 ) );
|
||||||
|
normals.push_back( Point3D( 0, -1, 0 ) );
|
||||||
|
|
||||||
|
texcoords.push_back( Point3D( 0, 0, 0 ) );
|
||||||
|
texcoords.push_back( Point3D( 1, 0, 0 ) );
|
||||||
|
texcoords.push_back( Point3D( 0, 1, 0 ) );
|
||||||
|
texcoords.push_back( Point3D( 1, 1, 0 ) );
|
||||||
|
|
||||||
|
vertex_index.push_back( 0 );
|
||||||
|
vertex_index.push_back( 1 );
|
||||||
|
vertex_index.push_back( 2 );
|
||||||
|
vertex_index.push_back( 3 );
|
||||||
|
|
||||||
|
tex_index.push_back( 0 );
|
||||||
|
tex_index.push_back( 1 );
|
||||||
|
tex_index.push_back( 2 );
|
||||||
|
tex_index.push_back( 3 );
|
||||||
|
|
||||||
|
ssgLeaf *leaf = gen_leaf( path, GL_TRIANGLE_STRIP, material,
|
||||||
|
nodes, normals, texcoords,
|
||||||
|
vertex_index, tex_index,
|
||||||
|
false, NULL );
|
||||||
|
|
||||||
|
object->addKid( leaf );
|
||||||
|
}
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ssgBranch *gen_runway_sign( const string path, const string name ) {
|
||||||
|
// for demo purposes we assume each element (letter) is 1x1 meter.
|
||||||
|
// Sign is placed 0.25 meters above the ground
|
||||||
|
|
||||||
|
ssgBranch *object = new ssgBranch();
|
||||||
|
object->setName( (char *)name.c_str() );
|
||||||
|
|
||||||
|
double offset = name.length() / 2.0;
|
||||||
|
|
||||||
|
string material = name + ".rgb";
|
||||||
|
|
||||||
|
point_list nodes; nodes.clear();
|
||||||
|
point_list normals; normals.clear();
|
||||||
|
point_list texcoords; texcoords.clear();
|
||||||
|
int_list vertex_index; vertex_index.clear();
|
||||||
|
int_list tex_index; tex_index.clear();
|
||||||
|
|
||||||
|
nodes.push_back( Point3D( -offset, 0, 0.25 ) );
|
||||||
|
nodes.push_back( Point3D( offset + 1, 0, 0.25 ) );
|
||||||
|
nodes.push_back( Point3D( -offset, 0, 1.25 ) );
|
||||||
|
nodes.push_back( Point3D( offset + 1, 0, 1.25 ) );
|
||||||
|
|
||||||
|
normals.push_back( Point3D( 0, -1, 0 ) );
|
||||||
|
normals.push_back( Point3D( 0, -1, 0 ) );
|
||||||
|
normals.push_back( Point3D( 0, -1, 0 ) );
|
||||||
|
normals.push_back( Point3D( 0, -1, 0 ) );
|
||||||
|
|
||||||
|
texcoords.push_back( Point3D( 0, 0, 0 ) );
|
||||||
|
texcoords.push_back( Point3D( 1, 0, 0 ) );
|
||||||
|
texcoords.push_back( Point3D( 0, 1, 0 ) );
|
||||||
|
texcoords.push_back( Point3D( 1, 1, 0 ) );
|
||||||
|
|
||||||
|
vertex_index.push_back( 0 );
|
||||||
|
vertex_index.push_back( 1 );
|
||||||
|
vertex_index.push_back( 2 );
|
||||||
|
vertex_index.push_back( 3 );
|
||||||
|
|
||||||
|
tex_index.push_back( 0 );
|
||||||
|
tex_index.push_back( 1 );
|
||||||
|
tex_index.push_back( 2 );
|
||||||
|
tex_index.push_back( 3 );
|
||||||
|
|
||||||
|
ssgLeaf *leaf = gen_leaf( path, GL_TRIANGLE_STRIP, material,
|
||||||
|
nodes, normals, texcoords,
|
||||||
|
vertex_index, tex_index,
|
||||||
|
false, NULL );
|
||||||
|
|
||||||
|
object->addKid( leaf );
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
|
@ -67,4 +67,12 @@ ssgBranch *fgAsciiObjLoad(const string& path, FGTileEntry *tile,
|
||||||
ssgBranch *fgGenTile( const string& path, FGTileEntry *t);
|
ssgBranch *fgGenTile( const string& path, FGTileEntry *t);
|
||||||
|
|
||||||
|
|
||||||
|
// Generate a taxi sign
|
||||||
|
ssgBranch *gen_taxi_sign( const string path, const string content );
|
||||||
|
|
||||||
|
|
||||||
|
// Generate a runway sign
|
||||||
|
ssgBranch *gen_runway_sign( const string path, const string name );
|
||||||
|
|
||||||
|
|
||||||
#endif // _OBJ_HXX
|
#endif // _OBJ_HXX
|
||||||
|
|
|
@ -89,6 +89,15 @@ FGTileLoader::add( FGTileEntry* tile )
|
||||||
tile_load_queue.push( tile );
|
tile_load_queue.push( tile );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
FGTileLoader::remove( FGTileEntry* tile )
|
||||||
|
{
|
||||||
|
tile_free_queue.push( tile );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@ -110,6 +119,16 @@ FGTileLoader::update()
|
||||||
tile->load( tile_path, true );
|
tile->load( tile_path, true );
|
||||||
FGTileMgr::loaded( tile );
|
FGTileMgr::loaded( tile );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( !tile_free_queue.empty() ) {
|
||||||
|
cout << "freeing next tile ..." << endl;
|
||||||
|
// free the next tile in the queue
|
||||||
|
FGTileEntry* tile = tile_free_queue.front();
|
||||||
|
tile_free_queue.pop();
|
||||||
|
tile->free_tile();
|
||||||
|
delete tile;
|
||||||
|
}
|
||||||
|
|
||||||
#endif // ENABLE_THREADS
|
#endif // ENABLE_THREADS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +154,16 @@ FGTileLoader::LoaderThread::run()
|
||||||
tile->load( loader->tile_path, true );
|
tile->load( loader->tile_path, true );
|
||||||
set_cancel( SGThread::CANCEL_DEFERRED );
|
set_cancel( SGThread::CANCEL_DEFERRED );
|
||||||
|
|
||||||
FGTileMgr::loaded( tile );
|
FGTileMgr::ready_to_attach( tile );
|
||||||
|
|
||||||
|
// Handle and pending removals
|
||||||
|
while ( !loader->tile_free_queue.empty() ) {
|
||||||
|
cout << "freeing next tile ..." << endl;
|
||||||
|
// free the next tile in the queue
|
||||||
|
FGTileEntry* tile = loader->tile_free_queue.pop();
|
||||||
|
tile->free_tile();
|
||||||
|
delete tile;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pthread_cleanup_pop(1);
|
pthread_cleanup_pop(1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,10 +57,15 @@ public:
|
||||||
/**
|
/**
|
||||||
* Add a tile to the end of the load queue.
|
* Add a tile to the end of the load queue.
|
||||||
* @param tile The tile to be loaded from disk.
|
* @param tile The tile to be loaded from disk.
|
||||||
* @param vis Current visibilty (in feet?) (see FGTileMgr::vis).
|
|
||||||
*/
|
*/
|
||||||
void add( FGTileEntry* tile );
|
void add( FGTileEntry* tile );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a tile from memory.
|
||||||
|
* @param tile The tile to be removed from memory.
|
||||||
|
*/
|
||||||
|
void remove( FGTileEntry* tile );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tile loader thread will only load one tile per call to the
|
* The tile loader thread will only load one tile per call to the
|
||||||
* update() method. This is a way to spread out the work of the
|
* update() method. This is a way to spread out the work of the
|
||||||
|
@ -83,9 +88,11 @@ private:
|
||||||
/**
|
/**
|
||||||
* FIFO queue of tiles to load from data files.
|
* FIFO queue of tiles to load from data files.
|
||||||
*/
|
*/
|
||||||
SGBlockingQueue< FGTileEntry* > tile_load_queue;
|
SGBlockingQueue< FGTileEntry * > tile_load_queue;
|
||||||
|
SGBlockingQueue< FGTileEntry * > tile_free_queue;
|
||||||
#else
|
#else
|
||||||
queue< FGTileEntry* > tile_load_queue;
|
queue< FGTileEntry * > tile_load_queue;
|
||||||
|
queue< FGTileEntry * > tile_free_queue;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -62,9 +62,15 @@ FGNewCache::~FGNewCache( void ) {
|
||||||
// Free a tile cache entry
|
// Free a tile cache entry
|
||||||
void FGNewCache::entry_free( long cache_index ) {
|
void FGNewCache::entry_free( long cache_index ) {
|
||||||
SG_LOG( SG_TERRAIN, SG_DEBUG, "FREEING CACHE ENTRY = " << cache_index );
|
SG_LOG( SG_TERRAIN, SG_DEBUG, "FREEING CACHE ENTRY = " << cache_index );
|
||||||
FGTileEntry *e = tile_cache[cache_index];
|
FGTileEntry *tile = tile_cache[cache_index];
|
||||||
e->free_tile();
|
tile->disconnect_ssg_nodes();
|
||||||
delete e;
|
tile->sched_removal();
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
tile->free_tile();
|
||||||
|
delete tile;
|
||||||
|
#endif
|
||||||
|
|
||||||
tile_cache.erase( cache_index );
|
tile_cache.erase( cache_index );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,7 +172,7 @@ void FGNewCache::make_space() {
|
||||||
long index = current->first;
|
long index = current->first;
|
||||||
FGTileEntry *e = current->second;
|
FGTileEntry *e = current->second;
|
||||||
|
|
||||||
if ( e->is_loaded() ) {
|
if ( e->is_loaded() && e->get_pending_models() == 0 ) {
|
||||||
// calculate approximate distance from view point
|
// calculate approximate distance from view point
|
||||||
sgdCopyVec3( abs_view_pos,
|
sgdCopyVec3( abs_view_pos,
|
||||||
globals->get_current_view()->get_abs_view_pos() );
|
globals->get_current_view()->get_abs_view_pos() );
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// tile.cxx -- routines to handle a scenery tile
|
// tileentry.cxx -- routines to handle a scenery tile
|
||||||
//
|
//
|
||||||
// Written by Curtis Olson, started May 1998.
|
// Written by Curtis Olson, started May 1998.
|
||||||
//
|
//
|
||||||
// Copyright (C) 1998, 1999 Curtis L. Olson - curt@flightgear.org
|
// Copyright (C) 1998 - 2001 Curtis L. Olson - curt@flightgear.org
|
||||||
//
|
//
|
||||||
// This program is free software; you can redistribute it and/or
|
// This program is free software; you can redistribute it and/or
|
||||||
// modify it under the terms of the GNU General Public License as
|
// modify it under the terms of the GNU General Public License as
|
||||||
|
@ -51,6 +51,7 @@
|
||||||
#include <Objects/obj.hxx>
|
#include <Objects/obj.hxx>
|
||||||
|
|
||||||
#include "tileentry.hxx"
|
#include "tileentry.hxx"
|
||||||
|
#include "tilemgr.hxx"
|
||||||
|
|
||||||
SG_USING_STD(for_each);
|
SG_USING_STD(for_each);
|
||||||
SG_USING_STD(mem_fun_ref);
|
SG_USING_STD(mem_fun_ref);
|
||||||
|
@ -64,7 +65,8 @@ FGTileEntry::FGTileEntry ( const SGBucket& b )
|
||||||
tile_bucket( b ),
|
tile_bucket( b ),
|
||||||
terra_transform( new ssgTransform ),
|
terra_transform( new ssgTransform ),
|
||||||
terra_range( new ssgRangeSelector ),
|
terra_range( new ssgRangeSelector ),
|
||||||
loaded(false)
|
loaded(false),
|
||||||
|
pending_models(0)
|
||||||
{
|
{
|
||||||
nodes.clear();
|
nodes.clear();
|
||||||
|
|
||||||
|
@ -103,6 +105,12 @@ static void my_remove_branch( ssgBranch * branch ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Schedule tile to be freed/removed
|
||||||
|
void FGTileEntry::sched_removal() {
|
||||||
|
global_tile_mgr.ready_to_delete( this );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Clean up the memory used by this tile and delete the arrays used by
|
// Clean up the memory used by this tile and delete the arrays used by
|
||||||
// ssg as well as the whole ssg branch
|
// ssg as well as the whole ssg branch
|
||||||
void FGTileEntry::free_tile() {
|
void FGTileEntry::free_tile() {
|
||||||
|
@ -134,45 +142,14 @@ void FGTileEntry::free_tile() {
|
||||||
}
|
}
|
||||||
index_ptrs.clear();
|
index_ptrs.clear();
|
||||||
|
|
||||||
// delete the terrain branch
|
// delete the terrain branch (this should already have been
|
||||||
int pcount = terra_transform->getNumParents();
|
// disconnected from the scene graph)
|
||||||
if ( pcount > 0 ) {
|
ssgDeRefDelete( terra_transform );
|
||||||
// find the first parent (should only be one)
|
|
||||||
ssgBranch *parent = terra_transform->getParent( 0 ) ;
|
|
||||||
if( parent ) {
|
|
||||||
// my_remove_branch( select_ptr );
|
|
||||||
parent->removeKid( terra_transform );
|
|
||||||
terra_transform = NULL;
|
|
||||||
} else {
|
|
||||||
SG_LOG( SG_TERRAIN, SG_ALERT,
|
|
||||||
"parent pointer is NULL! Dying" );
|
|
||||||
exit(-1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SG_LOG( SG_TERRAIN, SG_ALERT,
|
|
||||||
"Parent count is zero for an ssg tile! Dying" );
|
|
||||||
exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( lights_transform ) {
|
if ( lights_transform ) {
|
||||||
// delete the terrain lighting branch
|
// delete the terrain lighting branch (this should already have been
|
||||||
pcount = lights_transform->getNumParents();
|
// disconnected from the scene graph)
|
||||||
if ( pcount > 0 ) {
|
ssgDeRefDelete( lights_transform );
|
||||||
// find the first parent (should only be one)
|
|
||||||
ssgBranch *parent = lights_transform->getParent( 0 ) ;
|
|
||||||
if( parent ) {
|
|
||||||
parent->removeKid( lights_transform );
|
|
||||||
lights_transform = NULL;
|
|
||||||
} else {
|
|
||||||
SG_LOG( SG_TERRAIN, SG_ALERT,
|
|
||||||
"parent pointer is NULL! Dying" );
|
|
||||||
exit(-1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SG_LOG( SG_TERRAIN, SG_ALERT,
|
|
||||||
"Parent count is zero for an ssg light tile! Dying" );
|
|
||||||
exit(-1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -389,15 +366,14 @@ FGTileEntry::load( const SGPath& base, bool is_base )
|
||||||
<< " elevation = " << elev
|
<< " elevation = " << elev
|
||||||
<< " heading = " << hdg );
|
<< " heading = " << hdg );
|
||||||
|
|
||||||
// load the object itself
|
// object loading is deferred to main render thread,
|
||||||
|
// but lets figure out the paths right now.
|
||||||
SGPath custom_path = tile_path;
|
SGPath custom_path = tile_path;
|
||||||
ssgTexturePath( (char *)custom_path.c_str() );
|
|
||||||
custom_path.append( name );
|
custom_path.append( name );
|
||||||
ssgEntity *obj_model = ssgLoad( (char *)custom_path.c_str() );
|
|
||||||
|
|
||||||
// setup transforms
|
// setup transforms
|
||||||
Point3D geod( lon * SGD_DEGREES_TO_RADIANS,
|
Point3D geod( lon * SGD_DEGREES_TO_RADIANS,
|
||||||
lat * SGD_DEGREES_TO_RADIANS,
|
lat * SGD_DEGREES_TO_RADIANS,
|
||||||
elev );
|
elev );
|
||||||
Point3D world_pos = sgGeodToCart( geod );
|
Point3D world_pos = sgGeodToCart( geod );
|
||||||
Point3D offset = world_pos - center;
|
Point3D offset = world_pos - center;
|
||||||
|
@ -424,8 +400,118 @@ FGTileEntry::load( const SGPath& base, bool is_base )
|
||||||
ssgTransform *obj_trans = new ssgTransform;
|
ssgTransform *obj_trans = new ssgTransform;
|
||||||
obj_trans->setTransform( &obj_pos );
|
obj_trans->setTransform( &obj_pos );
|
||||||
|
|
||||||
// wire the scene graph together
|
// wire as much of the scene graph together as we can
|
||||||
obj_trans->addKid( obj_model );
|
new_tile->addKid( obj_trans );
|
||||||
|
|
||||||
|
// bump up the pending models count
|
||||||
|
pending_models++;
|
||||||
|
|
||||||
|
// push an entry onto the model load queue
|
||||||
|
FGDeferredModel *dm
|
||||||
|
= new FGDeferredModel( custom_path.str(), tile_path.str(),
|
||||||
|
this, obj_trans );
|
||||||
|
FGTileMgr::model_ready( dm );
|
||||||
|
} else if ( token == "OBJECT_TAXI_SIGN" ) {
|
||||||
|
// load object info
|
||||||
|
double lon, lat, elev, hdg;
|
||||||
|
in >> name >> lon >> lat >> elev >> hdg >> ::skipws;
|
||||||
|
SG_LOG( SG_TERRAIN, SG_INFO, "token = " << token
|
||||||
|
<< " name = " << name
|
||||||
|
<< " pos = " << lon << ", " << lat
|
||||||
|
<< " elevation = " << elev
|
||||||
|
<< " heading = " << hdg );
|
||||||
|
|
||||||
|
// load the object itself
|
||||||
|
SGPath custom_path = tile_path;
|
||||||
|
custom_path.append( name );
|
||||||
|
|
||||||
|
// setup transforms
|
||||||
|
Point3D geod( lon * SGD_DEGREES_TO_RADIANS,
|
||||||
|
lat * SGD_DEGREES_TO_RADIANS,
|
||||||
|
elev );
|
||||||
|
Point3D world_pos = sgGeodToCart( geod );
|
||||||
|
Point3D offset = world_pos - center;
|
||||||
|
sgMat4 POS;
|
||||||
|
sgMakeTransMat4( POS, offset.x(), offset.y(), offset.z() );
|
||||||
|
|
||||||
|
sgVec3 obj_rt, obj_up;
|
||||||
|
sgSetVec3( obj_rt, 0.0, 1.0, 0.0); // Y axis
|
||||||
|
sgSetVec3( obj_up, 0.0, 0.0, 1.0); // Z axis
|
||||||
|
|
||||||
|
sgMat4 ROT_lon, ROT_lat, ROT_hdg;
|
||||||
|
sgMakeRotMat4( ROT_lon, lon, obj_up );
|
||||||
|
sgMakeRotMat4( ROT_lat, 90 - lat, obj_rt );
|
||||||
|
sgMakeRotMat4( ROT_hdg, hdg, obj_up );
|
||||||
|
|
||||||
|
sgMat4 TUX;
|
||||||
|
sgCopyMat4( TUX, ROT_hdg );
|
||||||
|
sgPostMultMat4( TUX, ROT_lat );
|
||||||
|
sgPostMultMat4( TUX, ROT_lon );
|
||||||
|
sgPostMultMat4( TUX, POS );
|
||||||
|
|
||||||
|
sgCoord obj_pos;
|
||||||
|
sgSetCoord( &obj_pos, TUX );
|
||||||
|
ssgTransform *obj_trans = new ssgTransform;
|
||||||
|
obj_trans->setTransform( &obj_pos );
|
||||||
|
|
||||||
|
ssgBranch *custom_obj
|
||||||
|
= gen_taxi_sign( custom_path.str(), name );
|
||||||
|
|
||||||
|
// wire the pieces together
|
||||||
|
if ( (new_tile != NULL) && (custom_obj != NULL) ) {
|
||||||
|
obj_trans -> addKid( custom_obj );
|
||||||
|
}
|
||||||
|
new_tile->addKid( obj_trans );
|
||||||
|
} else if ( token == "OBJECT_RUNWAY_SIGN" ) {
|
||||||
|
// load object info
|
||||||
|
double lon, lat, elev, hdg;
|
||||||
|
in >> name >> lon >> lat >> elev >> hdg >> ::skipws;
|
||||||
|
SG_LOG( SG_TERRAIN, SG_INFO, "token = " << token
|
||||||
|
<< " name = " << name
|
||||||
|
<< " pos = " << lon << ", " << lat
|
||||||
|
<< " elevation = " << elev
|
||||||
|
<< " heading = " << hdg );
|
||||||
|
|
||||||
|
// load the object itself
|
||||||
|
SGPath custom_path = tile_path;
|
||||||
|
custom_path.append( name );
|
||||||
|
|
||||||
|
// setup transforms
|
||||||
|
Point3D geod( lon * SGD_DEGREES_TO_RADIANS,
|
||||||
|
lat * SGD_DEGREES_TO_RADIANS,
|
||||||
|
elev );
|
||||||
|
Point3D world_pos = sgGeodToCart( geod );
|
||||||
|
Point3D offset = world_pos - center;
|
||||||
|
sgMat4 POS;
|
||||||
|
sgMakeTransMat4( POS, offset.x(), offset.y(), offset.z() );
|
||||||
|
|
||||||
|
sgVec3 obj_rt, obj_up;
|
||||||
|
sgSetVec3( obj_rt, 0.0, 1.0, 0.0); // Y axis
|
||||||
|
sgSetVec3( obj_up, 0.0, 0.0, 1.0); // Z axis
|
||||||
|
|
||||||
|
sgMat4 ROT_lon, ROT_lat, ROT_hdg;
|
||||||
|
sgMakeRotMat4( ROT_lon, lon, obj_up );
|
||||||
|
sgMakeRotMat4( ROT_lat, 90 - lat, obj_rt );
|
||||||
|
sgMakeRotMat4( ROT_hdg, hdg, obj_up );
|
||||||
|
|
||||||
|
sgMat4 TUX;
|
||||||
|
sgCopyMat4( TUX, ROT_hdg );
|
||||||
|
sgPostMultMat4( TUX, ROT_lat );
|
||||||
|
sgPostMultMat4( TUX, ROT_lon );
|
||||||
|
sgPostMultMat4( TUX, POS );
|
||||||
|
|
||||||
|
sgCoord obj_pos;
|
||||||
|
sgSetCoord( &obj_pos, TUX );
|
||||||
|
ssgTransform *obj_trans = new ssgTransform;
|
||||||
|
obj_trans->setTransform( &obj_pos );
|
||||||
|
|
||||||
|
ssgBranch *custom_obj
|
||||||
|
= gen_runway_sign( custom_path.str(), name );
|
||||||
|
|
||||||
|
// wire the pieces together
|
||||||
|
if ( (new_tile != NULL) && (custom_obj != NULL) ) {
|
||||||
|
obj_trans -> addKid( custom_obj );
|
||||||
|
}
|
||||||
new_tile->addKid( obj_trans );
|
new_tile->addKid( obj_trans );
|
||||||
} else {
|
} else {
|
||||||
SG_LOG( SG_TERRAIN, SG_ALERT,
|
SG_LOG( SG_TERRAIN, SG_ALERT,
|
||||||
|
@ -478,9 +564,66 @@ FGTileEntry::load( const SGPath& base, bool is_base )
|
||||||
void
|
void
|
||||||
FGTileEntry::add_ssg_nodes( ssgBranch* terrain, ssgBranch* ground )
|
FGTileEntry::add_ssg_nodes( ssgBranch* terrain, ssgBranch* ground )
|
||||||
{
|
{
|
||||||
|
// bump up the ref count so we can remove this later without
|
||||||
|
// having ssg try to free the memory.
|
||||||
|
terra_transform->ref();
|
||||||
terrain->addKid( terra_transform );
|
terrain->addKid( terra_transform );
|
||||||
if (lights_transform != 0)
|
|
||||||
|
if ( lights_transform != 0 ) {
|
||||||
|
// bump up the ref count so we can remove this later without
|
||||||
|
// having ssg try to free the memory.
|
||||||
|
lights_transform->ref();
|
||||||
ground->addKid( lights_transform );
|
ground->addKid( lights_transform );
|
||||||
|
}
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
FGTileEntry::disconnect_ssg_nodes()
|
||||||
|
{
|
||||||
|
cout << "disconnecting ssg nodes" << endl;
|
||||||
|
|
||||||
|
// find the terrain branch parent
|
||||||
|
int pcount = terra_transform->getNumParents();
|
||||||
|
if ( pcount > 0 ) {
|
||||||
|
// find the first parent (should only be one)
|
||||||
|
ssgBranch *parent = terra_transform->getParent( 0 ) ;
|
||||||
|
if( parent ) {
|
||||||
|
// disconnect the tile (we previously ref()'d it so it
|
||||||
|
// won't get freed now)
|
||||||
|
parent->removeKid( terra_transform );
|
||||||
|
} else {
|
||||||
|
SG_LOG( SG_TERRAIN, SG_ALERT,
|
||||||
|
"parent pointer is NULL! Dying" );
|
||||||
|
exit(-1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SG_LOG( SG_TERRAIN, SG_ALERT,
|
||||||
|
"Parent count is zero for an ssg tile! Dying" );
|
||||||
|
exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the terrain lighting branch
|
||||||
|
if ( lights_transform ) {
|
||||||
|
pcount = lights_transform->getNumParents();
|
||||||
|
if ( pcount > 0 ) {
|
||||||
|
// find the first parent (should only be one)
|
||||||
|
ssgBranch *parent = lights_transform->getParent( 0 ) ;
|
||||||
|
if( parent ) {
|
||||||
|
// disconnect the light branch (we previously ref()'d
|
||||||
|
// it so it won't get freed now)
|
||||||
|
parent->removeKid( lights_transform );
|
||||||
|
} else {
|
||||||
|
SG_LOG( SG_TERRAIN, SG_ALERT,
|
||||||
|
"parent pointer is NULL! Dying" );
|
||||||
|
exit(-1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SG_LOG( SG_TERRAIN, SG_ALERT,
|
||||||
|
"Parent count is zero for an ssg light tile! Dying" );
|
||||||
|
exit(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
//
|
//
|
||||||
// Written by Curtis Olson, started May 1998.
|
// Written by Curtis Olson, started May 1998.
|
||||||
//
|
//
|
||||||
// Copyright (C) 1998, 1999 Curtis L. Olson - curt@flightgear.org
|
// Copyright (C) 1998 - 2001 Curtis L. Olson - curt@flightgear.org
|
||||||
//
|
//
|
||||||
// This program is free software; you can redistribute it and/or
|
// This program is free software; you can redistribute it and/or
|
||||||
// modify it under the terms of the GNU General Public License as
|
// modify it under the terms of the GNU General Public License as
|
||||||
|
@ -62,6 +62,39 @@ typedef vector < Point3D > point_list;
|
||||||
typedef point_list::iterator point_list_iterator;
|
typedef point_list::iterator point_list_iterator;
|
||||||
typedef point_list::const_iterator const_point_list_iterator;
|
typedef point_list::const_iterator const_point_list_iterator;
|
||||||
|
|
||||||
|
class FGTileEntry;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to hold deferred model loading info
|
||||||
|
*/
|
||||||
|
class FGDeferredModel {
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
string model_path;
|
||||||
|
string texture_path;
|
||||||
|
FGTileEntry *tile;
|
||||||
|
ssgTransform *obj_trans;
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
inline FGDeferredModel() { }
|
||||||
|
inline FGDeferredModel( const string mp, const string tp,
|
||||||
|
FGTileEntry *t, ssgTransform *ot )
|
||||||
|
{
|
||||||
|
model_path = mp;
|
||||||
|
texture_path = tp;
|
||||||
|
tile = t;
|
||||||
|
obj_trans = ot;
|
||||||
|
}
|
||||||
|
inline ~FGDeferredModel() { }
|
||||||
|
inline string get_model_path() const { return model_path; }
|
||||||
|
inline string get_texture_path() const { return texture_path; }
|
||||||
|
inline FGTileEntry *get_tile() const { return tile; }
|
||||||
|
inline ssgTransform *get_obj_trans() const { return obj_trans; }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class to encapsulate everything we need to know about a scenery tile.
|
* A class to encapsulate everything we need to know about a scenery tile.
|
||||||
|
@ -124,6 +157,13 @@ private:
|
||||||
*/
|
*/
|
||||||
volatile bool loaded;
|
volatile bool loaded;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count of pending models to load for this tile. This tile
|
||||||
|
* cannot be removed until this number reaches zero (i.e. no
|
||||||
|
* pending models to load for this tile.)
|
||||||
|
*/
|
||||||
|
volatile int pending_models;
|
||||||
|
|
||||||
ssgBranch* obj_load( const std::string& path,
|
ssgBranch* obj_load( const std::string& path,
|
||||||
ssgVertexArray* lights, bool is_base );
|
ssgVertexArray* lights, bool is_base );
|
||||||
|
|
||||||
|
@ -137,6 +177,9 @@ public:
|
||||||
// Destructor
|
// Destructor
|
||||||
~FGTileEntry();
|
~FGTileEntry();
|
||||||
|
|
||||||
|
// Schedule tile to be freed/removed
|
||||||
|
void sched_removal();
|
||||||
|
|
||||||
// Clean up the memory used by this tile and delete the arrays
|
// Clean up the memory used by this tile and delete the arrays
|
||||||
// used by ssg as well as the whole ssg branch
|
// used by ssg as well as the whole ssg branch
|
||||||
void free_tile();
|
void free_tile();
|
||||||
|
@ -158,8 +201,7 @@ public:
|
||||||
* Load tile data from a file.
|
* Load tile data from a file.
|
||||||
* @param base name of directory containing tile data file.
|
* @param base name of directory containing tile data file.
|
||||||
* @param is_base is this a base terrain object for which we should generate
|
* @param is_base is this a base terrain object for which we should generate
|
||||||
* random ground light points
|
* random ground light points */
|
||||||
*/
|
|
||||||
void load( const SGPath& base, bool is_base );
|
void load( const SGPath& base, bool is_base );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -168,6 +210,16 @@ public:
|
||||||
*/
|
*/
|
||||||
inline bool is_loaded() const { return loaded; }
|
inline bool is_loaded() const { return loaded; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* decrement the pending models count
|
||||||
|
*/
|
||||||
|
inline void dec_pending_models() { pending_models--; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return the number of remaining pending models for this tile
|
||||||
|
*/
|
||||||
|
inline int get_pending_models() const { return pending_models; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the "bucket" for this tile
|
* Return the "bucket" for this tile
|
||||||
*/
|
*/
|
||||||
|
@ -177,6 +229,12 @@ public:
|
||||||
* Add terrain mesh and ground lighting to scene graph.
|
* Add terrain mesh and ground lighting to scene graph.
|
||||||
*/
|
*/
|
||||||
void add_ssg_nodes( ssgBranch* terrain, ssgBranch* ground );
|
void add_ssg_nodes( ssgBranch* terrain, ssgBranch* ground );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* disconnect terrain mesh and ground lighting nodes from scene
|
||||||
|
* graph for this tile.
|
||||||
|
*/
|
||||||
|
void disconnect_ssg_nodes();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -70,11 +70,14 @@ static inline Point3D operator + (const Point3D& a, const sgdVec3 b)
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ENABLE_THREADS
|
#ifdef ENABLE_THREADS
|
||||||
SGLockedQueue<FGTileEntry*> FGTileMgr::loaded_queue;
|
SGLockedQueue<FGTileEntry *> FGTileMgr::attach_queue;
|
||||||
|
SGLockedQueue<FGDeferredModel *> FGTileMgr::model_queue;
|
||||||
#else
|
#else
|
||||||
queue<FGTileEntry*> FGTileMgr::loaded_queue;
|
queue<FGTileEntry *> FGTileMgr::attach_queue;
|
||||||
|
queue<FGTileDeferredModel *> FGTileMgr::model_queue;
|
||||||
#endif // ENABLE_THREADS
|
#endif // ENABLE_THREADS
|
||||||
|
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
FGTileMgr::FGTileMgr():
|
FGTileMgr::FGTileMgr():
|
||||||
state( Start ),
|
state( Start ),
|
||||||
|
@ -135,25 +138,6 @@ void FGTileMgr::sched_tile( const SGBucket& b ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// depricated for threading
|
|
||||||
#if 0
|
|
||||||
// load a tile
|
|
||||||
void FGTileMgr::load_tile( const SGBucket& b ) {
|
|
||||||
// see if tile already exists in the cache
|
|
||||||
FGTileEntry *t = tile_cache.get_tile( b );
|
|
||||||
|
|
||||||
if ( t == NULL ) {
|
|
||||||
SG_LOG( SG_TERRAIN, SG_DEBUG, "Loading tile " << b );
|
|
||||||
tile_cache.fill_in( b );
|
|
||||||
t = tile_cache.get_tile( b );
|
|
||||||
t->prep_ssg_node( scenery.center, vis);
|
|
||||||
} else {
|
|
||||||
SG_LOG( SG_TERRAIN, SG_DEBUG, "Tile already in cache " << b );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
static void CurrentNormalInLocalPlane(sgVec3 dst, sgVec3 src) {
|
static void CurrentNormalInLocalPlane(sgVec3 dst, sgVec3 src) {
|
||||||
sgVec3 tmp;
|
sgVec3 tmp;
|
||||||
sgSetVec3(tmp, src[0], src[1], src[2] );
|
sgSetVec3(tmp, src[0], src[1], src[2] );
|
||||||
|
@ -364,6 +348,28 @@ int FGTileMgr::update( double lon, double lat ) {
|
||||||
// load in the case of the threaded tile pager)
|
// load in the case of the threaded tile pager)
|
||||||
loader.update();
|
loader.update();
|
||||||
|
|
||||||
|
// load the next model in the load queue. Currently this must
|
||||||
|
// happen in the render thread because model loading can trigger
|
||||||
|
// texture loading which involves use of the opengl api.
|
||||||
|
if ( !model_queue.empty() ) {
|
||||||
|
cout << "loading next model ..." << endl;
|
||||||
|
// load the next tile in the queue
|
||||||
|
#ifdef ENABLE_THREADS
|
||||||
|
FGDeferredModel* dm = model_queue.pop();
|
||||||
|
#else
|
||||||
|
FGDeferredModel* dm = model_queue.front();
|
||||||
|
model_queue.pop();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ssgTexturePath( (char *)(dm->get_texture_path().c_str()) );
|
||||||
|
ssgEntity *obj_model
|
||||||
|
= ssgLoad( (char *)(dm->get_model_path().c_str()) );
|
||||||
|
dm->get_obj_trans()->addKid( obj_model );
|
||||||
|
dm->get_tile()->dec_pending_models();
|
||||||
|
|
||||||
|
delete dm;
|
||||||
|
}
|
||||||
|
|
||||||
if ( scenery.center == Point3D(0.0) ) {
|
if ( scenery.center == Point3D(0.0) ) {
|
||||||
// initializing
|
// initializing
|
||||||
cout << "initializing scenery current elevation ... " << endl;
|
cout << "initializing scenery current elevation ... " << endl;
|
||||||
|
@ -412,16 +418,15 @@ int FGTileMgr::update( double lon, double lat ) {
|
||||||
// Notify the tile loader that it can load another tile
|
// Notify the tile loader that it can load another tile
|
||||||
// loader.update();
|
// loader.update();
|
||||||
|
|
||||||
if ( !loaded_queue.empty() ) {
|
if ( !attach_queue.empty() ) {
|
||||||
#ifdef ENABLE_THREADS
|
#ifdef ENABLE_THREADS
|
||||||
FGTileEntry* e = loaded_queue.pop();
|
FGTileEntry* e = attach_queue.pop();
|
||||||
#else
|
#else
|
||||||
FGTileEntry* e = loaded_queue.front();
|
FGTileEntry* e = attach_queue.front();
|
||||||
loaded_queue.pop();
|
attach_queue.pop();
|
||||||
#endif
|
#endif
|
||||||
e->add_ssg_nodes( terrain, ground );
|
e->add_ssg_nodes( terrain, ground );
|
||||||
//std::cout << "Adding ssg nodes for "
|
// cout << "Adding ssg nodes for "
|
||||||
//<< e->get_tile_bucket() << "\n";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
|
|
||||||
// forward declaration
|
// forward declaration
|
||||||
class FGTileEntry;
|
class FGTileEntry;
|
||||||
|
class FGDeferredModel;
|
||||||
|
|
||||||
|
|
||||||
class FGTileMgr {
|
class FGTileMgr {
|
||||||
|
@ -119,20 +120,38 @@ private:
|
||||||
int counter_hack;
|
int counter_hack;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tiles to add to scene graph.
|
* Work queues.
|
||||||
|
*
|
||||||
|
* attach_queue is the tiles that have been loaded [by the pager]
|
||||||
|
* that can be attached to the scene graph by the render thread.
|
||||||
|
*
|
||||||
|
* model_queue is the set of models that need to be loaded by the
|
||||||
|
* primary render thread.
|
||||||
*/
|
*/
|
||||||
#ifdef ENABLE_THREADS
|
#ifdef ENABLE_THREADS
|
||||||
static SGLockedQueue<FGTileEntry*> loaded_queue;
|
static SGLockedQueue<FGTileEntry *> attach_queue;
|
||||||
|
static SGLockedQueue<FGDeferredModel *> model_queue;
|
||||||
#else
|
#else
|
||||||
static queue<FGTileEntry*> loaded_queue;
|
static queue<FGTileEntry *> attach_queue;
|
||||||
|
static queue<FGDeferredModel *> model_queue;
|
||||||
#endif // ENABLE_THREADS
|
#endif // ENABLE_THREADS
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a loaded tile to the scene graph queue.
|
* Add a loaded tile to the 'attach to the scene graph' queue.
|
||||||
*/
|
*/
|
||||||
static void loaded( FGTileEntry* t ) { loaded_queue.push(t); }
|
static void ready_to_attach( FGTileEntry *t ) { attach_queue.push( t ); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tile is detatched from scene graph and is ready to delete
|
||||||
|
*/
|
||||||
|
inline void ready_to_delete( FGTileEntry *t ) { loader.remove( t ); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a pending model to the 'deferred model load' queue
|
||||||
|
*/
|
||||||
|
static void model_ready( FGDeferredModel *dm ) { model_queue.push( dm ); }
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue