Skip to content

Commit

Permalink
media: Manager: Introduce a mediaCache
Browse files Browse the repository at this point in the history
It's not often that J2ME games stutter in FreeJ2ME, but when they
do, it's almost always because they're loading media streams during
gameplay, and it's especially noticeable if said streams are MIDI
while running under OpenJDK (the older it is, the worse it gets).

Sonic Jump (Siemens 132x176 version) is one such example, every
SFX is loaded during runtime, and the game doesn't store any,
so as soon as a different SFX from the last comes up, the game
stutters. This turns the old "MaxMidiStreams" setting into more
of a "MediaCacheSize", where it limits the amount of players of
any kind that can be kept in memory at the same time without reloading
any. This is achieved through a hashmap that evaluates an incoming
stream's MD5 against the ones already cached, and if it exists,
out media Manager simply returns the cached player instead of
creating a new one, vastly improving performance on apps that do
constant player allocations.

As a bonus, this actually helps DOOM II RPG, as its pool of unique
SFX and BGM is rather small (something around 16, despite all being
MIDI), which means we can probably increase the default value for
this cache without worrying about memory, as duplicate streams won't
be a thing anymore since they'll evaluate to the same MD5... unless
we start dealing with a game that generates those on the fly, of course.

But that's something for when Tone playback is implemented, and
it might not even need to go through here anyway.
  • Loading branch information
AShiningRay committed Oct 14, 2024
1 parent 845d06e commit 7ad3824
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 125 deletions.
101 changes: 69 additions & 32 deletions src/javax/microedition/media/Manager.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,32 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;

import org.recompile.mobile.Mobile;
import org.recompile.mobile.PlatformPlayer;

