The APU state is a good example. If an emulator isn't handling it at all, any games which use the APU interrupt or poll the status register won't work, thus we can assume that no save states lacking APU state will be using the APU. The default state should have everything silent and the frame interrupt disabled ($4017=$40).
As far as file format, about the simplest I can come up with that is extensible is the AIFF chunk style, where the file is a series of data blocks, each having a header with a fixed-length type tag and a fixed-length size, followed by size data bytes. This is easy to write and read without any extra state information. Any additional complexity in reading and writing has to give some useful benefit.
Here's something I just threw together as an example. The use of an end marker block allows nested groups of blocks, for example when you have multiple save states at the key frames in a movie file. Note how it also allows expansion of a given block, with older emulators reading a subset of the data, and newer emulators only reading as much data as the file provides.
Code:
void write_block( long type, long size, const void* in, FILE* out )
{
unsigned char b [8] = { type, type>>8, type>>16, type>>24,
size, size>>8, size>>16, size>>24 };
fwrite( b, 8, 1, out );
fwrite( in, size, 1, out );
}
void write_state( FILE* file )
{
write_block( 'NAPU', sizeof (apu_state), &apu_state, file );
write_block( 'NPPU', sizeof (ppu_state), &ppu_state, file );
/* etc... */
write_block( 'endb', 0, "", file );
}
void read_data( long size, void* out, FILE* in )
{
unsigned char b [4];
if ( fread( b, 4, 1, in ) )
{
long actual = b[3]<<24 | b[2]<<16 | b[1]<<8 | b[0];
fread( out, (size < actual ? size : actual), 1, in );
if ( actual > size )
fseek( in, actual - size, SEEK_CUR );
}
}
void read_state( FILE* file )
{
unsigned char b [4];
while ( fread( b, 4, 1, file ) > 0 )
{
switch ( b[3]<<24 | b[2]<<16 | b[1]<<8 | b[0] )
{
case 'NAPU':
read_data( sizeof apu_state, &apu_state, file );
break;
case 'NPPU':
read_data( sizeof ppu_state, &ppu_state, file );
break;
/* etc... */
case 'endb':
fseek( file, 4, SEEK_CUR ); // skip size
return;
}
}
}