Advertisement

Binary differences between CPU types and OSs (BIG headache)

Started by July 11, 2004 08:15 PM
11 comments, last by hplus0603 20 years, 7 months ago
Im currently writing a 3D-engine on an AMD proccesor(x86) and Win2k. However, when (or if :/ ) i get nvidia drivers to work on my Linux system i want to be able to port my engine to Linux, and maybe another type of CPU (PowerPC perhaps). Now, if your engine is singelplayer only and users with different CPU types (does the OS matter too?) don't exhange maps, its a piece of cake. HOWEVER, i want to implement multiplayer at a later stage without having to rewrite the entire engine. What stuff do i need to think about? I know that i need to convert the byte ordering if two computers with different byte ordering are communicating over a network, but is there anything else i need to have in mind? Ive looked for a tutorial on this but havent found any :( . (the same goes for transfering a file from say a Linux Intel P4 to a Mac PPC, what stuff do i need to have in mind and are there any good tutorials on this?). Cheers // BBB (yes im a swed so my english isn't perfect, hope its understandable hehe :) )
I think you have got the wrong idea about it, it's actually not such a big problem as you think. All you have to worry about is endian swapping and writing portable code. By portable code, I mean standard compliant and crossplatform. For instance, DirectX is windows only but openGL is crossplatform. Avoid compiler specific language extensions and #pragmas. All I/O should be endian swapped to the endian of your choice before writing it to file. That way the files will be in a consistant format that can be passed between machines of different endians. And that's all there's to it really, so not such a big headache after all :)
Advertisement
If you are using Sockets API look up htonl and ntohl.
[size="2"]Don't talk about writing games, don't write design docs, don't spend your time on web boards. Sit in your house write 20 games when you complete them you will either want to do it the rest of your life or not * Andre Lamothe
If your simulation uses floating point arithmetic, and you take advantage of the fact the the different machines can calculate the same results from the same inputs, then there's another problem: FPU results are not bit-exact between CPUs. Here are some things to watch out for:

Different CPU families implement the IEEE floating point standard slightly differently (but within the limits of the standard).

Different CPUs within the same family (say, x86) may use different internal width (say, 80 bits for Intel, and 64 bits for AMD).

Different compilers generate different instruction sequences for the same code for the same CPU.

Various DLLs you call may choose to change the rounding mode or precision bits of the FP control word of the CPU. DirectX is notorious for doing this, but others can, too. Throwing exceptions or catching signals will also change these bits.

You can combat these by making sure you test and possibly re-set the precision bits the way you want them at the start of each simulation step. You can also round all simulation quantities to a known precision (narrower than the CPU register precision) each step, so as to avoid large drift (on the theory that small differences will get rounded the same). You should still make sure to send "baseline" data to all clients every so often, to make sure they can't drift too far out of sync.

Or you can choose to write all your simulation code in fixed point math.
enum Bool { True, False, FileNotFound };
If you have an authoritative node ("server") in your topology, and send out authoritative state about objects every so often, you most often don't need to worry about these things. The most important one is to test-and-set-if-necessary the rounding and precision mode bits of the FPU control word, because libraries/exceptions/signals WILL change them, and your physical simulation will possibly explode because of it.

To round to a given precision, do something like:

double round_value( double value, double precision, double inversePrecision ) {  double d = floor( value * inversePrecision + 0.5 );  return d * precision;}


I made the function take precision and inversePrecision as arguments, to let you pre-calculate the values once and for all. Typically, you'll have class instances that round quantities for your as appropriate, configured with rounding policies that you determine (which end up supplying the values of precision and inversePrecision).

Your local C++ language reference manual or compiler man page should tell you what the type "double" really means.
enum Bool { True, False, FileNotFound };
If you output your structures as binary in one big hit (like fwrite(&myobj, sizeof(myobj), 1, fp)) then stack alignment is something to watch out for. Just because you think some struct should pack down to x bytes doesn't mean it does - it might have padding inserted by the compiler. Writing structs as straight binary in this manner is a bad idea for this and other reasons.
My stuff.Shameless promotion: FreePop: The GPL god-sim.
Advertisement
Quote:
So you are telling me that the only
thing i have to do is add a bool big_endian; to my entity class
and convert the endianess of all variabels in file/ socket data
if the endianess of the file/ socket data is not the same
as the current machine?


