diff --git a/NOMADVRLib/ConfigFile.cpp b/NOMADVRLib/ConfigFile.cpp
index 7ba4e32e5ed358fd2dc42bbfa4a329362577aeda..a96909607a44bf6a110ecfbe66996ee0d8f2df87 100644
--- a/NOMADVRLib/ConfigFile.cpp
+++ b/NOMADVRLib/ConfigFile.cpp
@@ -82,6 +82,10 @@ menubutton_t menubutton;
 
 std::vector<information> info;
 
+int secret;
+const char * server;
+int port;
+
 const char * loadConfigFileErrors[] =
 {
 	"All Ok",//0
@@ -290,6 +294,10 @@ void initState()
 
 	bondscaling = 0.7f;
 	bondThickness = 1.0f;
+
+	secret=0;
+	server=nullptr;
+	port=-1;
 }
 
 int loadConfigFile(const char * f)
@@ -530,7 +538,7 @@ int loadConfigFile(const char * f)
 						return -11;
 			}
 		} else if (!strcmp(s, "atomglyph")) {
-			r=fscanf (F, "%s", s);
+			r=fscanf (F, "%99s", s);
 			if (r==0)
 				return -15;
 			if (!strcmp(s, "icosahedron"))
@@ -606,7 +614,7 @@ int loadConfigFile(const char * f)
 		} else if (!strcmp (s, "atomcolour")) {
 			char atom [100];
 			float rgb[3];
-			r = fscanf(F, "%s %f %f %f", atom, rgb, rgb + 1, rgb + 2);
+			r = fscanf(F, "%99s %f %f %f", atom, rgb, rgb + 1, rgb + 2);
 			if (r!=4) {
 				eprintf ("Error loading atom colour");
 				return -19;
@@ -622,7 +630,7 @@ int loadConfigFile(const char * f)
 			char atom [100];
 			float rgb[3];
 			float size;
-			r = fscanf(F, "%s %f %f %f %f", atom, rgb, rgb + 1, rgb + 2, &size);
+			r = fscanf(F, "%99s %f %f %f %f", atom, rgb, rgb + 1, rgb + 2, &size);
 			if (r!=5) {
 				eprintf ("Error loading newatom");
 				return -20;
@@ -711,7 +719,7 @@ int loadConfigFile(const char * f)
 				eprintf("Error reading bondthickness");
 		}
 		else if (!strcmp(s, "menubutton")) {
-			r = fscanf(F, "%s", s);
+			r = fscanf(F, "%99s", s);
 			if (!strcmp(s, "Record"))
 				menubutton = Record;
 			else if (!strcmp(s, "Infobox"))
@@ -743,6 +751,15 @@ int loadConfigFile(const char * f)
 #endif
 		} else if (!strcmp (s, "\x0d")) { //discard windows newline (problem in Sebastian Kokott's phone (?!)
 			continue;
+		} else if (!strcmp (s, "server")) { //multiuser support
+			int r;
+			if (server)
+				delete(server);
+			server=new char[100];
+			r=fscanf (F, "%99s %d %d", server, &port, &secret);
+			if (r<3) {
+				eprintf ("Error reading server paramters");
+			}
 		} else {
 			eprintf( "Unrecognized parameter %s\n", s);
 			for (int i=0;i<strlen(s);i++)
diff --git a/NOMADVRLib/ConfigFile.h b/NOMADVRLib/ConfigFile.h
index 8c9d967f9a92f930630cf4cfa0198112b707ea58..bf252fe11b9eda10e8d381b383da2aff9536d138 100644
--- a/NOMADVRLib/ConfigFile.h
+++ b/NOMADVRLib/ConfigFile.h
@@ -83,6 +83,11 @@ extern const char * loadConfigFileErrors[];
 void cleanConfig();
 int loadConfigFile(const char * f);
 
+//for multiuser
+extern int secret;
+extern const char * server;
+extern int port;
+
 struct information {
 	float pos[3];
 	float size;
diff --git a/NOMADVRLib/atomsGL.cpp b/NOMADVRLib/atomsGL.cpp
index 546257dbcc5269544e1b973a3700a85ff491098a..6c20dee2a10f701476430cd60adc40bec2c7bfe0 100644
--- a/NOMADVRLib/atomsGL.cpp
+++ b/NOMADVRLib/atomsGL.cpp
@@ -674,6 +674,8 @@ GLenum SetupMarker(GLuint *MarkerVAO, GLuint *MarkerVertBuffer)
 
 void CleanUnitCell (GLuint *UnitCellVAO, GLuint *UnitCellVertBuffer, GLuint *UnitCellIndexBuffer)
 {
+	if (!has_abc)
+		return;
 	glDeleteVertexArrays(1, UnitCellVAO);
 	glDeleteBuffers(1, UnitCellVertBuffer);
 	glDeleteBuffers(1, UnitCellIndexBuffer);
diff --git a/OpenVR/TimestepData/LoadPNG.cpp b/OpenVR/TimestepData/LoadPNG.cpp
index 0e9d259050005add5dbd6546fa45c93a6b0625c1..db94f670be05ac907f539db996f9f8c8ab79c909 100644
--- a/OpenVR/TimestepData/LoadPNG.cpp
+++ b/OpenVR/TimestepData/LoadPNG.cpp
@@ -1,5 +1,5 @@
 /*
-# Copyright 2016-2018 Ruben Jesus Garcia Hernandez
+# Copyright 2016-2018 Ruben Jesus Garcia-Hernandez
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
@@ -16,19 +16,24 @@
 
 #include <vector>
 #include "LoadPNG.h"
+#include "NOMADVRLib/eprintf.h"
 #include "shared/lodepng.h"
 
-GLuint LoadPNG (const char *image)
+GLuint LoadPNG(const char *image, int renderMode)
 {
-	GLuint m_iTexture;
-	glGenTextures(1, &m_iTexture);
-	glBindTexture(GL_TEXTURE_2D, m_iTexture);
-
 	std::vector<unsigned char> imageRGBA;
 	unsigned nImageWidth, nImageHeight;
 	unsigned nError = lodepng::decode(imageRGBA, nImageWidth, nImageHeight,
 			image);
-	
+	if (nError!=0) {
+		eprintf ("Error loading texture %s, lodepng::decode error %d", image, nError);
+		return 0;
+	}
+
+	GLuint m_iTexture;
+	glGenTextures(1, &m_iTexture);
+	glBindTexture(GL_TEXTURE_2D, m_iTexture);
+
 	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, nImageWidth, nImageHeight,
 			0, GL_RGBA, GL_UNSIGNED_BYTE, &imageRGBA[0]);
 
@@ -36,9 +41,19 @@ GLuint LoadPNG (const char *image)
 
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
-
+	switch (renderMode) {
+	case linear:
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+		break;
+	case nearest:
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+		break;
+	default:
+		eprintf("Error, unknown render mode in LoadPNG");
+		return 0;
+	}
 	//rgh fixme: revise this if texture sampling is too slow
 	GLfloat fLargest;
 	glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &fLargest);
diff --git a/OpenVR/TimestepData/LoadPNG.h b/OpenVR/TimestepData/LoadPNG.h
index 5f39aeeae127102733145cb76d71ff957736e74b..5cbd76c48f78151b17ea1ff7eb5f16c86f49e0f9 100644
--- a/OpenVR/TimestepData/LoadPNG.h
+++ b/OpenVR/TimestepData/LoadPNG.h
@@ -1,5 +1,5 @@
 /*
-# Copyright 2016-2018 Ruben Jesus Garcia Hernandez
+# Copyright 2016-2018 Ruben Jesus Garcia-Hernandez
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
@@ -18,6 +18,12 @@
 #define LOADPNG_H
 #include "NOMADVRLib/MyGL.h"
 
-GLuint LoadPNG (const char *image);
+enum RenderMode {
+	linear = 0,
+	nearest = 1,
+	error = 2
+};
+
+GLuint LoadPNG(const char *image, int renderMode=linear);
 
 #endif
diff --git a/OpenVR/TimestepData/hellovr_opengl_main.cpp b/OpenVR/TimestepData/hellovr_opengl_main.cpp
index 284461e02865bef4a3d0e4dcaf2c2e8f1fb726b6..ebbbaca103df9c50e6dada77cbd9b36df2d1634e 100644
--- a/OpenVR/TimestepData/hellovr_opengl_main.cpp
+++ b/OpenVR/TimestepData/hellovr_opengl_main.cpp
@@ -32,6 +32,7 @@
 #define new DEBUG_NEW
 
 #include <vector>
+#include <thread>
 
 #include <SDL.h>
 #include <GL/glew.h>
@@ -348,6 +349,10 @@ private: // OpenGL bookkeeping
 	int myargc;
 	char **myargv;
 	int currentConfig;
+
+	std::thread *tcpconn;
+	void connectTCP();
+	int sock;
 };
 
 const float CMainApplication::videospeed = 0.01f;
@@ -429,9 +434,85 @@ int CMainApplication::LoadConfigFile (const char *c)
 	if (solid)
 		MessageBoxA(0, "Only spheres implemented as atom glyphs in HTC Vive", "Atom Glyph", 0);
 
+	//change currentiso if needed
+	if (currentiso > ISOS || currentiso <0) 
+		currentiso = (currentiso + ISOS + 1) % (ISOS+1); //beware of (-1)
+	//add multiuser support
+	if (port!=-1 && tcpconn==nullptr) { //do not change servers as we change config file for now
+		tcpconn=new std::thread(&CMainApplication::connectTCP, this);
+	}
+
+
+
 	return r;
 }
 
+void CMainApplication::connectTCP() 
+{
+	//https://stackoverflow.com/questions/5444197/converting-host-to-ip-by-sockaddr-in-gethostname-etc
+	struct sockaddr_in serv_addr;
+	struct hostent *he;
+	if ( (he = gethostbyname(server) ) == nullptr ) {
+      return; /* error */
+	}
+	memset((char *) &serv_addr, 0, sizeof(serv_addr));
+	memcpy(&serv_addr.sin_addr, he->h_addr_list[0], he->h_length);
+	serv_addr.sin_family = AF_INET;
+	serv_addr.sin_port = htons(port);
+	sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
+	if ( connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr))) {
+      return; /* error */
+	}
+	//read state
+	int n;
+	int32_t tmp;
+	tmp=htonl (secret);
+	n = send(sock, (char*)&tmp , sizeof(tmp), 0);
+	if (n<sizeof(tmp))
+		return;
+	char what;
+	while (true) {
+		n=recv(sock, &what, sizeof(what), 0);
+		if (n<1)
+			return;
+		switch (what) {
+		case 't':
+			n=recv (sock, (char*)&tmp, sizeof(tmp), 0);
+			if (n<sizeof(tmp))
+				return;
+			currentset=ntohl(tmp)%TIMESTEPS;
+			break;
+		case 'i':
+			n=recv (sock, (char*)&tmp, sizeof(tmp), 0);
+			if (n<sizeof(tmp))
+				return;
+			currentiso=ntohl(tmp)%(ISOS+1);
+			break;
+		case 's':
+			char s;
+			n=recv (sock, &s, sizeof(s), 0);
+			if (n<sizeof(s))
+				return;
+			showAtoms=(bool)s;
+			break;
+		case 'n':
+			n=recv (sock, (char*)&tmp, sizeof(tmp), 0);
+			if (n<sizeof(tmp))
+				return;
+			//load config file
+			if (currentConfig!=ntohl(tmp)%myargc) {
+				currentConfig=ntohl(tmp)%myargc;
+				CleanScene();
+				LoadConfigFile(myargv[currentConfig]);
+				SetupScene();
+			}
+			break;
+		default:
+			eprintf ("Unknown state sent from server: %c\n", what);
+		}
+	}
+}
+
 //-----------------------------------------------------------------------------
 // Purpose: Constructor
 //-----------------------------------------------------------------------------
@@ -480,7 +561,7 @@ CMainApplication::CMainApplication(int argc, char *argv[])
 	, m_bShowCubes(true)
 	, currentset(0)
 	, elapsedtime(videospeed*float(SDL_GetTicks()))
-	, currentiso(ISOS)
+	, currentiso(-1) // (-> ISOS, but at this point ISOS is not yet initialized)
 	, firstdevice(-1)
 	, seconddevice(-1)
 	, m_iTexture(0)
@@ -500,6 +581,8 @@ CMainApplication::CMainApplication(int argc, char *argv[])
 	, myargc(argc)
 	, myargv(argv)
 	, currentConfig(1)
+	, tcpconn(0)
+	, sock(-1)
 {
 	LoadConfigFile(argv[currentConfig]);
 	for (int j=0;j<3;j++)
@@ -1006,6 +1089,22 @@ bool CMainApplication::HandleInput()
 						currentConfig++;
 						if (currentConfig>=myargc)
 							currentConfig=1;
+						if (sock>=0) {
+							char w='c';
+							int32_t tmp;
+							tmp=htonl(currentConfig);
+							int n;
+							n=send(sock, &w, sizeof(w), 0);
+							if (n<sizeof(w)) {
+								closesocket(sock);
+								sock=-1;
+							}
+							n=send(sock, (char*)&tmp, sizeof(tmp), 0);
+							if (n<sizeof(tmp)) {
+								closesocket(sock);
+								sock=-1;
+							}
+						}
 						CleanScene();
 						LoadConfigFile(myargv[currentConfig]);
 						SetupScene();
@@ -1492,6 +1591,8 @@ bool CMainApplication::SetupTexturemaps()
 	else
 		path=s.substr(0, l)+"\\"+NUMBERTEXTURE;
 	numbersTexture=LoadPNG(path.c_str(), nearest);
+	if (numbersTexture==0)
+		eprintf ("Error loading %s\n", path);
 	return ( m_iTexture != 0 && e==GL_NO_ERROR);
 }
 
@@ -1535,8 +1636,10 @@ void CMainApplication::CleanScene()
 		ISOS=0;
 	}
 	//atoms
