Re: [GtkGLExt] capturing video clips
- From: cbeau <cbeauc gmail com>
- To: "z pekar gmail com" <z pekar gmail com>
- Cc: gtkglext-list gnome org
- Subject: Re: [GtkGLExt] capturing video clips
- Date: Mon, 25 Feb 2008 18:07:17 -0500
I am capturing my GL surface to a movie file using an open-source movie encoder called "transcode" and getting the pixel data with a call to "gdk_pixbuf_get_from_drawable" and then "piping" that data through an open pipe to transcode. Please find the code for my program attached. I have to warn you though: the code is a bit messy. The important bits are mainly:
---------------------------------------
length += sprintf( movie_command, "transcode -i /dev/fd/0 --use_rgb -x raw,null -g %dx%d -o \"%s\" -f %d -w %d ", scapture.enc_width, scapture.enc_height, gtk_entry_get_text(GTK_ENTRY(scapture.movie_name)), nfps, nkbps );
length += sprintf(movie_command+length, "-y xvid,null");
scapture.pipe = popen( movie_command, "w" );
Then, on every refresh of the screen (every step of my simulation), I do:
screenshot = gdk_pixbuf_get_from_drawable( screenshot, GDK_DRAWABLE(GTK_WIDGET(image)->window), gdk_colormap_get_system(), 0, 0, 0, 0, scapture.enc_width, scapture.enc_height );
fwrite( gdk_pixbuf_get_pixels(GDK_PIXBUF(screenshot)), gdk_pixbuf_get_rowstride(GDK_PIXBUF(screenshot)), gdk_pixbuf_get_height(GDK_PIXBUF(screenshot)), scapture.pipe );
And when I'm done, I do
pclose( scapture.pipe );
By the way:
GtkWidget *image;
image = gtk_drawing_area_new();
gtk_widget_set_size_request( GTK_WIDGET(image), 100, 100);
g_signal_connect(GTK_OBJECT(image), "expose_event", G_CALLBACK(expose_arena), NULL);
gtk_widget_set_gl_capability( image, glconfig, NULL, FALSE, GDK_GL_RGBA_TYPE );
---------------------------------------------
The real issue with this which is also an issue for taking screenshots, is that the obscured parts of an onscreen widget like a gtk_drawing_area will have "junk" in them (not get rendered) and so there will be junk in your captured image/movie. For example, I'd go for a coffee, my screensaver would kick in and my encoder would record nothing but junk.
To prevent this, it is good to switch to using an offscreen widget. For example, use a GtkImage to display an offscreen "GdkPixmap" and render your OpenGL scenes to the GdkPixmap. This way, the GdkPixmap is always fully rendered because it is offscreen and "captures" of the GdkPixmap (NOT THE GtkImage) are always right regardless of whether GtkImage is fully rendered or not. However, I have had tones of troubles getting GdkPixmap rendering to work and it still doesn't work on many graphics card (offscreen rendering not working well at all!). If you are using a GdkPixmap, then the corresponding call above would be replaced by:
---------------------------------------
screenshot = gdk_pixbuf_get_from_drawable( screenshot, GDK_DRAWABLE(pixmap), gdk_colormap_get_system(), 0, 0, 0, 0, scapture.enc_width, scapture.enc_height );
Where:
GdkPixmap *pixmap;
pixmap = gdk_pixmap_new( NULL, width, height, gdk_gl_config_get_depth(glconfig) );
glpixmap = gdk_pixmap_set_gl_capability( pixmap, glconfig, NULL );
gtk_image_set_from_pixmap( GTK_IMAGE(image), pixmap, NULL );
-----------------------------------------
If you want to download the full program to try it out and get a better feel for how the code relates the the actual GUI, then feel free to download from:
http://masyv.sourceforge.net
Cheers,
cbeau.
/*
Program:
masyv
Description:
A generic user interface for the visualisation of
multi-agent systems.
Copyright (C) 2002 Catherine Beauchemin
Authors: Catherine Beauchemin & Kipp Cannon
Contacts: cbeau users sourceforge net
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <gtk/gtk.h>
#include <gtk/gtkgl.h>
#include <masyv/masyv.h>
#include "MASyV_logo.xpm"
#define PROGRAM_NAME "masyv"
int GLX_PIXMAP_WORKS = 0;
char ui_socket_address[] = "/tmp/maXXXXXX";
int client_socket_fd = -1;
FILE *stat_log_file = NULL;
int process_id = -1;
int auto_advance_handle = -1;
int max_time = -1; /* Time at which MASyV will pause the client */
int time_incr = 1; /* Show ever time_incr time steps in stats and visualization */
GtkWidget *main_window;
GtkWidget *time_step_label;
GtkWidget *hidden_button;
GtkWidget *image;
GdkPixmap *pixmap;
GdkGLConfig *glconfig;
/* Stats parameters */
struct stats_t {
GtkTextBuffer *text_buffer;
GtkWidget *text_view;
GtkWidget *log_name;
} sstats;
/* Movie parameters */
struct capture_t {
GtkWidget *movie_name;
GtkWidget *codec;
GtkWidget *framerate;
GtkWidget *bitrate;
FILE *pipe;
int enc_width, enc_height;
};
struct capture_t scapture = {
pipe: NULL
};
struct image_t {
int number;
float opacity;
};
struct layer_t {
int numimage;
int *width, *height; /* Image size */
GLuint *texname;
struct image_t **imgtable;
};
struct arena_t {
int current_time_step;
GdkGLContext *glcontext;
GdkGLDrawable *gldrawable;
float rescale_factor; /* Factor by which to rescale the grid's dimensions */
float clip_factor; /* Proportion by which to divide (clip) the area */
int width, height;
int nwidth, nheight; /* width and height offset due to geometry of unit_cell */
float unit_cell[2][2]; /* see explanation below */
int numlayer;
struct layer_t *layer;
};
struct arena_t arena = {
rescale_factor: 1.0,
clip_factor: 1.0
};
/* the matrix unit_cell is used to store the unit vectors
that are necessary to define the unit cells. The matrix
is defined as (unit_cell = E):
__ __ _ _ _ _
| | | | | |
| E[0][0] E[0][1] | | X | | X_N |
| | | | = | |
| E[1][0] E[1][1] | | Y | | Y_N |
|__ __| |_ _| |_ _|
and thus:
X_N = E[0][0] * X + E[0][1] * Y;
Y_N = E[1][0] * X + E[1][1] * Y;
*/
void kill_client( void )
{
if( process_id >= 0 ) {
masyv_send_terminate_signal( client_socket_fd );
kill( process_id, SIGKILL );
process_id = -1;
}
}
void destroy( void )
{
kill_client();
remove( ui_socket_address ); /* closing and removing socket */
gtk_main_quit();
}
void destroy_widget( GtkWidget *widget, gpointer data)
{
GtkWidget *widget_to_kill = (GtkWidget *) data;
gtk_widget_destroy( widget_to_kill );
}
void swap_the_buffers( GdkGLDrawable *gldrawable )
{
if( !GLX_PIXMAP_WORKS )
gdk_gl_drawable_swap_buffers( gldrawable );
}
void initialize_arena( struct arena_t *arena, float width, float height )
{
GdkGLPixmap *glpixmap;
if( GLX_PIXMAP_WORKS ) {
pixmap = gdk_pixmap_new( NULL, width, height, gdk_gl_config_get_depth(glconfig) );
glpixmap = gdk_pixmap_set_gl_capability( pixmap, glconfig, NULL );
gtk_image_set_from_pixmap( GTK_IMAGE(image), pixmap, NULL );
arena->gldrawable = gdk_pixmap_get_gl_drawable(pixmap);
arena->glcontext = gdk_gl_context_new( arena->gldrawable, NULL, FALSE, GDK_GL_RGBA_TYPE );
}
else {
gtk_widget_set_size_request( GTK_WIDGET(image), width, height );
arena->gldrawable = gtk_widget_get_gl_drawable( image );
arena->glcontext = gtk_widget_get_gl_context( image );
}
if( gdk_gl_drawable_gl_begin(arena->gldrawable, arena->glcontext) ) {
glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glClearColor( 1.0, 1.0, 1.0, 1.0 );
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glViewport(0, 0, width, height);
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
glOrtho( 0.0, width/arena->rescale_factor, height/arena->rescale_factor, 0.0, -1.0, 0.0 );
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glFlush();
swap_the_buffers( arena->gldrawable );
gdk_gl_drawable_gl_end( arena->gldrawable );
}
gtk_widget_queue_draw( image );
}
void draw_agent( int rx, int ry, int ln, int in, float opacity, struct arena_t *arena )
{
float dx,dy;
dx = arena->layer[ln].width[in];
dy = arena->layer[ln].height[in];
if( opacity < 1.0 ) {
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
glColor4f(1.0, 1.0, 1.0, opacity);
}
else
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
glBindTexture( GL_TEXTURE_2D, arena->layer[ln].texname[in] );
glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0); glVertex2f( rx-dx, ry-dy );
glTexCoord2f(0.0, 1.0); glVertex2f( rx-dx, ry+dy );
glTexCoord2f(1.0, 1.0); glVertex2f( rx+dx, ry+dy );
glTexCoord2f(1.0, 0.0); glVertex2f( rx+dx, ry-dy );
glEnd();
}
void draw_all_agents( struct arena_t *arena )
{
int ln, x, y;
float rx, ry;
for( ln = 0; ln < arena->numlayer; ln++ )
for( x = arena->nwidth; x < arena->width; x++ )
for( y = arena->nheight; y < arena->height; y++ )
if( arena->layer[ln].imgtable[x][y].number ) {
rx = (x+0.5)*arena->unit_cell[0][0] + (y+0.5)*arena->unit_cell[0][1];
ry = (x+0.5)*arena->unit_cell[1][0] + (y+0.5)*arena->unit_cell[1][1];
draw_agent( rx, ry, ln, arena->layer[ln].imgtable[x][y].number, arena->layer[ln].imgtable[x][y].opacity, arena );
}
}
/* When widget is exposed its content is redrawn. */
gint expose_arena( GtkWidget *widget, GdkEventExpose *event )
{
GdkGLContext *glcontext = gtk_widget_get_gl_context( widget );
GdkGLDrawable *gldrawable = gtk_widget_get_gl_drawable( widget );
/* Draw only last expose. */
if( event->count > 0 )
return(1);
if(!gdk_gl_drawable_gl_begin( gldrawable, glcontext ) )
return(0);
glClear( GL_COLOR_BUFFER_BIT );
if( arena.layer != NULL )
draw_all_agents( &arena );
swap_the_buffers( gldrawable );
gdk_gl_drawable_gl_end( gldrawable );
return(1);
}
void take_screenshot( GtkWidget *ok_button, char *filename )
{
static GdkPixbuf *screenshot = NULL;
if( GLX_PIXMAP_WORKS )
screenshot = gdk_pixbuf_get_from_drawable( screenshot, GDK_DRAWABLE(pixmap), gdk_colormap_get_system(), 0, 0, 0, 0, scapture.enc_width, scapture.enc_height );
else
screenshot = gdk_pixbuf_get_from_drawable( screenshot, GDK_DRAWABLE(GTK_WIDGET(image)->window), gdk_colormap_get_system(), 0, 0, 0, 0, scapture.enc_width, scapture.enc_height );
gdk_pixbuf_save( GDK_PIXBUF(screenshot), filename, "png", NULL, NULL );
g_free( filename );
}
void get_log_file_name( GtkWidget *ok_button, char *filename )
{
gtk_entry_set_text( GTK_ENTRY(sstats.log_name), filename );
g_free(filename);
}
void get_movie_name( GtkWidget *ok_button, char *filename )
{
gtk_entry_set_text( GTK_ENTRY(scapture.movie_name), filename );
g_free(filename);
}
void file_name_changed( GtkWidget *widget, GtkWidget *check_button )
{
if( strlen( gtk_entry_get_text(GTK_ENTRY(widget)) ) < 1 )
gtk_widget_set_sensitive( GTK_WIDGET(check_button), FALSE );
else
gtk_widget_set_sensitive( GTK_WIDGET(check_button), TRUE );
}
void check_button_clicked( GtkWidget *widget, gpointer which_check_button )
{
typedef enum {
Xvid,
Raw,
WindowsMedia,
QuickTime
} encode_type_t;
char movie_command[300];
int iter_codec = gtk_combo_box_get_active( GTK_COMBO_BOX(scapture.codec) );
int nfps = gtk_spin_button_get_value_as_int( GTK_SPIN_BUTTON(scapture.framerate) );
int nkbps = gtk_spin_button_get_value_as_int( GTK_SPIN_BUTTON(scapture.bitrate) );
int length = 0;
/* If it is the checkbox for saving animation */
if( which_check_button == "movie" ) {
if( GTK_TOGGLE_BUTTON( widget )->active ) {
length += sprintf( movie_command, "transcode -i /dev/fd/0 --use_rgb -x raw,null -g %dx%d -o \"%s\" -f %d -w %d ", scapture.enc_width, scapture.enc_height, gtk_entry_get_text(GTK_ENTRY(scapture.movie_name)), nfps, nkbps );
switch( iter_codec ) {
case Raw:
length += sprintf(movie_command+length, "-z --use_rgb -k -y raw,null");
break;
case WindowsMedia:
length += sprintf(movie_command+length, "-y ffmpeg,null -F wmv1");
break;
case QuickTime:
length += sprintf(movie_command+length, "-y mov,null -F mjpa,null" );
break;
case Xvid:
default:
length += sprintf(movie_command+length, "-y xvid,null");
break;
}
/* Tentative command to eventually switch to GStreamer */
/* length += sprintf( movie_command, "gst-launch fdsrc fd=0 ! video/x-raw-rgb, width=%s, height=%s, bpp=32, depth=24 ! ffmpegcolorspace ! theoraenc quality=32 ! oggmux ! filesink location=\"%s\"", scapture.enc_width, scapture.enc_height, gtk_entry_get_text(GTK_ENTRY(scapture.movie_name)) ); */
scapture.pipe = popen( movie_command, "w" );
if( !scapture.pipe ) {
gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON(widget), FALSE );
fprintf( stderr, "%s: cannot open movie pipe", PROGRAM_NAME );
return;
}
}
else {
pclose( scapture.pipe );
scapture.pipe = NULL;
gtk_widget_set_sensitive( GTK_WIDGET(widget), FALSE );
}
}
/* If it is the checkbox for logging the stats to file */
else {
if( GTK_TOGGLE_BUTTON( widget )->active ) {
stat_log_file = fopen(gtk_entry_get_text(GTK_ENTRY(sstats.log_name)) , "a");
if( !stat_log_file ) {
gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON(widget), FALSE);
fprintf( stderr, "cannot open file" );
return;
}
}
else if( stat_log_file ) {
fclose( stat_log_file );
stat_log_file = NULL;
}
}
}
void open_file_selection( GtkWidget *widget, gpointer data )
{
GtkWidget *dialog;
char *filename = NULL;
void (*filename_handler)(GtkWidget *, char *) = data;
/* Function to call if a filename selected */
dialog = gtk_file_chooser_dialog_new("Select File...", NULL, GTK_FILE_CHOOSER_ACTION_SAVE, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, NULL);
if( gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT ) {
filename = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(dialog) );
}
gtk_widget_destroy( dialog );
if( filename != NULL )
filename_handler( widget, filename );
}
void receive_pixtablespec(float grid_unit_cell[2][2], int sum_layer_sizes, int numlayer, int *numimage, void *data)
{
struct arena_t *arena = (struct arena_t * ) data;
int i,j;
for( i = 0; i < 2; i++)
for( j = 0; j < 2; j++)
arena->unit_cell[i][j] = grid_unit_cell[i][j];
if( arena->layer != NULL ) {
free( arena->layer[0].width );
free( arena->layer[0].height );
free( arena->layer[0].texname );
free( arena->layer );
}
arena->numlayer = numlayer;
arena->layer = ( struct layer_t * ) malloc( arena->numlayer * sizeof( struct layer_t ) );
for( i = 0; i < arena->numlayer; i++ )
arena->layer[i].numimage = numimage[i];
arena->layer[0].width = ( int * ) malloc( sum_layer_sizes * sizeof( int ) );
arena->layer[0].height = ( int * ) malloc( sum_layer_sizes * sizeof( int ) );
arena->layer[0].texname = ( GLuint * ) malloc( sum_layer_sizes * sizeof( GLuint ) );
for( i = 1; i < arena->numlayer; i++ ) {
arena->layer[i].width = arena->layer[i-1].width + arena->layer[i-1].numimage;
arena->layer[i].height = arena->layer[i-1].height + arena->layer[i-1].numimage;
arena->layer[i].texname = arena->layer[i-1].texname + arena->layer[i-1].numimage;
}
}
void receive_arena_size(int width, int height, void *data)
{
struct arena_t *arena = (struct arena_t * ) data;
float transfw = width*arena->unit_cell[0][0];
float transfh = height*arena->unit_cell[1][1];
int sufficient_width, sufficient_height;
int ln, x;
arena->width = width;
arena->height = height;
arena->nwidth = -ceilf((float)height*arena->unit_cell[0][1]/arena->unit_cell[1][1]);
arena->nheight = -ceilf((float)width*arena->unit_cell[1][0]/arena->unit_cell[0][0]);
transfw *= arena->rescale_factor*arena->clip_factor;
transfh *= arena->rescale_factor*arena->clip_factor;
initialize_arena( arena, transfw, transfh );
/* Encoders encode better with heights or widths that are multiples of 16 */
/* so the area to be encoded is clipped to satisfy this. */
scapture.enc_width = (int)transfw - (int)transfw%16;
scapture.enc_height = (int)transfh - (int)transfh%16;
/* For a different geometry */
sufficient_width = width - arena->nwidth;
sufficient_height = height - arena->nheight;
/* Allocate the image table */
for( ln = 0; ln < arena->numlayer; ln++ ) {
arena->layer[ln].imgtable = (struct image_t **) malloc( sufficient_width * sizeof(struct image_t *) );
arena->layer[ln].imgtable[0] = (struct image_t *) calloc( sufficient_width * sufficient_height, sizeof(struct image_t) );
arena->layer[ln].imgtable[0] -= arena->nheight;
for( x = 1; x < sufficient_width; x++ )
arena->layer[ln].imgtable[x] = arena->layer[ln].imgtable[x-1] + sufficient_height;
arena->layer[ln].imgtable -= arena->nwidth;
}
}
void receive_texels(int ln,int in,int width, int height, unsigned char *pixels, void *data )
{
struct arena_t *arena = (struct arena_t *) data;
if( gdk_gl_drawable_gl_begin(arena->gldrawable, arena->glcontext) ) {
glGenTextures( 1, &arena->layer[ln].texname[in] );
glBindTexture(GL_TEXTURE_2D, arena->layer[ln].texname[in] );
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels );
gdk_gl_drawable_gl_end( arena->gldrawable );
}
arena->layer[ln].width[in] = width/2.0;
arena->layer[ln].height[in] = height/2.0;
}
void receive_start_step(int total_time, int clear, void *data)
{
struct arena_t *arena = (struct arena_t *) data;
char stotal_time[12];
int ln;
arena->current_time_step = total_time;
sprintf( stotal_time, "%d", arena->current_time_step );
gtk_label_set_text( GTK_LABEL( time_step_label ), stotal_time );
if( clear )
for( ln = 0; ln < arena->numlayer; ln++ )
memset( arena->layer[ln].imgtable[0], 0, arena->width*arena->height*sizeof(struct image_t) );
}
void receive_agent( int x, int y, int ln, int in, int opacity, void *data )
{
struct arena_t *arena = (struct arena_t *) data;
arena->layer[ln].imgtable[x][y].number = in;
arena->layer[ln].imgtable[x][y].opacity = opacity/255.0;
}
void receive_stats(char *stats, void *data)
{
GtkTextIter iter;
gtk_text_buffer_get_end_iter(sstats.text_buffer, &iter);
gtk_text_buffer_insert(sstats.text_buffer, &iter, stats, -1);
gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(sstats.text_view),&iter,0,FALSE,1,0);
if( stat_log_file != NULL ) {
fprintf( stat_log_file, "%s", stats );
}
}
void receive_terminate_signal(void *data)
{
GtkWidget *finished_window;
GtkWidget *button;
GtkWidget *label;
GtkWidget *vbox;
/* Terminate the client and remove the socket */
g_source_remove( auto_advance_handle );
auto_advance_handle = -1;
kill_client();
remove( ui_socket_address );
/* Tell the user it's all over now */
/* FIXME = make this more robust */
finished_window = gtk_window_new( GTK_WINDOW_TOPLEVEL );
gtk_window_set_title( GTK_WINDOW(finished_window), "Client finished");
g_signal_connect( GTK_OBJECT(finished_window), "destroy", G_CALLBACK(destroy_widget), finished_window );
vbox = gtk_vbox_new( FALSE, 0 );
gtk_container_set_border_width( GTK_CONTAINER (vbox), 15);
gtk_container_add( GTK_CONTAINER( finished_window ), vbox );
button = gtk_button_new_with_label( "OK" );
g_signal_connect( GTK_OBJECT( button ), "clicked", G_CALLBACK( destroy_widget ), finished_window );
gtk_box_pack_end( GTK_BOX( vbox ), button, FALSE, FALSE, 0 );
label = gtk_label_new("The client is done. You can quit MASyV now.\n");
gtk_label_set_line_wrap( GTK_LABEL(label), TRUE);
gtk_box_pack_start( GTK_BOX( vbox ), label, FALSE, FALSE, 0 );
gtk_widget_show_all(finished_window);
}
struct masyv_receive_ops_t masyv_receive_ops = {
masyv_receive_arena_size: receive_arena_size,
masyv_receive_pixtablespec: receive_pixtablespec,
masyv_receive_texels: receive_texels,
masyv_receive_start_step: receive_start_step,
masyv_receive_agent: receive_agent,
masyv_receive_stats: receive_stats,
masyv_receive_terminate_signal: receive_terminate_signal
};
gint advance_handler(GtkButton *button, gpointer data)
{
struct arena_t *arena = (struct arena_t *) data;
static GdkPixbuf *screenshot = NULL;
/* If no client has been selected yet */
if( client_socket_fd < 0 )
return( FALSE );
if( (arena->current_time_step < max_time) || (max_time < 0) ) {
masyv_send_req_time_advance( client_socket_fd, &masyv_receive_ops, time_incr, arena );
gdk_gl_drawable_gl_begin( arena->gldrawable, arena->glcontext );
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
draw_all_agents( arena );
glFlush();
swap_the_buffers( arena->gldrawable );
gdk_gl_drawable_gl_end( arena->gldrawable );
gtk_widget_queue_draw( image );
if( scapture.pipe ) {
if( GLX_PIXMAP_WORKS )
screenshot = gdk_pixbuf_get_from_drawable( screenshot, GDK_DRAWABLE(pixmap), gdk_colormap_get_system(), 0, 0, 0, 0, scapture.enc_width, scapture.enc_height );
else
screenshot = gdk_pixbuf_get_from_drawable( screenshot, GDK_DRAWABLE(GTK_WIDGET(image)->window), gdk_colormap_get_system(), 0, 0, 0, 0, scapture.enc_width, scapture.enc_height );
fwrite( gdk_pixbuf_get_pixels(GDK_PIXBUF(screenshot)), gdk_pixbuf_get_rowstride(GDK_PIXBUF(screenshot)), gdk_pixbuf_get_height(GDK_PIXBUF(screenshot)), scapture.pipe );
}
}
else {
g_source_remove( auto_advance_handle );
auto_advance_handle = -1;
}
return( TRUE );
}
gint continuous_advance( gpointer data )
{
advance_handler( NULL, data );
return( TRUE );
}
gint play_pause_handler( GtkWidget *button, gpointer data )
{
/* If no client has been selected yet */
if( client_socket_fd < 0 )
return( FALSE );
if( auto_advance_handle < 0 ) { /* Play handler */
auto_advance_handle = g_idle_add( continuous_advance, data );
}
else { /* Pause handler */
g_source_remove( auto_advance_handle );
auto_advance_handle = -1;
fflush( NULL ); /* flushes all output streams */
}
gtk_widget_show( hidden_button );
gtk_widget_hide( button );
hidden_button = button;
return( TRUE );
}
void open_client( void )
{
GtkWidget *dialog;
dialog = gtk_file_chooser_dialog_new( "Choose client simulation", NULL, GTK_FILE_CHOOSER_ACTION_OPEN, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, NULL);
if( gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT ) {
client_socket_fd = masyv_run_simulation( gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(dialog) ), 0, 0, NULL, ui_socket_address, PROGRAM_NAME, &process_id, &masyv_receive_ops, &arena );
}
gtk_widget_destroy( dialog );
}
static void attr_list_insert( PangoAttrList *attrlist, PangoAttribute *attr )
{
attr->start_index = 0;
attr->end_index = G_MAXINT;
pango_attr_list_insert( attrlist, attr );
}
void about_window( void )
{
GtkWidget *about_window;
GtkWidget *button;
GtkWidget *frame;
GtkWidget *hseparator;
GtkWidget *label;
GtkWidget *vbox, *hbox;
GdkPixmap *apixmap;
GdkBitmap *mask;
GtkWidget *aimage;
PangoAttrList *attrlist;
about_window = gtk_window_new( GTK_WINDOW_TOPLEVEL );
gtk_window_set_title( GTK_WINDOW(about_window), "About MASyV");
g_signal_connect( GTK_OBJECT(about_window), "destroy", G_CALLBACK(destroy_widget), about_window );
vbox = gtk_vbox_new( FALSE, 0 );
gtk_container_set_border_width( GTK_CONTAINER (vbox), 15);
gtk_container_add( GTK_CONTAINER( about_window ), vbox );
button = gtk_button_new_with_label( "OK" );
g_signal_connect( GTK_OBJECT( button ), "clicked", G_CALLBACK( destroy_widget ), about_window );
gtk_box_pack_end( GTK_BOX( vbox ), button, FALSE, FALSE, 0 );
hseparator = gtk_hseparator_new();
gtk_box_pack_end( GTK_BOX( vbox ), hseparator, TRUE, FALSE, 10 );
hbox = gtk_hbox_new( FALSE, 10 );
gtk_box_pack_start( GTK_BOX( vbox ), hbox, FALSE, FALSE, 0 );
apixmap = gdk_pixmap_create_from_xpm_d( main_window->window, &mask, NULL, MASyV_logo_xpm );
aimage = gtk_image_new_from_pixmap( apixmap, mask );
gtk_container_add( GTK_CONTAINER( hbox ), aimage );
vbox = gtk_vbox_new( FALSE, 5 );
gtk_box_pack_start( GTK_BOX( hbox ), vbox, FALSE, FALSE, 0 );
attrlist = pango_attr_list_new();
attr_list_insert( attrlist, pango_attr_size_new( 20*PANGO_SCALE ) );
attr_list_insert( attrlist, pango_attr_weight_new( PANGO_WEIGHT_BOLD ) );
attr_list_insert( attrlist, pango_attr_foreground_new( 8738, 39835, 5911 ) );
label = gtk_label_new( "MASyV - Version " PACKAGE_VERSION );
gtk_label_set_attributes( GTK_LABEL(label), attrlist );
gtk_label_set_justify( GTK_LABEL(label), GTK_JUSTIFY_CENTER );
gtk_label_set_line_wrap( GTK_LABEL(label), FALSE );
gtk_box_pack_start( GTK_BOX( vbox ), label, FALSE, FALSE, 0 );
pango_attr_list_unref( attrlist );
frame = gtk_frame_new(NULL);
gtk_frame_set_shadow_type( GTK_FRAME( frame ), GTK_SHADOW_IN );
gtk_box_pack_start( GTK_BOX( vbox ), frame, FALSE, FALSE, 0 );
vbox = gtk_vbox_new( FALSE, 5 );
gtk_container_set_border_width( GTK_CONTAINER (vbox), 10);
gtk_container_add( GTK_CONTAINER( frame ), vbox );
label = gtk_label_new( "A Multi-Agent System Vizualization Program\nDistributed under the GNU GPL v2.0" );
gtk_label_set_line_wrap( GTK_LABEL(label), TRUE);
gtk_box_pack_start( GTK_BOX( vbox ), label, FALSE, FALSE, 0 );
label = gtk_label_new( "http://masyv.sourceforge.net/" );
gtk_label_set_line_wrap( GTK_LABEL(label), TRUE);
gtk_box_pack_start( GTK_BOX( vbox ), label, FALSE, FALSE, 0 );
label = gtk_label_new( "Copyright (C) 2002 - Catherine Beauchemin" );
gtk_label_set_line_wrap( GTK_LABEL(label), TRUE);
gtk_box_pack_start( GTK_BOX( vbox ), label, FALSE, FALSE, 0 );
label = gtk_label_new( "Authors:\n Catherine Beauchemin <"PACKAGE_BUGREPORT">\n Kipp Cannon" );
gtk_label_set_justify( GTK_LABEL(label), GTK_JUSTIFY_LEFT);
gtk_label_set_line_wrap( GTK_LABEL(label), TRUE);
gtk_box_pack_start( GTK_BOX( vbox ), label, FALSE, FALSE, 0 );
gtk_widget_show_all(about_window);
}
void make_gui( GtkWidget *moviebutton, GtkWidget *statbutton )
{
static GtkActionEntry menu_entries[] = {
{ "FileMenu", NULL, "_File" },
{ "SimMenu", NULL, "_Simulation" },
{ "HelpMenu", NULL, "_Help" },
{ "Quit", GTK_STOCK_QUIT, "_Quit", "<control>Q", "Exit the program", destroy },
{ "SimRun", GTK_STOCK_EXECUTE, "_Run simulation...", NULL, "Start a client simulation from file.", open_client },
{ "SimKill", GTK_STOCK_CANCEL, "_Kill simulation", NULL, "Kill client simulation.", kill_client },
{ "HelpAbout", NULL, "_About", NULL, "About MASyV...", about_window },
/* { "HelpAbout", GTK_STOCK_ABOUT, "_About", NULL, "About MASyV...", about_window },*/
};
static const char *ui_description =
"<ui>"
" <menubar name='MainMenu'>"
" <menu action='FileMenu'>"
" <menuitem action='Quit'/>"
" </menu>"
" <menu action='SimMenu'>"
" <menuitem action='SimRun'/>"
" <menuitem action='SimKill'/>"
" </menu>"
" <menu action='HelpMenu'>"
" <menuitem action='HelpAbout'/>"
" </menu>"
" </menubar>"
"</ui>";
GtkActionGroup *action_group;
GtkUIManager *ui_manager;
GtkAccelGroup *accel_group;
GError *error;
GtkWidget *menubar;
struct rt_t {
GtkWidget *time;
GtkWidget *units;
} real_time;
GtkObject *spinbutton_adj;
GdkPixmap *apixmap;
GdkBitmap *mask;
GtkWidget *button;
GtkWidget *frame;
GtkWidget *combo;
GtkWidget *label;
GtkWidget *notebook;
GtkWidget *scrolledwindow;
GtkWidget *vbox,*svbox,*hbox;
/* Creates the main Window ; sets its title ; attaches the closing behavior */
main_window = gtk_window_new( GTK_WINDOW_TOPLEVEL );
gtk_window_set_title( GTK_WINDOW(main_window), "MASyV");
g_signal_connect( GTK_OBJECT(main_window), "destroy", G_CALLBACK(destroy), NULL );
gtk_quit_add_destroy(1, GTK_OBJECT(main_window));
vbox = gtk_vbox_new( FALSE, 0 );
gtk_container_add( GTK_CONTAINER( main_window ), vbox );
/* Create Menu */
action_group = gtk_action_group_new ("MenuActions");
gtk_action_group_add_actions( action_group, menu_entries, G_N_ELEMENTS(menu_entries), main_window);
ui_manager = gtk_ui_manager_new();
gtk_ui_manager_insert_action_group( ui_manager, action_group, 0 );
accel_group = gtk_ui_manager_get_accel_group( ui_manager );
gtk_window_add_accel_group( GTK_WINDOW(main_window), accel_group );
error = NULL;
if( !gtk_ui_manager_add_ui_from_string( ui_manager, ui_description, -1, &error ) ) {
g_message( "Building menus failed: %s", error->message );
g_error_free( error );
exit( EXIT_FAILURE );
}
menubar = gtk_ui_manager_get_widget( ui_manager, "/MainMenu" );
gtk_box_pack_start( GTK_BOX( vbox ), menubar, FALSE, FALSE, 0 );
/* Create the Notebook */
notebook = gtk_notebook_new();
gtk_notebook_set_tab_pos( GTK_NOTEBOOK( notebook ), GTK_POS_TOP );
gtk_box_pack_start( GTK_BOX( vbox ), notebook, TRUE, TRUE, 0 );
/* Create Simulation TAB */
vbox = gtk_vbox_new( FALSE, 10 );
gtk_container_set_border_width( GTK_CONTAINER( vbox ), 10 );
gtk_notebook_append_page( GTK_NOTEBOOK( notebook ), vbox, gtk_label_new( "Simulation" ) );
/* Create frame to hold the arena.layout */
hbox = gtk_hbox_new( FALSE, 0 );
gtk_box_pack_start( GTK_BOX( vbox ), hbox, TRUE, FALSE, 0 );
frame = gtk_frame_new(NULL);
gtk_frame_set_shadow_type( GTK_FRAME(frame), GTK_SHADOW_IN );
gtk_box_pack_start( GTK_BOX( hbox ), frame, TRUE, FALSE, 0 );
/* Create glconfig attributes for gtkglext */
if( GLX_PIXMAP_WORKS )
glconfig = gdk_gl_config_new_by_mode( GDK_GL_MODE_RGBA | GDK_GL_MODE_DEPTH );
else
glconfig = gdk_gl_config_new_by_mode( GDK_GL_MODE_RGBA | GDK_GL_MODE_DEPTH | GDK_GL_MODE_DOUBLE );
if( glconfig == NULL ) {
g_print ("*** Problem with gtkglext.\n");
exit(1);
}
if( GLX_PIXMAP_WORKS ) {
/* Create the GtkImage that will house the GdkPixmap used for OpenGL rendering */
gtk_widget_show_all( main_window );
apixmap = gdk_pixmap_create_from_xpm_d( main_window->window, &mask, NULL, MASyV_logo_xpm );
image = gtk_image_new_from_pixmap( apixmap, mask );
}
else {
image = gtk_drawing_area_new();
gtk_widget_set_size_request( GTK_WIDGET(image), 100, 100);
g_signal_connect(GTK_OBJECT(image), "expose_event", G_CALLBACK(expose_arena), NULL);
gtk_widget_set_gl_capability( image, glconfig, NULL, FALSE, GDK_GL_RGBA_TYPE );
}
gtk_container_add( GTK_CONTAINER(frame), image );
/* Create the bottom box for pause, play, etc. buttons */
hbox = gtk_hbox_new( FALSE, 10 );
/* Button to take screenshot */
button = gtk_button_new_with_label( "Screenshot" );
g_signal_connect( GTK_OBJECT( button ), "clicked",G_CALLBACK( open_file_selection ), take_screenshot );
gtk_box_pack_start( GTK_BOX( hbox ), button, FALSE, FALSE, 0 );
/* Button to advance by one step */
button = gtk_button_new_from_stock( GTK_STOCK_GO_FORWARD );
g_signal_connect( GTK_OBJECT( button ), "clicked", G_CALLBACK( advance_handler ), &arena );
gtk_box_pack_start( GTK_BOX( hbox ), button, FALSE, FALSE, 0 );
/* Button for Pause (starts off hidden) */
hidden_button = gtk_button_new_with_label( "Pause" );
/* hidden_button = gtk_button_new_from_stock( GTK_STOCK_MEDIA_PAUSE ); */
g_signal_connect( GTK_OBJECT(hidden_button), "clicked", G_CALLBACK( play_pause_handler ), &arena );
gtk_box_pack_start( GTK_BOX( hbox ), hidden_button, FALSE, FALSE, 0 );
/* Button for Play */
button = gtk_button_new_with_label( "Play" );
/* button = gtk_button_new_from_stock( GTK_STOCK_MEDIA_PLAY ); */
g_signal_connect( GTK_OBJECT(button), "clicked", G_CALLBACK( play_pause_handler ), &arena );
gtk_box_pack_start( GTK_BOX( hbox ), button, FALSE, FALSE, 0 );
/* Record to movie */
moviebutton = gtk_check_button_new_with_label( "Save animation." );
g_signal_connect( GTK_OBJECT( moviebutton ), "clicked", G_CALLBACK(check_button_clicked), "movie");
gtk_widget_set_sensitive( GTK_WIDGET(moviebutton), FALSE );
gtk_box_pack_start( GTK_BOX( hbox ), moviebutton, FALSE, TRUE, 0 );
/* Time step */
time_step_label = gtk_label_new( "" );
gtk_box_pack_end( GTK_BOX( hbox ), time_step_label, FALSE, FALSE, 0 );
label = gtk_label_new( "Step # :" );
gtk_box_pack_end( GTK_BOX( hbox ), label, FALSE, FALSE, 0 );
gtk_box_pack_end( GTK_BOX( vbox ), hbox, FALSE, FALSE, 0 );
/* Create Statistics TAB */
vbox = gtk_vbox_new( FALSE, 0 );
gtk_notebook_append_page( GTK_NOTEBOOK( notebook ), vbox, gtk_label_new( "Statistics" ) );
/* Create the text widget */
hbox = gtk_hbox_new( FALSE, 0 );
gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
sstats.text_view = gtk_text_view_new();
sstats.text_buffer = gtk_text_view_get_buffer( GTK_TEXT_VIEW(sstats.text_view) );
gtk_text_view_set_editable( GTK_TEXT_VIEW(sstats.text_view), FALSE );
gtk_text_view_set_cursor_visible( GTK_TEXT_VIEW(sstats.text_view), FALSE );
/* Add a scroll bar for the text view widget */
scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
gtk_container_add( GTK_CONTAINER(scrolledwindow), sstats.text_view );
gtk_box_pack_start(GTK_BOX(hbox), scrolledwindow, TRUE, TRUE, 0);
/* Create hbox to contain widgets for saving the stats to log file */
hbox = gtk_hbox_new( FALSE, 10 );
gtk_container_set_border_width( GTK_CONTAINER( hbox ), 10);
gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 0);
/* Create the check button for whether or not to write stats to file */
statbutton = gtk_check_button_new_with_label( "Log to file:" );
g_signal_connect( GTK_OBJECT( statbutton ), "clicked", G_CALLBACK(check_button_clicked), NULL);
gtk_widget_set_sensitive( GTK_WIDGET(statbutton), FALSE );
gtk_box_pack_start( GTK_BOX( hbox ), statbutton, FALSE, TRUE, 0 );
sstats.log_name = gtk_entry_new();
g_signal_connect( GTK_OBJECT(sstats.log_name),"changed",G_CALLBACK(file_name_changed), statbutton);
gtk_box_pack_start( GTK_BOX( hbox ), sstats.log_name, TRUE, TRUE, 0 );
button = gtk_button_new_with_label( "Browse..." );
g_signal_connect( GTK_OBJECT( button ), "clicked", G_CALLBACK(open_file_selection), get_log_file_name );
gtk_box_pack_start( GTK_BOX( hbox ), button, FALSE, TRUE, 0 );
/* Create Properties TAB */
vbox = gtk_vbox_new( FALSE, 0 );
gtk_notebook_append_page( GTK_NOTEBOOK( notebook ), vbox, gtk_label_new( "Properties" ) );
gtk_container_set_border_width( GTK_CONTAINER( vbox ), 10 );
/* Capture Option frame */
frame = gtk_frame_new( "Capture Options" );
gtk_box_pack_start( GTK_BOX( vbox ), frame, FALSE, FALSE, 0);
svbox = gtk_vbox_new( FALSE, 10 );
gtk_container_set_border_width( GTK_CONTAINER( svbox ), 5 );
gtk_container_add( GTK_CONTAINER( frame ), svbox );
/* File name */
hbox = gtk_hbox_new( FALSE, 10 );
gtk_box_pack_start( GTK_BOX( svbox ), hbox, FALSE, FALSE, 0);
label = gtk_label_new( "Filename:" );
gtk_box_pack_start( GTK_BOX( hbox ), label, FALSE, FALSE, 0);
scapture.movie_name = gtk_entry_new();
g_signal_connect( G_OBJECT(scapture.movie_name),"changed",G_CALLBACK(file_name_changed), moviebutton );
gtk_box_pack_start( GTK_BOX( hbox ), scapture.movie_name, TRUE, TRUE, 0 );
button = gtk_button_new_with_label( "Browse..." );
g_signal_connect( G_OBJECT( button ), "clicked",G_CALLBACK(open_file_selection), get_movie_name);
gtk_box_pack_start( GTK_BOX( hbox ), button, FALSE, TRUE, 0 );
/* Capture output format */
hbox = gtk_hbox_new( FALSE, 10 );
gtk_box_pack_start( GTK_BOX( svbox ), hbox, FALSE, FALSE, 0);
label = gtk_label_new( "Video CODEC:" );
gtk_box_pack_start( GTK_BOX( hbox ), label, FALSE, FALSE, 0);
scapture.codec = gtk_combo_box_new_text();
gtk_box_pack_start( GTK_BOX( hbox ), scapture.codec, FALSE, FALSE, 0);
gtk_combo_box_append_text( GTK_COMBO_BOX(scapture.codec), "XviD" );
gtk_combo_box_append_text( GTK_COMBO_BOX(scapture.codec), "Uncompressed" );
gtk_combo_box_append_text( GTK_COMBO_BOX(scapture.codec), "WindowsMedia" );
gtk_combo_box_append_text( GTK_COMBO_BOX(scapture.codec), "QuickTime" );
gtk_combo_box_set_active( GTK_COMBO_BOX(scapture.codec), 0 );
/* Frame rate */
hbox = gtk_hbox_new( FALSE, 10 );
gtk_box_pack_start( GTK_BOX( svbox ), hbox, FALSE, FALSE, 0);
label = gtk_label_new( "Playback frame rate:" );
gtk_box_pack_start( GTK_BOX( hbox ), label, FALSE, FALSE, 0);
spinbutton_adj = gtk_adjustment_new( 10, 1, INT_MAX, 1, 10, 10 );
scapture.framerate = gtk_spin_button_new( GTK_ADJUSTMENT(spinbutton_adj), 1, 0 );
gtk_box_pack_start( GTK_BOX( hbox ), scapture.framerate, FALSE, FALSE, 0);
label = gtk_label_new( "fps" );
gtk_box_pack_start( GTK_BOX( hbox ), label, FALSE, FALSE, 0);
/* Bitrate */
hbox = gtk_hbox_new( FALSE, 10 );
gtk_box_pack_start( GTK_BOX( svbox ), hbox, FALSE, FALSE, 0);
label = gtk_label_new( "Encoder bitrate:" );
gtk_box_pack_start( GTK_BOX( hbox ), label, FALSE, FALSE, 0);
spinbutton_adj = gtk_adjustment_new( 600, 1, INT_MAX, 1, 10, 10 );
scapture.bitrate = gtk_spin_button_new( GTK_ADJUSTMENT(spinbutton_adj), 1, 0 );
gtk_box_pack_start( GTK_BOX( hbox ), scapture.bitrate, FALSE, FALSE, 0);
label = gtk_label_new( "kbps" );
gtk_box_pack_start( GTK_BOX( hbox ), label, FALSE, FALSE, 0);
/* Display everything */
gtk_widget_show_all( main_window );
gtk_widget_hide( GTK_WIDGET(hidden_button) );
}
int main( int argc, char *argv[] )
{
int key;
char *simulation_name = NULL;
char *stat_file_name = NULL;
GtkWidget *moviebutton = NULL;
GtkWidget *statbutton = NULL;
/* Allowing GTK to retreive its command line options */
gtk_init( NULL, NULL );
/* gtk_init( &argc, &argv ); */
/* Allowing GtkGLExt to retreive its command line options */
gdk_gl_init( NULL, NULL );
/* gdk_gl_init( &argc, &argv ); */
/* Check if the OpenGL extension is supported. */
if( gdk_gl_query_extension() == FALSE ) {
g_print("OpenGL extension not supported\n");
exit(1);
}
/* Parsing masyv's command line options */
while( (key = getopt(argc, argv, "s:f:i:t:r:x:wh")) != -1 )
switch(key) {
case 's':
simulation_name = optarg;
break;
case 'f':
stat_file_name = optarg;
break;
case 'i':
time_incr = atoi(optarg);
break;
case 't':
max_time = atoi(optarg);
break;
case 'r':
arena.rescale_factor = atof(optarg);
break;
case 'x':
arena.clip_factor = atof(optarg);
break;
case 'w':
GLX_PIXMAP_WORKS = 1;
break;
case 'h':
default:
fprintf( stderr, "MASyV v"PACKAGE_VERSION" - Multi-Agent System Visualization (C) 2002 Catherine Beauchemin\n\n" );
fprintf( stderr, "Usage: masyv [masyv options] -- [client options]\n" );
fprintf( stderr, " e.g. masyv -s my_client -- --number_of_ants 3\n\n" );
fprintf( stderr, "MASyV options:\n" );
fprintf( stderr, "-s name name of client simulation to be run\n" );
fprintf( stderr, "-f file name of output file for the statistics\n" );
fprintf( stderr, "-i num print every num steps\n" );
fprintf( stderr, "-t time total number of steps (-1 is infinity)\n" );
fprintf( stderr, "-r factor rescaling factor for simulation area\n" );
fprintf( stderr, "-x factor clipping factor for simulation area\n" );
fprintf( stderr, "-w use off-screen instead of on-screen rendering (faster if supported)\n" );
fprintf( stderr, "-h displays options and exits\n" );
exit(0);
break;
}
/* Creates the user interface and all its widgets */
make_gui( moviebutton, statbutton );
arena.layer = NULL;
/* Setting values received from the command line */
if( simulation_name != NULL )
client_socket_fd = masyv_run_simulation( simulation_name, optind, argc, argv, ui_socket_address, PROGRAM_NAME, &process_id, &masyv_receive_ops, &arena );
if( stat_file_name != NULL ) {
gtk_entry_set_text( GTK_ENTRY(sstats.log_name), stat_file_name );
gtk_widget_set_sensitive( GTK_WIDGET(statbutton), TRUE );
gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON(statbutton), TRUE );
}
gtk_main();
exit(0);
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]