public final class Manager
{
public static final String TONE_DEVICE_LOCATOR = "device://tone";
public static Player midiPlayers[] = new Player[32]; /* Default max amount of players in FreeJ2ME's config */
public static byte midiPlayersIndex = 0;

/* Default max amount of players in FreeJ2ME's config */
public static Player mediaPlayers[] = new Player[32];
public static byte mediaPlayersIndex = 0;

/* Midi Caching for better performance on certain VMs like OpenJDK 8 with jars that constantly load a similar set of streams. */
private static Map<String, Byte> mediaCache = new HashMap<>();

public static boolean dumpAudioStreams = false;
public static short audioDumpIndex = 0;

public static Player createPlayer(InputStream stream, String type) throws IOException, MediaException
{
stream.mark(1024);
String streamMD5 = generateMD5Hash(stream, 1024);
stream.reset();

if(dumpAudioStreams)
{
// Copy the stream contents into a temporary stream to be saved as file
Expand All @@ -58,44 +70,49 @@ public static Player createPlayer(InputStream stream, String type) throws IOExce
if (!dumpFile.isDirectory()) { dumpFile.mkdirs(); }

if(type.equalsIgnoreCase("audio/mid") || type.equalsIgnoreCase("audio/midi") || type.equalsIgnoreCase("sp-midi") || type.equalsIgnoreCase("audio/spmidi"))
{ dumpFile = new File(dumpPath + "Stream" + Short.toString(audioDumpIndex) + ".mid");}
else if(type.equalsIgnoreCase("audio/x-wav") || type.equalsIgnoreCase("audio/wav")) { dumpFile = new File(dumpPath + "Stream" + Short.toString(audioDumpIndex) + ".wav");}
else if(type.equalsIgnoreCase("audio/mpeg") || type.equalsIgnoreCase("audio/mp3")) { dumpFile = new File(dumpPath + "Stream" + Short.toString(audioDumpIndex) + ".mp3");}
{ dumpFile = new File(dumpPath + "Stream_" + streamMD5 + ".mid");}
else if(type.equalsIgnoreCase("audio/x-wav") || type.equalsIgnoreCase("audio/wav")) { dumpFile = new File(dumpPath + "Stream_" + streamMD5 + ".wav");}
else if(type.equalsIgnoreCase("audio/mpeg") || type.equalsIgnoreCase("audio/mp3")) { dumpFile = new File(dumpPath + "Stream_" + streamMD5 + ".mp3");}

outStream = new FileOutputStream(dumpFile);

streamCopy.writeTo(outStream);

audioDumpIndex++;
}

//System.out.println("Create Player Stream "+type);
if(type.equalsIgnoreCase("audio/mid") || type.equalsIgnoreCase("audio/midi") || type.equalsIgnoreCase("sp-midi") || type.equalsIgnoreCase("audio/spmidi"))
/* If we currently have this stream's player cached, return it instantly to avoid creating a new player and its overhead */
if (mediaCache.containsKey(streamMD5)) { return mediaPlayers[mediaCache.get(streamMD5)]; }

// Otherwise, let's create and cache a new one.

// If the index is out of bounds, we reached the end of our cache, go back to the start to find a position to free
if(mediaPlayersIndex >= mediaPlayers.length) { mediaPlayersIndex = 0; }

// Run through the entire cache index to find a suitable position to slot the new player in.
for(; mediaPlayersIndex < mediaPlayers.length; mediaPlayersIndex++)
{
if(midiPlayersIndex >= midiPlayers.length) { midiPlayersIndex = 0; }
for(; midiPlayersIndex < midiPlayers.length; midiPlayersIndex++)
if(mediaPlayers[mediaPlayersIndex] == null) { break; } /* A null position means we can use it right away */

/* Otherwise, we prefer deallocating a position if it is not playing (running). */
else if(mediaPlayers[mediaPlayersIndex] != null && mediaPlayers[mediaPlayersIndex].getState() == Player.PREFETCHED)
{
mediaPlayers[mediaPlayersIndex].deallocate();
mediaCache.values().remove(mediaPlayersIndex);
break;
}
/* If we ever reach this one, it's because all the other slots are used, and are playing. Deallocate the last cache position as a last resort. */
else if(mediaPlayersIndex == mediaPlayers.length-1)
{
if(midiPlayers[midiPlayersIndex] == null) { break; } /* A null position means we can use it right away */
/* Otherwise, we only deallocate a position if it is not playing (running). */
else if(midiPlayers[midiPlayersIndex] != null && midiPlayers[midiPlayersIndex].getState() == Player.PREFETCHED)
{
midiPlayers[midiPlayersIndex].deallocate();
break;
}
/* If we ever reach this one, it's because all the other slots are used, and are playing */
else if(midiPlayersIndex == midiPlayers.length-1)
{
midiPlayers[midiPlayersIndex].deallocate();
break;
}
mediaPlayers[mediaPlayersIndex].deallocate();
mediaCache.values().remove(mediaPlayersIndex);
break;
}
midiPlayers[midiPlayersIndex] = new PlatformPlayer(stream, type);
return midiPlayers[midiPlayersIndex++];
}
else
{
return new PlatformPlayer(stream, type);
}

mediaPlayers[mediaPlayersIndex] = new PlatformPlayer(stream, type);
mediaCache.put(streamMD5, mediaPlayersIndex);
mediaPlayersIndex++;

return mediaPlayers[mediaCache.get(streamMD5)];
}

public static Player createPlayer(String locator) throws MediaException
Expand Down Expand Up @@ -124,6 +141,26 @@ public static void playTone(int note, int duration, int volume)

public static void updatePlayerNum(byte num)
{
midiPlayers = new Player[num];
mediaPlayers = new Player[num];
}

private static String generateMD5Hash(InputStream stream, int byteCount)
{
try
{
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] data = new byte[byteCount];
int bytesRead = stream.read(data, 0, byteCount);

if (bytesRead != -1) { md.update(data, 0, bytesRead); }

// Convert MD5 hash to hex string
StringBuilder md5Sum = new StringBuilder();
for (byte b : md.digest()) { md5Sum.append(String.format("%02x", b)); }

return md5Sum.toString();
} catch (Exception e) { System.out.println("Failed to generate stream MD5:" + e.getMessage()); }

return null;
}
}
31 changes: 15 additions & 16 deletions src/libretro/freej2me_libretro.c
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ int phoneType; /* 0=standard, 1=nokia, 2=siemens, 3=motorola, 4=sonyEricsson */
int gameFPS; /* Auto(0), 60, 30, 15 */
int soundEnabled; /* also acts as a boolean */
int customMidi; /* Also acts as a boolean */
int maxMidiPlayers; /* Maximum amount of MIDI Players allowed on FreeJ2ME at any given time */
int midiCacheSize; /* Maximum amount of MIDI Players allowed on FreeJ2ME at any given time */
int dumpAudioStreams;
/* Variables used to manage the pointer speed when controlled from an analog stick */
int pointerXSpeed = 8;
Expand Down Expand Up @@ -356,19 +356,18 @@ static void check_variables(bool first_time_startup)
else if (!strcmp(var.value, "on")) { customMidi = 1; }
}

