trekawek/coffee-gb

Framerate not limited

Closed this issue · 6 comments

Hi,

I'm writing a port to make this emulator playable in minecraft. I'm noticing an issue where the FPS is not 'limited' resulting my game into running at superspeed. Is there any frame limiter built in? I might be looking over it.

Just for more information, I changed the SwingController to take in input from minecraft. I also changed the SwingDisplay a bit to prepare a frame that can be shown in minecraft but nothing too spectaculair otherwise..

Sorry for the spamming, found the underlying issue. Without a sound controller the emulator runs without a frame limit

Hi @InstantlyMoist,

Yes, as you noticed, the SoundOutput is used to limit the frame rate. If you cannot and don't want to use it, you may implement your own subsystem that will do the same, e.g. as a custom SoundOutput or something else, bound under a tick() method in the main emulation loop.

The main idea for the algorithm is to:

  1. Store the current time in millis: startTime.
  2. Count N ticks. I'd suggest choosing N as a 2 power, e.g. 65536.
  3. After N ticks, get the current time again: endTime.
  4. Calculate how much time should pass for this N ticks (65536/TICKS_PER_SEC = 15.625 millis) - this is actually a constant, which can be precalculated.
  5. Check how much time actually elapsed when running these N ticks: wallTime = endTime - startTime.
  6. Thread.sleep() for the difference between emulator time and wall time: 15.625 - wallTime.

If for some reason the wall time is larger than the emulator time, it means that the emulation is too slow and can't keep up with the real Gameboy speed. This shouldn't happen, but for the sound-based frame limit sometimes it does, which results in a sound buffer underrun and a short crash in the speakers.

I might be completely missing the point here, but here is what I've come up with:

`public class AudioSystemSoundOutput implements SoundOutput, Runnable {

private final int N = 65536;
private final long millis = (N * 1000) / Gameboy.TICKS_PER_SEC;

private volatile boolean doStop;
private volatile boolean isPlaying;

private long startTime = System.currentTimeMillis();
private long endTime = startTime;

@Override
public void run() {

}

public void stopThread() {
    doStop = true;
}

@Override
public void start() {
    isPlaying = true;
}

@Override
public void stop() {
    isPlaying = false;
}

@Override
public void play(int left, int right) {
    synchronized (this) {
        startTime = System.currentTimeMillis();
        int tickPos = 0;
        while (tickPos++ != N) {
            this.notify();
        }
        endTime = System.currentTimeMillis();
        long wallTime = endTime - startTime;
        long shouldWait = millis - wallTime;
        try {
            Thread.sleep(shouldWait);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

}`

Though this results in a blank screen showing up. I'm not entirely sure if this is what you meant. But if you can give me a nudge in the right direction that would be great :D

This is almost good, but you have to remember that the play() is called for every tick. This play() invocations is something that you need to count in the tickPos rather than increasing it manually inside the method. I was thinking about something like that:

    private static final int WINDOW_TICKS = 65536;

    private static final long WINDOW_DURATION_MILLIS = 1000 * WINDOW_TICKS / Gameboy.TICKS_PER_SEC;

    private long windowStartedAt = System.currentTimeMillis();

    private int ticks;

    @Override
    public void play(int left, int right) {
        ticks++;
        if (ticks < WINDOW_TICKS) {
            return;
        }
        long windowFinishedAt = System.currentTimeMillis();
        long actualWindowDuration = windowFinishedAt - windowStartedAt;
        long timeToSleep = WINDOW_DURATION_MILLIS - actualWindowDuration;
        if (timeToSleep > 0) {
            try {
                Thread.sleep(timeToSleep);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        ticks = 0;
        windowStartedAt = windowFinishedAt;
    }

Awesome, thanks for the help. It's working now :D

If you're interested: https://youtu.be/WmKephD31DU