Some people do that. Basically the sender never converts anything. Others go the route putting a standard format on the wire and letting people convert if thier native format is different than the standard format. That's actually the more common. Most of the base Internet protocols put big-endian on the wire all the time and clients use ntoX and htoX to convert. One disadvantage of this route is that you have to convert at both ends if the native formats are different than the wire format.

For my stuff I put little-endian on the wire since the vast majority of machines out there are little-endian.
-Mike
It is incredibly unsafe to rely on struct packing being the same for a binary protocol. Worse for saved files, seeing as these are persisteny.

The same compiler may vary this just by compiler settings, so watch out.

Also I guess these days your code should all be 64-bit aware.

Mark
I think some people here are making it sound worse than it is again. Maybe this is why you had the wrong impression in the first place, so many people with misinformation.

There is nothing to worry about with floats. Yes, the calculation results may not be "bit-exact" but they are in the same format and thus can be read by eachother. Small inaccuracies do not matter. Floating point is not exact anyways, it can't be and it's not meant to be. If you are concerned about a 1e-10 error then you shouldn't be using floats in the first place.
Again, floats are fine so long as you byte-swap them, just like everything else.

About packing alignment: Nothing to worry about here either, just set the struct alignment in your compiler options the same on both platforms and you'll be fine. Of course, you should already be using structs that are power of 2 sizes anyways, it's good practice.

BBB, instead of using bool big_endian, opt for a more efficient compile-time solution. Do some preprocessor stuff to determine what endian machine you're on, then define some swapping macros/functions.

for instance
#ifdef LITTLE_ENDIAN#define SWAP16(x) _byteswap_ushort(x)#define SWAP32(x) _byteswap_ulong(x)#else#define SWAP16#define SWAP32#endif

for my personal byteswapping routines I do something like this:
(This a made up example, and may contain typos)
typedef struct{ short magic, version; long identifier; long texture_reference; long vertex_count; long triangle_count; vertex * verticies; triangle * triangles; byte * sufrace_indicies; char name[32];} geometry_definition;bs_data geometry_definition_byteswap_data[] ={ _2byte, 2, _4byte, 7, _1byte, 32,};geometry_definition * read_geometry_file(FILERECORD * f){ geometry_definition * geom = malloc(f->size);// implementation left as an exercise for the reader byteswap_fread(geom, geometry_definition_byteswap_data, f->stream);//fix up pointers and byteswap here return geom;}


The way I implement byteswap_fread is to first define a byteswap_memcpy and use that in my byteswap_fread, byteswap_fwrite, byteswap_netsend, and related functions.
Quote:
Melekor writes:
Small inaccuracies do not matter. Floating point is not exact anyways, it can't be and it's not meant to be.


I'm sad to say it, but this is just lies and superstition.


Floating point has very well defined precision; as long as you stay within the precision, it is exact. For example, this code is GUARANTEED to always print "match":

float a = 2.0f;float b = 1.0f;float c = 4.0f;float d = 0.5f;float e = a + b + a*d;if( c == e ) {  puts( "match" );}



Saying that small differences between nodes don't matter means that you have to keep sending your game state over and over again from the server, because if you can't make the computations match on the client and the server, the client cannot display any derived decisions to the user; the server has to inform the client about each decision explicitly. For example, if a player runs very close to the edge of a lava chasm, one one machine, the player might fall down and die a horrible screaming death; on another, he might steer clear and move on to frag you. Displaying the screaming death to some user, if that's not what actually happened, should not happen, at least not as long as there is no packet loss.

If you need to do a lot of things on a narrow network pipe, being able to depend on the fact that all clients will calculate exactly the same results from some input, as the server will given the same input, is very important. Luckily, if you follow the suggestions I gave in this thread above, you can generally make this guarantee, and reap the substantial network bandwidth savings (which, in turn, translate to more objects possible in a networked game on a given network connection).
enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement