Improved Rewind

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

JetSetIlly

I've made an improvement to the rewind system this evening. I was prompted by a problem somebody was having on AtariAge and I tried to solve it.  I think it's quite interesting so I'll give a brief explanation here.

The problem was related to being able add a breakpoint and being able to step back an instruction once the breakpoint has triggered. Gopher2600 can do this and has been able to for a long time, but user-input could cause issues. I'll explain why:

Gopher2600 creates a snapshot of the emulated machine every frame but crucially, can automatically reconstruct any intermediary state. So, from a paused position you can instruct the emulator to, for example, "GOTO frame 10, scanline 4, clock 50" and it can do that even though that specific state was never recorded.

I like to think of this as being a bit like video compression - record key-frames and delta information for the frames in between. In the case of the Atari 2600, the delta information is just the instructions of the 6507 program.

However, controller input from the user is also delta information that needs to be fed into the reconstruction. Gopher2600's flaw could be seen when rewinding to an intermediary state where user input happened after a key-frame. In those instances user-input would be lost. For rewinding gameplay, this wasn't a problem, but it can very definitely be a problem inside the debugger.

The improvement is to record user-input during the intermediary states and reinsert the user input on reconstruction.

The specific example I was trying to solve was to be able to step back an instruction after the emulation has halted due to a memory write. In this example, the memory write happens in response to user-input so previously, stepping back an instruction from the breakpoint would result in a different reconstructed codepath - causing the debugger to be in an unexpected place.

I created a short video demonstrating the new feature.


1) A "watch write" command is entered in the terminal
2) I press "right" on the keyboard (you can see the SWCHA register in the Ports window change briefly)
3) The emulation is halted and the reason given in the title bar
4) Execution can be stepped back to the previous instruction as expected


Gopher2600's input system is quite complex so I need to make sure all this is bug free and robust before making another release. But this is something I've been wanting to add for a long time. Happily, it turned out to be a lot easier than I expected :-)
https://github.com/JetSetIlly/Gopher2600
@JetSetIlly@mastodon.gamedev.place
@jetsetilly.bsky.social

JetSetIlly

#1
The analogy of video compression and the concepts of key-frames and delta information is a good one I believe. It's worth exploring and explaining some more.

I've mentioned how in the case of 2600 emulation, the delta information in this analogy is effectively just the 6507 program. That is, given a starting state the 6507 program is deterministic and will lead us to any subsequent intermediary state.

The problem is caused by information from outside of the 6507 program influencing the code path of the program. In the post above I talked about user input being a source of influencing information.

The question is whether there are other sources of influence?  As it happens, there are.

Randomisation

Gopher2600, like Stella, allows randomisation of the data bus. This emulates the behaviour where a pins are "undriven" by the TIA. Data bus pins are always driven by the CPU but this is not the case with the TIA. In these instances the floating pins are indeterminate and the programmer should not rely on the values on this pins. It is very useful therefore, to ensure that floating pins are useless for effective decision making. Randomising the pins is a good and cheap way of doing this.

However, randomisation could cause problems with the rewind system precisely because it can influence decision making. What we don't want is for an intermediate state to be recreated using random numbers that cause a different code path to be taken. That would be very confusing for the game developer.

The solution to this is pseudo-randomisation. Pseudo-randomisation requires a seed which is normally taken to be the value of the system clock (of the host computer). But this type of seed isn't so useful for us because we want to be able to recreate a random number. We can reuse a seed but remember that we are potentially asking for a random number every time the TIA is accessed - we need to know the precise seed at any given moment.

The solution I've chosen for this this is to use a point of reference within the emulation that never changes. In the case of the 2600 a good reference point is the state of the television. The television sub-system can report the location of the scanline at any moment. The GetCoords() function returns the current frame and scanline number and also the current horizontal position in terms of colour clocks.

Using the TV coordinates as the basis for our random seed allows us to answer the question, "what is the random number at this point of time?" The answer is the same the first time we ask it and on subsequent times during rewind.

There may be other solutions to this but I believe this is an elegant solution.

Networking

Another source of influence is data from a connected network. With the help of the PlusCart a 6507 program can reference data received from the internet. An example of information that might be received from the network include the current time or the current global high score. However, there is no guarantee that information from the network will be same the next time we ask for it and for the rewind system this is a problem.

What we definitely don't want to do is replay the network activity. In other words we don't want the rewind system to cause a network request to re-occur. In terms of our delta information, this could lead to an incorrect intermediate state. But we also don't want to put unwanted pressure on the connected server by flooding it with bogus requests.

The solution to this is, I'm afraid, very brutal. Whenever network activity occurs the rewind system is flushed. That is, a boundary state is inserted immediately on completion of the network activity. This means that a user can never rewind past a network event.

This certainly solves the problem of arriving at incorrect intermediary states but it also solves the problem of a user accidentally posting a high score multiple times, for example.

I'm not sure if there's a more elegant way of handling this. Network data is by its nature completely unpredictable. Unlike, say, user-input or randomisation, which are only somewhat unpredictable.

Caching the network data might be a good solution but my instinct is that it would require a high level of complexity that I'm not sure would bring much benefit. Still, it's something to consider for the future.

https://github.com/JetSetIlly/Gopher2600
@JetSetIlly@mastodon.gamedev.place
@jetsetilly.bsky.social

Thomas Jentzsch

Couldn't you record and replay the network input too?

JetSetIlly

Quote from: Thomas Jentzsch on 03 Dec 2024, 01:06 AMCouldn't you record and replay the network input too?
That's what I meant by caching really. So, rather than accessing the network directly, it could go through a caching proxy (inside the emulator).

It's possible I think but it sounds overly complex.
https://github.com/JetSetIlly/Gopher2600
@JetSetIlly@mastodon.gamedev.place
@jetsetilly.bsky.social

Thomas Jentzsch

Quote from: JetSetIlly on 03 Dec 2024, 01:12 AM
Quote from: Thomas Jentzsch on 03 Dec 2024, 01:06 AMCouldn't you record and replay the network input too?
That's what I meant by caching really. So, rather than accessing the network directly, it could go through a caching proxy (inside the emulator).

It's possible I think but it sounds overly complex.
I think it would be sufficient to record the input registers whenever the are accessed. Same as with controllers. So during the replay, you just lookup the current timestamp.

JetSetIlly

Quote from: Thomas Jentzsch on 03 Dec 2024, 01:16 AMI think it would be sufficient to record the input registers whenever the are accessed. Same as with controllers. So during the replay, you just lookup the current timestamp.
Yes. That would work. It amounts to the same thing though really.

The way I would implement caching is for the PlusROM network functions to wrap the network data with timestamp information. The proxy would peel off the timestamp and if it's in the cache the cached data is returned, otherwise make the network request.

The beauty of this is that there would be no special treatment in the rewind package, like there has to be for user input. It's just an inherent property of the PlusROM emulation.
https://github.com/JetSetIlly/Gopher2600
@JetSetIlly@mastodon.gamedev.place
@jetsetilly.bsky.social

Thomas Jentzsch