-	::CleanAtoms(&m_unAtomVAO, &m_glAtomVertBuffer, &BondIndices);
-	::cleanAtoms(&numAtoms, TIMESTEPS, &atoms);
+	if (atoms) {
+		::CleanAtoms(&m_unAtomVAO, &m_glAtomVertBuffer, &BondIndices);
+		::cleanAtoms(&numAtoms, TIMESTEPS, &atoms);
+	}
 	//unit cell
 	::CleanUnitCell(&m_unUnitCellVAO, &m_glUnitCellVertBuffer, &m_glUnitCellIndexBuffer);
 	//marker
@@ -1739,49 +1842,39 @@ float GetTextureCoordinate (char c)
 	return u;
 }
 
+//if selected atom, display atom # and distance. 
+//Otherwise, display timestep in firstdevice and iso in seconddevice
 void CMainApplication::RenderControllerGlyph (const vr::Hmd_Eye nEye, const int controller)
 {
-	if (selectedAtom==-1)
-		return;
-	if (controller == seconddevice) {
-		Vector3 pos; 
-		PrepareControllerGlyph(nEye, controller, &pos);
-		pos /=scaling;
-		pos-=UserPosition;
-		pos=Vector3(pos[0], -pos[2], pos[1]);
-		
-		pos-=Vector3(atoms[currentset][selectedAtom*4+0], atoms[currentset][selectedAtom*4+1], atoms[currentset][selectedAtom*4+2]);
-
-		char dis [200];
-		sprintf (dis, "%0.2fa", pos.length());
-		int l=strlen (dis);
-		float *vert;
-		vert=new float[l*4*(4+3+2)];
-		for (int i=0;i<l;i++) {
-			float u=GetTextureCoordinate(dis[i]);
-			FillVerticesGlyph (vert, i, u);
-		} //for
-		short int *ind=FillIndicesGlyph(l);
-		RenderNumbersControllerGlyph (l, vert, ind, numbersTexture);
-		return;
-	} // if
-	
-	if (selectedAtom==-1)
-		return;
-
+	char string[200];
 	Vector3 pos; 
 	PrepareControllerGlyph(nEye, controller, &pos);
-
-	//display atom number
-	char atom [200];
-	sprintf (atom, "%d", selectedAtom+1);
-	//sprintf (atom, "%d %.2f %.2f %.2f", selectedAtom+1, atoms[currentset][selectedAtom*4+0], atoms[currentset][selectedAtom*4+1], atoms[currentset][selectedAtom*4+2]);
-	int l=strlen (atom);
-
+	if (controller == seconddevice) {
+		if (selectedAtom==-1) { //isos
+			sprintf (string, "%d", currentiso+1);
+		} else {
+			pos /=scaling;
+			pos-=UserPosition;
+			pos=Vector3(pos[0], -pos[2], pos[1]);
+		
+			pos-=Vector3(atoms[currentset][selectedAtom*4+0], atoms[currentset][selectedAtom*4+1], atoms[currentset][selectedAtom*4+2]);
+		
+			sprintf (string, "%0.2fa", pos.length());
+		} //if selectedatom
+	} else { // if controller == firstdevice
+		if (selectedAtom==-1) { //timestep
+			sprintf (string, "%d", currentset+1);
+		} else {
+			//display atom number
+			sprintf (string, "%d", selectedAtom+1);
+			//sprintf (atom, "%d %.2f %.2f %.2f", selectedAtom+1, atoms[currentset][selectedAtom*4+0], atoms[currentset][selectedAtom*4+1], atoms[currentset][selectedAtom*4+2]);
+		}
+	}	
+	int l=strlen (string);
 	float *vert;
 	vert=new float[l*4*(4+3+2)];
 	for (int i=0;i<l;i++) {
-		float u=GetTextureCoordinate(atom[i]);
+		float u=GetTextureCoordinate(string[i]);
 		FillVerticesGlyph (vert, i, u);
 	}
 
@@ -2814,7 +2907,7 @@ void CMainApplication::RenderScene(vr::Hmd_Eye nEye)
 					PaintGrid(nEye, i);
 				} //for all isos in descending order
 				if ((e = glGetError()) != GL_NO_ERROR)
-					dprintf("Gl error after paintgrid: %d, %s\n", e, gluErrorString(e));
+					dprintf("Gl error after paintgrid within RenderScene: %d, %s\n", e, gluErrorString(e));
 				if (numAtoms!=0) {
 					if (menubutton == Infobox && savetodisk)
 						RenderInfo(nEye);