dirkwhoffmann / virtualc64

VirtualC64 is a cycle-accurate C64 emulator for macOS
https://dirkwhoffmann.github.io/virtualc64
Other
342 stars 33 forks source link

Implement run-ahead #782

Closed dirkwhoffmann closed 1 month ago

dirkwhoffmann commented 5 months ago

There is a neat summary of the run-ahead technique here: https://bsnes.org/articles/input-run-ahead

I think the run-ahead technique can be integrated into VirtualC64 with reasonable effort, and the emulator would benefit significantly from it.

As preparatory work, the following should be done:

dirkwhoffmann commented 5 months ago

Update:

My current run-ahead prototyping code looks as follows:

void
Emulator::computeFrame()
{
    // Emulate the main instance for one frame
    main.execute();

    if (config.runAhead) {

        if (updateRunAhead || RUA_ON_STEROIDS) {

            // Recreate the runahead instance from scratch
            ahead = main; updateRunAhead = false;

            if (debugBuild && ahead != main) {

                main.dump(Category::Checksums);
                ahead.dump(Category::Checksums);
                fatal("Corrupted run-ahead clone");
            }

            // Advance to the proper frame
            ahead.fastForward(config.runAhead);

        } else {

            // Run the run-ahead instance in parallel to the main instance
            ahead.execute();
        }
    }
}

Currently, it only works with debug option RUA_ON_STEROIDS enabled, which forces the emulator to recreates the run-ahead instance every frame. This is a performance nightmare, but it is still fast enough in release builds for experimental testing. Later, the run-ahead instance will only be recreated if the primary instance diverges due to an external event.

It already works pretty nicely. Below, I’ve tested with Boulder Dash and a run-ahead of 4 frames. It does feel snappier, but this is just a first personal impression and not backed up by any data.

Bildschirmfoto 2024-02-21 um 08 57 14

A good test candidate would be a program that explicitly tests the user’s reaction time by displaying something on the screen and measuring how fast the user reacts, e.g., by pressing a button. If somebody knows such a program, any hint is highly appreciated.

dirkwhoffmann commented 5 months ago

Just thinking out loud: Instead of running two instances in parallel and recreating the second one via fast-forwarding when an external event comes in, the same effect is achievable by rewinding. In detail:

When the instance gets dirty due to an external event (joystick movement, etc.), fast-rewind by n frames by copying over a state from the ring buffer and fast-forward by emulating n frames.

Pros:

Cons:

UPDATE: There is another big Con: We cannot easily rewind what's been written in the audio buffer. The advantage of the current approach is that the run-ahead instance only provides the texture. Audio is still coming from the main instance.

dirkwhoffmann commented 5 months ago

Update: The run-ahead instance is only recreated when needed. In addition, frames that are not displayed are computed in headless mode, which further saves computation time. Now, run-ahead can be used in debug builds without any issues. The new run-ahead logic looks as follows and should be pretty self-explanatory:

void
Emulator::computeFrame()
{
    if (config.runAhead) {

        // Run the main instance
        main.executeHeadless();

        // Recreate the run-ahead instance if necessary
        if (main.isDirty || RUA_ON_STEROIDS) recreateRunAheadInstance();

        // Run the runahead instance
        ahead.execute();

    } else {

        // Run the main instance
        main.execute();
    }
}

void 
Emulator::recreateRunAheadInstance()
{
    // Recreate the runahead instance from scratch
    ahead = main; main.isDirty = false;

    if (RUA_DEBUG && ahead != main) {

        main.dump(Category::Checksums);
        ahead.dump(Category::Checksums);
        fatal("Corrupted run-ahead clone detected");
    }

    // Advance to the proper frame
    ahead.fastForward(config.runAhead - 1);
}

void 
C64::fastForward(isize frames)
{
    auto target = frame + frames;

    // Execute until the target frame has been reached
    while (frame < target) executeHeadless();
}
dirkwhoffmann commented 1 month ago

Fixed in v5.0b1