kodi-game/kodi-game-scripting

Automatically determine if rewind is supported

fetzerch opened this issue · 2 comments

This ticket is just to document evaluated options for statically determining if a core supports rewind.

Rewind in libretro is implemented by the 3 API calls retro_serialize_size, retro_serialize and retro_unserialize. (https://github.com/libretro/RetroArch/blob/master/libretro-common/include/libretro.h#L2123). The frontend detects if rewind is supported by calling retro_serialize_size after a game has been loaded. Unfortunately there's no way to detect that without loading a game.

Some options:

Analyze the sources (with plain regex)

Quite naively, but an option would be check if the method implementation is return 0;. While this looks simple and we can get some fast results just with regular expressions, it get's complicated due to comments, #ifdefs, ....

$ find build -iname "libretro.c*" -exec sh -c "echo {}; pcregrep -M \"(?s)retro_serialize_size[^}]*}\" {}; echo ''" \;

build/build/handy/src/handy/libretro/libretro.cpp
size_t retro_serialize_size(void)
{
   char tempfilename[1024];
   if(!lynx)
      return 0;

   gettempfilename(tempfilename);
   return lynx->MemoryContextSave(tempfilename, NULL);
}

build/build/dosbox/src/dosbox/libretro/libretro.cpp
size_t retro_serialize_size (void) { return 0; }

build/build/bsnes-mercury-performance/src/bsnes-mercury-performance/target-libretro/libretro.cpp
size_t retro_serialize_size(void) {
  return SuperFamicom::system.serialize_size();
}

build/build/mgba/src/mgba/src/platform/libretro/libretro.c
size_t retro_serialize_size(void) {
	return sizeof(struct GBASerializedState);
}
	if (size != retro_serialize_size()) {
		return false;
	}
	if (size != retro_serialize_size()) {
		return false;
	}

build/build/gambatte/src/gambatte/libgambatte/libretro/libretro.cpp
size_t retro_serialize_size(void)
{
   return gb.stateSize();
}
   serialize_size = retro_serialize_size();

   if (size != serialize_size)
      return false;

   gb.saveState(data);
   return true;
}
   serialize_size = retro_serialize_size();

   if (size != serialize_size)
      return false;

   gb.loadState(data);
   return true;
}

build/build/bsnes-mercury-accuracy/src/bsnes-mercury-accuracy/target-libretro/libretro.cpp
size_t retro_serialize_size(void) {
  return SuperFamicom::system.serialize_size();
}