var.key = "freej2me_maxmidiplayers";
var.key = "freej2me_mediacachesize";
if (Environ(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
{
if (!strcmp(var.value, "32")) { maxMidiPlayers = 32; }
else if (!strcmp(var.value, "1")) { maxMidiPlayers = 1; }
else if (!strcmp(var.value, "2")) { maxMidiPlayers = 2; }
else if (!strcmp(var.value, "4")) { maxMidiPlayers = 4; }
else if (!strcmp(var.value, "8")) { maxMidiPlayers = 8; }
else if (!strcmp(var.value, "16")) { maxMidiPlayers = 16; }
else if (!strcmp(var.value, "32")) { maxMidiPlayers = 32; }
else if (!strcmp(var.value, "48")) { maxMidiPlayers = 48; }
else if (!strcmp(var.value, "64")) { maxMidiPlayers = 64; }
else if (!strcmp(var.value, "96")) { maxMidiPlayers = 96; }
if (!strcmp(var.value, "48")) { midiCacheSize = 48; }
else if (!strcmp(var.value, "1")) { midiCacheSize = 1; }
else if (!strcmp(var.value, "2")) { midiCacheSize = 2; }
else if (!strcmp(var.value, "4")) { midiCacheSize = 4; }
else if (!strcmp(var.value, "8")) { midiCacheSize = 8; }
else if (!strcmp(var.value, "16")) { midiCacheSize = 16; }
else if (!strcmp(var.value, "32")) { midiCacheSize = 32; }
else if (!strcmp(var.value, "64")) { midiCacheSize = 64; }
else if (!strcmp(var.value, "96")) { midiCacheSize = 96; }
}

var.key = "freej2me_dumpaudiostreams";
Expand Down Expand Up @@ -458,7 +457,7 @@ static void check_variables(bool first_time_startup)
/* Prepare a string to pass those core options to the Java app */
options_update = malloc(sizeof(char) * PIPE_MAX_LEN);

snprintf(options_update, PIPE_MAX_LEN, "FJ2ME_LR_OPTS:|%lux%lu|%d|%d|%d|%d|%d|%d|%d|%d", screenRes[0], screenRes[1], halveCanvasRes, rotateScreen, phoneType, gameFPS, soundEnabled, customMidi, maxMidiPlayers, dumpAudioStreams);
snprintf(options_update, PIPE_MAX_LEN, "FJ2ME_LR_OPTS:|%lux%lu|%d|%d|%d|%d|%d|%d|%d|%d", screenRes[0], screenRes[1], halveCanvasRes, rotateScreen, phoneType, gameFPS, soundEnabled, customMidi, midiCacheSize, dumpAudioStreams);
optstrlen = strlen(options_update);

/* 0xD = 13, which is the special case where the java app will receive the updated configs */
Expand Down Expand Up @@ -517,7 +516,7 @@ void retro_init(void)
*/
check_variables(true);

char resArg[2][4], halveCanvas[2], rotateArg[2], phoneArg[2], fpsArg[3], soundArg[2], midiArg[2], maxMidiArg[3], dumpAudioArg[2];
char resArg[2][4], halveCanvas[2], rotateArg[2], phoneArg[2], fpsArg[3], soundArg[2], midiArg[2], mediaCacheArg[3], dumpAudioArg[2];
sprintf(resArg[0], "%lu", screenRes[0]); /* Libretro config Width */
sprintf(resArg[1], "%lu", screenRes[1]); /* Libretro config Height */
sprintf(halveCanvas, "%d", halveCanvasRes);
Expand All @@ -526,7 +525,7 @@ void retro_init(void)
sprintf(fpsArg, "%d", gameFPS);
sprintf(soundArg, "%d", soundEnabled);
sprintf(midiArg, "%d", customMidi);
sprintf(maxMidiArg,"%d", maxMidiPlayers);
sprintf(mediaCacheArg,"%d", midiCacheSize);
sprintf(dumpAudioArg, "%d", dumpAudioStreams);

/* start java process */
Expand All @@ -552,7 +551,7 @@ void retro_init(void)
params[8] = strdup(fpsArg);
params[9] = strdup(soundArg);
params[10] = strdup(midiArg);
params[11] = strdup(maxMidiArg);
params[11] = strdup(mediaCacheArg);
params[12] = strdup(dumpAudioArg);
params[13] = NULL; // Null-terminate the array

Expand Down
25 changes: 12 additions & 13 deletions src/libretro/freej2me_libretro.h
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,11 @@ struct retro_core_option_v2_definition core_options[] =
"Default"
},
{
"freej2me_maxmidiplayers",
"Virtual Phone Settings > Max MIDI Players",
"Max MIDI Players",
"Sets the maximum amount of MIDI files that a given J2ME app can load up and play at any given time. Lower values may reduce memory usage and improve the VM's performance (especially on ID Software games like DOOM II RPG where setting this to 1 vastly reduces memory usage compared to the default), but some games might need bigger values.",
"Sets the maximum amount of MIDI files that a given J2ME app can load up and play at any given time. Lower values may reduce memory usage and improve the VM's performance (especially on ID Software games like DOOM II RPG where setting this to 1 vastly reduces memory usage compared to the default), but some games might need bigger values.",
"freej2me_mediacachesize",
"Virtual Phone Settings > Media Cache Size",
"Media Cache Size",
"FreeJ2ME uses a media caching system to improve performance on certain VMs like OpenJDK 8 with apps that constantly allocate a similar set of media streams instead of keeping them allocated all the time (often to save precious memory). Most modern platforms should be able to hold the default of 48 unique streams in memory at once, which is also more than most J2ME apps would realistically load on real hardware. Feel free to adjust if your platform is starved for memory and/or you know the jar you're running only really uses a few slots of media but cycles the data it loads there.",
"FreeJ2ME uses a media caching system to improve performance on certain VMs like OpenJDK 8 with apps that constantly allocate a similar set of media streams instead of keeping them allocated all the time (often to save precious memory). Most modern platforms should be able to hold the default of 48 unique streams in memory at once, which is also more than most J2ME apps would realistically load on real hardware. Feel free to adjust if your platform is starved for memory and/or you know the jar you're running only really uses a few slots of media but cycles the data it loads there.",
"vphone_settings",
{
{ "1", "1" },
Expand All @@ -216,7 +216,7 @@ struct retro_core_option_v2_definition core_options[] =
{ "96", "96" },
{ NULL, NULL },
},
"32"
"48"
},
{
"freej2me_dumpaudiostreams",
Expand Down Expand Up @@ -461,10 +461,9 @@ struct retro_core_option_definition core_options_v1 [] =
"Default"
},
{
"freej2me_maxmidiplayers",
"Max MIDI Players",
"Sets the maximum amount of MIDI files that a given J2ME app can load up and play at any given time. Lower values may reduce memory usage and improve the VM's performance (especially on ID Software games like DOOM II RPG where setting this to 1 vastly reduces memory usage compared to the default), but some games might need bigger values.",
{
"freej2me_mediacachesize",
"Media Cache Size",
"FreeJ2ME uses a media caching system to improve performance on certain VMs like OpenJDK 8 with apps that constantly allocate a similar set of media streams instead of keeping them allocated all the time (often to save precious memory). Most modern platforms should be able to hold the default of 48 unique streams in memory at once, which is also more than most J2ME apps would realistically load on real hardware. Feel free to adjust if your platform is starved for memory and/or you know the jar you're running only really uses a few slots of media but cycles the data it loads there.", {
{ "1", "1" },
{ "2", "2" },
{ "4", "4" },
Expand All @@ -476,7 +475,7 @@ struct retro_core_option_definition core_options_v1 [] =
{ "96", "96" },
{ NULL, NULL },
},
"32"
"48"
},
{
"freej2me_dumpaudiostreams",
Expand Down Expand Up @@ -616,8 +615,8 @@ static const struct retro_variable vars[] =
"MIDI Soundfont; off|on"
},
{ /* Max MIDI Players */
"freej2me_maxmidiplayers",
"Max MIDI Players: 32|1|2|4|8|16|48|64|96"
"freej2me_mediacachesize",
"Media Cache Size: 48|1|2|4|8|16|32|64|96"
},
{ /* Dump Audio Streams */
"freej2me_dumpaudiostreams",
Expand Down
Loading

0 comments on commit 7ad3824

Please sign in to comment.