here is my proposal for update of RZX format from version 0.13 to version 0.14.
TL;DR:
Allow proper RZX recording regardless of blocked interrupts.
The change itself:
Rationale:Change the meaning of the 16 bit fetch counter of the IO recording frame in the following way:
bits 0-14 - the fetch counter itself
bit 15 - when set, the frame ends but the interrupt is not actually generated
The RZX format has served us for many years, but it has one annoying flaw, as we have already discussed some 13 years ago in this thread. As it is now, when the frame ends and the interrupt should be generated, the emulator can record the frame end only under the following conditions:
1) The interrupts are disabled.
2) The interrupts are enabled and the interrupt happens.
This seems to cover all the needed cases needed, but it actually does not. There is one more case which is missing:
3) The interrupts are enabled but blocked and the interrupt doesn't happen.
Under normal circumstances, the interrupts are not blocked for the entire duration of the interrupt pending period, so the emulator will be able to end the frame a bit later using the cases 1) or 2). But in case the interrupts were blocked for the entire duration, the recording emulator has to deal with that somehow. The choices are:
1) Abort the RZX recording. Not ideal for sure.
2) Emit the frame regardless. This leads to broken RZX which can't be played back reliably. As discussed before, when playing back the RZX, the emulator must disregard its own idea of blocking the interrupts. It can only use its own idea of disabled/enabled interrupts, but must generate the interrupt whenever it is enabled, regardless of if it is blocked or not.
3) Not finish the frame and combine it with the following one, creating a jumbo frame which actually spans several frames. This is perhaps the currently used solution in emulators, but even that has limited uses - this can be used for at most couple of frames before the 16 bit counters overflow, and then the recording emulator is back to square one - it will perhaps have to abort the RZX recording. Not to mention that such jumbo frames are pain for playback in the emulators with high possibility of not being handled properly.
4) Technically, it can start creating snapshots at each such frame. While this "works" in a way, it's hardly a desirable solution.
This proposal adds the preferable solution for this problem:
5) Simply emit a frame which relays the information that the interrupt didn't happen.
What now:
OK, that's the proposal, but what should you do now? There are several options:
1) Do nothing. I can understand that not everybody is willing to jump aboard. Fair enough. The chance of encountering a publicly distributed RZX file which would actually need this is pretty low, so feel free not to do anything.
2) To support the change and claim compatibility, the good news is that there is no need to modify the existing recording code unless you want to. All that is needed is to add support for playback of these frames. And that is actually extremely trivial. For example, these are essentially all the changes which were needed to do this in ZXDS:
Code: Select all
diff --git a/source/main.cpp b/source/main.cpp
index 91335e4..9cb0429 100644
--- a/source/main.cpp
+++ b/source/main.cpp
@@ -139,7 +139,9 @@ bool emulate_rzx_frame( void )
if ( rzx_fetch_count <= RZX_SHORT_FRAME_LIMIT ) {
z80_emulate_fetches( &z80, rzx_fetch_count ) ;
- z80_force_irq( &z80 ) ;
+ if ( rzx_force_irq ) {
+ z80_force_irq( &z80 ) ;
+ }
return false ;
}
@@ -203,7 +205,9 @@ bool emulate_rzx_frame( void )
// Now trigger an interrupt, even at places we would not normally
// allow it due blocked instructions like delayed EI.
- z80_force_irq( &z80 ) ;
+ if ( rzx_force_irq ) {
+ z80_force_irq( &z80 ) ;
+ }
return true ;
}
diff --git a/source/rzx.cpp b/source/rzx.cpp
index 577ba05..cb7e8bb 100644
--- a/source/rzx.cpp
+++ b/source/rzx.cpp
@@ -16,6 +16,10 @@ DTCM_BSS static uint start_count ;
DTCM_BSS bool rzx_playing ;
+// Flag set when RZX should force IRQ at end of frame.
+
+DTCM_BSS bool rzx_force_irq ;
+
// How many instructions (or more exactly, M1 fetch cycles) to execute in the current frame.
//
// Each instruction takes at least 4 T states, but may take as much as 23 (EX (SP),IX),
@@ -468,7 +473,9 @@ bool prepare_rzx_frame( Z80 * z )
return false ;
}
- rzx_fetch_count = le( frame.fetch_count ) ;
+ const uint fetch_count = le( frame.fetch_count ) ;
+ rzx_fetch_count = ( fetch_count & 0x7FFF ) ;
+ rzx_force_irq = ( ( fetch_count & 0x8000 ) == 0 ) ;
in_count = le( frame.in_count ) ;
#ifdef DEBUG_RZX
3) If you want to change your RZX recording code, or you are only starting to write your own for the first time, you may think about emitting these frames any time the frame ends and the interrupt is not taken. However, while the created RZX would work in any emulator which supports this, any older emulator would not be able to play it back correctly. So I suggest not to do this and look at the next option instead. Still, the ability of being able to play such frames back in compatible emulator can be great help while you are developing and testing your RZX recording code.
4) Use these frames internally any way you wish, but do not let them appear in the final RZX stream unless necessary. This is what ZXDS does. It uses these frames a lot (the ability to force the frame flush any time I need is extremely helpful, especially for the implementation of the rollback) but then it coalesces them with the following short frames whenever possibile, so they never appear in the resulting stream. Still, in case the finalization sweep pass would not work out for some reason (for example, the SD card becomes full while recording), it's nice that these frames can be played back as they are and the RZX can be independently finalized later.
So, these are the options - which one you choose is entirely up to you, but it would be appreciated if you consider at least the option 2).
For reference, here is the code which handles the RZX frame end and which is called after each instruction whenever the interrupt is pending:
Code: Select all
// Handle end-of-frame during RZX recording.
void finish_rzx_frame( Z80 * z )
{
hope( z ) ;
hope( rzx_recording ) ;
// Do nothing if we are not recording frames.
if ( input_block_offset == 0 ) {
return ;
}
// Make sure this is called only when the interrupt is really pending.
hope( ( z->regs.iff & F_IRQ ) != 0 ) ;
// What we do depends on if the interrupts are enabled or not and blocked or not.
const bool enabled = ( z->regs.iff & F_IFF1 ) != 0 ;
const bool blocked = ( z->regs.iff & F_BLOCK ) != 0 ;
// In case the interrupts are enabled and not blocked, interrupt will happen, so we have to finish
// the frame no matter what so the RZX replay knows that it must generate the interrupt at this point.
// This is where the short RZX frames may come from, for example because of IRQ retrigger.
if ( enabled && ! blocked ) {
flush_rzx_frame( z ) ;
return ;
}
// In case the interrupts are disabled, interrupt will not happen, regardless of if it is blocked or not.
// In this case we finish the frame so the RZX replay knows that the frame has ended at this point.
// But we do it only if we didn't finish it already shortly before now, or the previous frame was forced.
if ( ! enabled ) {
if ( frame_fetches( z ) > RZX_SHORT_FRAME_LIMIT || last_frame_was_forced ) {
flush_rzx_frame( z ) ;
}
return ;
}
// Otherwise force flush of the frame, unless it was already finished shortly before.
//
// If we didn't, we could miss the frame end entirely in case the interrupt is blocked by chain of EI/FD/DD,
// creating a jumbo frame. Seems like the RZX authors simply forgot to deal with this case.
// Technically, we could start storing the snapshots at each such point, but that's hardly desirable.
// Therefore this support for forced frames was added to deal with it effectively.
if ( frame_fetches( z ) > RZX_SHORT_FRAME_LIMIT ) {
flush_rzx_frame( z, true ) ;
}
}
Well, that's all, I guess. Comments?
Patrik