build/build/picodrive/src/picodrive/platform/libretro/libretro.c
size_t retro_serialize_size(void)
{
   struct savestate_state state = { 0, };

build/build/bnes/src/bnes/libretro/libretro.cpp
size_t retro_serialize_size() { return NES::system.serialize_size; }

build/build/gw/src/gw/src/libretro.c
size_t retro_serialize_size()
{
  return 0;
}

build/build/beetle-wswan/src/beetle-wswan/libretro.cpp
size_t retro_serialize_size(void)
{
   StateMem st;
   memset(&st, 0, sizeof(st));

   if (!MDFNSS_SaveSM(&st, 0, 0, NULL, NULL, NULL))
      return 0;

   free(st.data);
   return serialize_size = st.len;
}

build/build/hatari/src/hatari/libretro/libretro.c
size_t retro_serialize_size(void)
{
   return 0;
}

build/build/prboom/src/prboom/libretro/libretro.c.orig
size_t retro_serialize_size(void)
{
   return 0;
}

build/build/prboom/src/prboom/libretro/libretro.c
size_t retro_serialize_size(void)
{
   return 0;
}

build/build/bluemsx/src/bluemsx/libretro.c
size_t retro_serialize_size(void)
{
   return 0;
}

build/build/reicast/src/reicast/core/libretro/libretro.cpp
size_t retro_serialize_size (void)
{
   return 0; //TODO
}

build/build/yabause/src/yabause/libretro/libretro.c
size_t retro_serialize_size(void)
{
   void *buffer;
   size_t size;

   ScspMuteAudio(SCSP_MUTE_SYSTEM);
   YabSaveStateBuffer (&buffer, &size);
   ScspUnMuteAudio(SCSP_MUTE_SYSTEM);

   free(buffer);

   return size;
}

build/build/nestopia/src/nestopia/libretro/libretro.cpp
size_t retro_serialize_size(void)
{
   std::stringstream ss;
   if (machine->SaveState(ss, Api::Machine::NO_COMPRESSION))
      return 0;
   return ss.str().size();
}

build/build/virtualjaguar/src/virtualjaguar/libretro.c
size_t retro_serialize_size(void)
{
   return 0;
}

build/build/fmsx/src/fmsx/libretro.c
size_t retro_serialize_size(void)
{
   return 0x100000;
}

build/build/4do/src/4do/libretro.c
size_t retro_serialize_size(void)
{
   return _3do_SaveSize();
}

build/build/vbam/src/vbam/src/libretro/libretro.cpp
size_t retro_serialize_size(void)
{
   return serialize_size;
}

build/build/beetle-gba/src/beetle-gba/libretro.cpp
size_t retro_serialize_size(void)
{
   StateMem st;
   memset(&st, 0, sizeof(st));

   if (!MDFNSS_SaveSM(&st, 0, 0, NULL, NULL, NULL))
   {
      if (log_cb)
         log_cb(RETRO_LOG_WARN, "[mednafen]: Module gba doesn't support save states.\n");
      return 0;
   }

build/build/tgbdual/src/tgbdual/libretro/libretro.cpp
size_t retro_serialize_size(void)
{
	if (!(_serialize_size[0] + _serialize_size[1]))
   {
      for(int i = 0; i < 2; ++i)
      {
         if (g_gb[i])
            _serialize_size[i] = g_gb[i]->get_state_size();
      }
	if (size == retro_serialize_size())
   {
		uint8_t *ptr = (uint8_t*)data;

      for(int i = 0; i < 2; ++i)
      {
         if (g_gb[i])
         {
            g_gb[i]->save_state_mem(ptr);
            ptr += _serialize_size[i];
         }
	if (size == retro_serialize_size())
   {
		uint8_t *ptr = (uint8_t*)data;

      for(int i = 0; i < 2; ++i)
      {
         if (g_gb[i])
         {
            g_gb[i]->restore_state_mem(ptr);
            ptr += _serialize_size[i];
         }

build/build/fbalpha2012/src/fbalpha2012/svn-current/trunk/src/burner/libretro/libretro.cpp
size_t retro_serialize_size()
{
   if (state_size)
      return state_size;

   BurnAcb = burn_dummy_state_cb;
   state_size = 0;
   BurnAreaScan(ACB_VOLATILE | ACB_WRITE, 0);
   return state_size;
}

build/build/beetle-lynx/src/beetle-lynx/libretro.cpp
size_t retro_serialize_size(void)
{
   StateMem st;
   memset(&st, 0, sizeof(st));

   if (!MDFNSS_SaveSM(&st, 0, 0, NULL, NULL, NULL))
   {
      if (log_cb)
         log_cb(RETRO_LOG_WARN, "[mednafen]: Module lynx doesn't support save states.\n");
      return 0;
   }

build/build/tyrquake/src/tyrquake/common/libretro.c
size_t retro_serialize_size(void)
{
   return 0;
}

build/build/meteor/src/meteor/libretro/libretro.cpp
size_t retro_serialize_size(void)
{
	first_run_load();
	std::ostringstream stream;
	AMeteor::SaveState(stream);
	unsigned serialize_size = stream.str().size();

	// We want constant sized streams, and thus we have to pad.
	uint32_t current_size = *(uint32_t*)(AMeteor::CartMemData + AMeteor::CartMem::MAX_SIZE);

	// Battery is not installed (yet!),
	// accomodate for the largest possible battery size if it does get installed after this check.
	// Flash 128kB + 4 byte state variable.
	if (!current_size)
		serialize_size += AMeteor::CartMem::MAX_SIZE + sizeof(uint32_t);

	return serialize_size;
}

build/build/pcem/src/pcem/src/libretro.c
size_t retro_serialize_size(void)
{
   return 0;
}

build/build/snes9x2010/src/snes9x2010/libretro/libretro.c
size_t retro_serialize_size (void)
{
   uint8_t *tmpbuf;

   tmpbuf = (uint8_t*)malloc(5000000);
   memstream_set_buffer(tmpbuf, 5000000);
   S9xFreezeGame("");
   free(tmpbuf);
   return memstream_get_last_size();
}

build/build/nx/src/nx/nxengine/libretro/libretro.cpp
size_t retro_serialize_size(void)
{
   return 0;
}

build/build/desmume/src/desmume/libretro/libretro.cpp
size_t retro_serialize_size (void)
{
    // HACK: Usually around 10 MB but can vary frame to frame!
    return 1024 * 1024 * 12;
}

build/build/fuse/src/fuse/src/libretro.c
size_t retro_serialize_size(void)
{
   fuse_emulation_pause();
   snapshot_write("dummy.szx"); // filename is only used to get the snapshot type
   fuse_emulation_unpause();
   return snapshot_size;
}

build/build/vecx/src/vecx/libretro/libretro.c
size_t retro_serialize_size(void)
{
	return vecx_statesz();
}

build/build/2048/src/2048/libretro.c
size_t retro_serialize_size(void)
{
   return game_data_size();
}

build/build/fceumm/src/fceumm/src/drivers/libretro/libretro.c
size_t retro_serialize_size(void)
{
   if (serialize_size == 0)
   {
      /* Something arbitrarily big.*/
      uint8_t *buffer = (uint8_t*)malloc(1000000);
      memstream_set_buffer(buffer, 1000000);

      FCEUSS_Save_Mem();
      serialize_size = memstream_get_last_size();
      free(buffer);
   }
   if (size != retro_serialize_size())
      return false;

   memstream_set_buffer((uint8_t*)data, size);
   FCEUSS_Save_Mem();
   return true;
}
   if (size != retro_serialize_size())
      return false;

   memstream_set_buffer((uint8_t*)data, size);
   FCEUSS_Load_Mem();
   return true;
}

build/build/o2em/src/o2em/libretro.c
size_t retro_serialize_size(void) 
{ 
	//return STATE_SIZE;
	return 0;
}

build/build/prosystem/src/prosystem/core/libretro.c
size_t retro_serialize_size(void) 
{ 
   return 32829;
}

build/build/dinothawr/src/dinothawr/libretro.cpp
size_t retro_serialize_size(void)
{
   return 0;
}

build/build/quicknes/src/quicknes/libretro/libretro.cpp
size_t retro_serialize_size(void)
{
   Mem_Writer writer;
   if (emu->save_state(writer))
      return 0;

   return writer.size();
}

build/build/beetle-psx/src/beetle-psx/libretro.cpp
size_t retro_serialize_size(void)
{
   if (enable_variable_serialization_size)
   {
      StateMem st;
      memset(&st, 0, sizeof(st));

      if (!MDFNSS_SaveSM(&st, 0, 0, NULL, NULL, NULL))
      {
         return 0;
      }

build/build/beetle-vb/src/beetle-vb/libretro.cpp
size_t retro_serialize_size(void)
{
   StateMem st;
   MDFNGI *curgame = (MDFNGI*)game;
   memset(&st, 0, sizeof(st));

   if (!MDFNSS_SaveSM(&st, 0, 0, NULL, NULL, NULL))
   {
      if (log_cb)
         log_cb(RETRO_LOG_WARN, "[mednafen]: Module vb doesn't support save states.\n");
      return 0;
   }

build/build/snes9x2002/src/snes9x2002/libretro/libretro.c
size_t retro_serialize_size (void)
{
   uint8_t *tmpbuf;

   tmpbuf = (uint8_t*)malloc(5000000);
   memstream_set_buffer(tmpbuf, 5000000);
   S9xFreezeGame("");
   free(tmpbuf);
   return memstream_get_last_size();
}

build/build/vba-next/src/vba-next/libretro/libretro.cpp
size_t retro_serialize_size(void)
{
   return serialize_size;
}

build/build/pcsx-rearmed/src/pcsx-rearmed/frontend/libretro.c
size_t retro_serialize_size(void)
{
	// it's currently 4380651-4397047 bytes,
	// but have some reserved for future
	return 0x440000;
}
	size_t r_size = retro_serialize_size();
	if (fp == NULL)
		return;

	if (fp->pos > r_size)
		SysPrintf("ERROR: save buffer overflow detected\n");
	else if (fp->is_write && fp->pos < r_size)
		// make sure we don't save trash in leftover space
		memset(fp->buf + fp->pos, 0, r_size - fp->pos);
	free(fp);
}

build/build/beetle-pce-fast/src/beetle-pce-fast/libretro.cpp
size_t retro_serialize_size(void)
{
   StateMem st;
   memset(&st, 0, sizeof(st));

   if (!MDFNSS_SaveSM(&st, 0, 0, NULL, NULL, NULL))
      return 0;

   free(st.data);
   return serialize_size = st.len;
}

build/build/genplus/src/genplus/libretro/libretro.c.orig
size_t retro_serialize_size(void) { return STATE_SIZE; }

build/build/genplus/src/genplus/libretro/libretro.c
size_t retro_serialize_size(void) { return STATE_SIZE; }

build/build/beetle-ngp/src/beetle-ngp/libretro.cpp
size_t retro_serialize_size(void)
{
   StateMem st;
   memset(&st, 0, sizeof(st));

   if (!MDFNSS_SaveSM(&st, 0, 0, NULL, NULL, NULL))
      return 0;

   free(st.data);
   return serialize_size = st.len;
}

build/build/stella/src/stella/libretro.cxx
size_t retro_serialize_size(void) 
{
   //return STATE_SIZE;
   return 0;
}

build/build/mupen64plus/src/mupen64plus/libretro/libretro.c
size_t retro_serialize_size (void)
{
    return 16788288 + 1024; /* < 16MB and some change... ouch */
}

build/build/snes9x/src/snes9x/libretro/libretro.cpp
size_t retro_serialize_size()
{
   return rom_loaded ? S9xFreezeSize() : 0;
}

build/build/lutro/src/lutro/deps/retroluxury/test/libretro.c
size_t retro_serialize_size( void )
{
  return 0;
}

build/build/lutro/src/lutro/libretro.c
size_t retro_serialize_size(void)
{
   return 0;
}

build/build/bsnes-mercury-balanced/src/bsnes-mercury-balanced/target-libretro/libretro.cpp
size_t retro_serialize_size(void) {
  return SuperFamicom::system.serialize_size();
}

Analyze the binary

Another naive approach could be to analyze the binary and do estimations on the function size of retro_serialize_size (those that support rewind have to calculate something and the function might be bigger). This turned out to be unreliable because some cores just call another function (4do) or have a static size.

$ find install -name "game.libretro.*.so" -exec sh -c "echo {}; nm --print-size --size-sort --radix=d {} | grep retr
o_serialize_size" \;

install/game.libretro.beetle-gba/game.libretro.beetle-gba.so
0000000000886956 0000000000000197 T retro_serialize_size
install/game.libretro.prosystem/game.libretro.prosystem.so
0000000000005638 0000000000000011 T retro_serialize_size
install/game.libretro.beetle-pce-fast/game.libretro.beetle-pce-fast.so
0000000000277462 0000000000000153 T retro_serialize_size
install/game.libretro.dinothawr/game.libretro.dinothawr.so
0000000000270589 0000000000000011 T retro_serialize_size
install/game.libretro.nestopia/game.libretro.nestopia.so
0000000001135389 0000000000000268 T retro_serialize_size
install/game.libretro.4do/game.libretro.4do.so
0000000000008426 0000000000000013 T retro_serialize_size
install/game.libretro.picodrive/game.libretro.picodrive.so
0000000000081084 0000000000000145 T retro_serialize_size

Analyze the sources using clang

Might be an interesting exercise to write a small clang checker for this this:

What if we had sample games that we could load via ctypes and then query serialize size? This might be useful for some basic unit testing. Reicast, for instance, crashes if you call retro_init() and then retro_deinit() without calling retro_load_game() in between.

It's also something we could add to the core .info files. They aren't against adding metadata in there.