/* rzx.c: .rzx files
Copyright (c) 2002-2016 Philip Kendall
Copyright (c) 2014 Sergio Baldoví
Copyright (c) 2015 Stuart Brady
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Author contact information:
E-mail: philip-fuse@shadowmagic.org.uk
*/
#include "config.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
#ifdef WIN32
#include <windows.h>
#endif /* #ifdef WIN32 */
#include "debugger/debugger.h"
#include "event.h"
#include "fuse.h"
#include "infrastructure/startup_manager.h"
#include "machine.h"
#include "movie.h"
#include "peripherals/ula.h"
#include "rzx.h"
#include "settings.h"
#include "snapshot.h"
#include "timer/timer.h"
#include "ui/ui.h"
#include "utils.h"
#include "z80/z80.h"
#include "z80/z80_macros.h"
#define RZX_SENTINEL_TIME ( ULA_CONTENTION_SIZE - 1000 )
#define RZX_SENTINEL_TIME_REDUCE 8000
/* The offset used to get the count of instructions from the R register;
(instruction count) = R + rzx_instructions_offset */
int rzx_instructions_offset;
/* The number of bytes read via IN during the current frame */
size_t rzx_in_count;
/* The number of frames we've recorded in this RZX file */
static size_t autosave_frame_count;
/* And the values of those bytes */
libspectrum_byte *rzx_in_bytes;
/* How big is the above array? */
size_t rzx_in_allocated;
/* Are we currently recording a .rzx file? */
int rzx_recording;
/* Is the .rzx file being recorded in competition mode? */
int rzx_competition_mode;
/* The filename we'll save this recording into */
static char *rzx_filename;
/* Are we currently playing back a .rzx file? */
int rzx_playback;
int sentinel_warning;
/* The number of instructions in the current .rzx playback frame */
size_t rzx_instruction_count;
/* The current RZX data */
libspectrum_rzx *rzx;
/* Fuse's DSA key */
libspectrum_rzx_dsa_key rzx_key = {
"A9E3BD74E136A9ABD41E614383BB1B01EB24B2CD7B920ED6A62F786A879AC8B00F2FF318BF96F81654214B1A064889FF6D8078858ED00CF61D2047B2AAB7888949F35D166A2BBAAE23A331BD4728A736E76901D74B195B68C4A2BBFB9F005E3655BDE8256C279A626E00C7087A2D575F78D7DC5CA6E392A535FFE47A816BA503", /* p */
"FE8D540EED2CAE1983690E2886259F8956FB5A19", /* q */
"9680ABFFB98EF2021945ADDF86C21D6EE3F7C8777FB0A0220AB59E9DFA3A3338611B32CFD1F22F8F26547858754ED93BFBDD87DC13C09F42B42A36B2024467D98EB754DEB2847FCA7FC60C81A99CF95133847EA38AD9D037AFE9DD189E9F0EE47624848CEE840D7E3724A39681E71B97ECF777383DC52A48C0A2C93BADA93F4C", /* g */
"46605F0514D56BC0B4207A350367A5038DBDD4DD62B7C997D26D0ADC5BE42D01F852C199E34553BCBCE5955FF80E3B402B55316606D7E39C0F500AE5EE41A7B7A4DCE78EC19072C21FCC7BA48DFDC830C17B72BCAA2B2D70D9DFC0AAD9B7E73F7AEB6241E54D55C33E41AB749CAAFBE7AB00F2D74C500E5F5DD63BD299C65778", /* y */
"948744AA7A1D1BE9EE65150B0A95A678B4181F0E" /* x */
};
/* By how much is the speed allowed to deviate from 100% whilst recording
a competition mode RZX file */
static const float SPEED_TOLERANCE = 5;
/* How often will we create an autosave file */
static const size_t AUTOSAVE_INTERVAL = 5 * 50;
/* Debugger events */
static const char * const event_type_string = "rzx";
static const char * const end_event_detail_string = "end";
int end_event;
static int start_playback( libspectrum_rzx *from_rzx );
static void start_recording( libspectrum_rzx *to_rzx, int competition_mode );
static int recording_frame( void );
static int playback_frame( void );
static int counter_reset( void );
static void rzx_sentinel( libspectrum_dword ts, int type,
void *user_data );
static int sentinel_event;
static int
rzx_init( void *context )
{
rzx_recording = rzx_playback = 0;
rzx_in_bytes = NULL;
rzx_in_allocated = 0;
sentinel_warning = 0;
sentinel_event = event_register( rzx_sentinel, "RZX sentinel" );
end_event = debugger_event_register( event_type_string, end_event_detail_string );
return 0;
}
static int
rzx_add_snap( libspectrum_rzx *to_rzx, int automatic )
{
int error;
libspectrum_snap *snap = libspectrum_snap_alloc();
error = snapshot_copy_to( snap );
if( error ) {
libspectrum_snap_free( snap );
return error;
}
error = libspectrum_rzx_add_snap( to_rzx, snap, automatic );
if( error ) {
libspectrum_snap_free( snap );
return error;
}
return 0;
}
int rzx_start_recording( const char *filename, int embed_snapshot )
{
int error;
if( rzx_playback ) return 1;
rzx = libspectrum_rzx_alloc();
/* Store the filename */
rzx_filename = utils_safe_strdup( filename );
/* If we're embedding a snapshot, create it now */
if( embed_snapshot ) {
error = rzx_add_snap( rzx, 0 );
if( error ) {
libspectrum_free( rzx_filename );
libspectrum_rzx_free( rzx );
return error;
}
}
start_recording( rzx, settings_current.competition_mode );
return 0;
}
int rzx_stop_recording( void )
{
libspectrum_byte *buffer; size_t length;
libspectrum_error libspec_error; int error;
if( !rzx_recording ) return 0;
/* Stop recording data */
rzx_recording = 0;
if( settings_current.movie_stop_after_rzx ) movie_stop();
/* Embed final snapshot */
if( !rzx_competition_mode ) rzx_add_snap( rzx, 0 );
libspectrum_free( rzx_in_bytes );
rzx_in_bytes = NULL;
rzx_in_allocated = 0;
ui_menu_activate( UI_MENU_ITEM_RECORDING, 0 );
ui_menu_activate( UI_MENU_ITEM_RECORDING_ROLLBACK, 0 );
libspectrum_creator_set_competition_code(
fuse_creator, settings_current.competition_code
);
length = 0;
buffer = NULL;
libspec_error = libspectrum_rzx_write(
&buffer, &length, rzx, LIBSPECTRUM_ID_SNAPSHOT_SZX, fuse_creator,
settings_current.rzx_compression, rzx_competition_mode ? &rzx_key : NULL
);
if( libspec_error != LIBSPECTRUM_ERROR_NONE ) {
libspectrum_free( rzx_filename );
libspectrum_rzx_free( rzx );
return libspec_error;
}
error = utils_write_file( rzx_filename, buffer, length );
libspectrum_free( rzx_filename );
if( error ) {
libspectrum_free( buffer );
libspectrum_rzx_free( rzx );
return error;
}
libspectrum_free( buffer );
libspec_error = libspectrum_rzx_free( rzx );
if( libspec_error != LIBSPECTRUM_ERROR_NONE ) return libspec_error;
return 0;
}
static libspectrum_snap*
rzx_get_initial_snapshot( void )
{
libspectrum_rzx_iterator it;
for( it = libspectrum_rzx_iterator_begin( rzx );
it;
it = libspectrum_rzx_iterator_next( it ) ) {
libspectrum_rzx_block_id id = libspectrum_rzx_iterator_get_type( it );
switch( id ) {
case LIBSPECTRUM_RZX_INPUT_BLOCK:
/* If we get this then there can't have been an initial snap to start
from */
return NULL;
case LIBSPECTRUM_RZX_SNAPSHOT_BLOCK:
/* Got initial snap */
return libspectrum_rzx_iterator_get_snap( it );
default:
continue;
}
}
return NULL;
}
int rzx_start_playback( const char *filename, int check_snapshot )
{
utils_file file;
libspectrum_error libspec_error; int error;
libspectrum_snap* snap;
if( rzx_recording ) return 1;
rzx = libspectrum_rzx_alloc();
error = utils_read_file( filename, &file );
if( error ) return error;
libspec_error = libspectrum_rzx_read( rzx, file.buffer, file.length );
if( libspec_error != LIBSPECTRUM_ERROR_NONE ) {
utils_close_file( &file );
return libspec_error;
}
utils_close_file( &file );
snap = rzx_get_initial_snapshot();
if( !snap && check_snapshot ) {
/* We need to load an external snapshot. Could be skipped if the snapshot
is preloaded from command line */
error = utils_open_snap();
if( error ) return error;
}
error = start_playback( rzx );
if( error ) {
libspectrum_rzx_free( rzx );
return error;
}
return 0;
}
int
rzx_start_playback_from_buffer( const unsigned char *buffer, size_t length )
{
int error;
libspectrum_snap* snap;
if( rzx_recording ) return 0;
rzx = libspectrum_rzx_alloc();
error = libspectrum_rzx_read( rzx, buffer, length );
if( error ) return error;
snap = rzx_get_initial_snapshot();
if( !snap ) {
error = utils_open_snap();
if( error ) {
libspectrum_rzx_free( rzx );
return error;
}
}
error = start_playback( rzx );
if( error ) {
libspectrum_rzx_free( rzx );
return error;
}
return 0;
}
static int
start_playback( libspectrum_rzx *from_rzx )
{
int error;
libspectrum_snap *snap;
error = libspectrum_rzx_start_playback( from_rzx, 0, &snap );
if( error ) return error;
if( snap ) {
error = snapshot_copy_from( snap );
if( error ) return error;
}
/* End of frame will now be generated by the RZX code */
event_remove_type( spectrum_frame_event );
/* Add a sentinel event to prevent tstates overrun (bug #25) */
event_add( RZX_SENTINEL_TIME, sentinel_event );
sentinel_warning = 0;
tstates = libspectrum_rzx_tstates( from_rzx );
rzx_instruction_count = libspectrum_rzx_instructions( from_rzx );
rzx_playback = 1;
counter_reset();
ui_menu_activate( UI_MENU_ITEM_RECORDING, 1 );
ui_menu_activate( UI_MENU_ITEM_RECORDING_ROLLBACK, 0 );
return 0;
}
int rzx_stop_playback( int add_interrupt )
{
libspectrum_error libspec_error;
if( !rzx_playback ) return 0;
rzx_playback = 0;
if( settings_current.movie_stop_after_rzx ) movie_stop();
ui_menu_activate( UI_MENU_ITEM_RECORDING, 0 );
ui_menu_activate( UI_MENU_ITEM_RECORDING_ROLLBACK, 0 );
event_remove_type( sentinel_event );
/* We've now finished with the RZX file, so add an end of frame
event if we've been requested to do so; we don't if we just run
out of frames, as this occurs just before a normal end of frame
and everything works normally as rzx_playback is now zero again */
if( add_interrupt ) {
event_add( machine_current->timings.tstates_per_frame,
spectrum_frame_event );
/* We're no longer doing RZX playback, so tstates now be <= the
normal frame count */
if( tstates > machine_current->timings.tstates_per_frame )
tstates = machine_current->timings.tstates_per_frame;
} else {
/* Ensure that tstates will be zero after it is reduced in
spectrum_frame() */
tstates = machine_current->timings.tstates_per_frame;
}
libspec_error = libspectrum_rzx_free( rzx );
if( libspec_error != LIBSPECTRUM_ERROR_NONE ) return libspec_error;
debugger_event( end_event );
return 0;
}
static void
start_recording( libspectrum_rzx *to_rzx, int competition_mode )
{
libspectrum_rzx_start_input( to_rzx, tstates );
counter_reset();
rzx_in_count = 0;
autosave_frame_count = 0;
rzx_recording = 1;
ui_menu_activate( UI_MENU_ITEM_RECORDING, 1 );
if( competition_mode ) {
if( !libspectrum_gcrypt_version() )
ui_error( UI_ERROR_WARNING,
"gcrypt not available: recording will NOT be signed" );
settings_current.emulation_speed = 100;
rzx_competition_mode = 1;
} else {
ui_menu_activate( UI_MENU_ITEM_RECORDING_ROLLBACK, 1 );
rzx_competition_mode = 0;
}
}
int
rzx_continue_recording( const char *filename )
{
utils_file file;
libspectrum_error libspec_error; int error;
libspectrum_snap* snap = NULL;
libspectrum_rzx_iterator last_it = NULL;
if( rzx_recording || rzx_playback ) return 1;
/* Store the filename */
rzx_filename = utils_safe_strdup( filename );
error = utils_read_file( filename, &file );
if( error ) return error;
rzx = libspectrum_rzx_alloc();
libspec_error = libspectrum_rzx_read( rzx, file.buffer, file.length );
if( libspec_error != LIBSPECTRUM_ERROR_NONE ) {
utils_close_file( &file );
return libspec_error;
}
utils_close_file( &file );
/* Get final snapshot */
last_it = libspectrum_rzx_iterator_last( rzx );
if( last_it ) snap = libspectrum_rzx_iterator_get_snap( last_it );
if( snap ) {
error = snapshot_copy_from( snap );
if( error ) return error;
} else {
libspectrum_free( rzx_filename );
libspectrum_rzx_free( rzx );
return 1;
}
start_recording( rzx, 0 );
return 0;
}
int
rzx_finalise_recording( const char *filename )
{
libspectrum_byte *buffer; size_t length;
libspectrum_error libspec_error; int error;
utils_file file;
if( rzx_recording || rzx_playback ) return 1;
error = utils_read_file( filename, &file );
if( error ) return error;
rzx = libspectrum_rzx_alloc();
libspec_error = libspectrum_rzx_read( rzx, file.buffer, file.length );
if( libspec_error != LIBSPECTRUM_ERROR_NONE ) {
utils_close_file( &file );
libspectrum_rzx_free( rzx );
return libspec_error;
}
utils_close_file( &file );
libspec_error = libspectrum_rzx_finalise( rzx );
if( libspec_error != LIBSPECTRUM_ERROR_NONE ) {
libspectrum_rzx_free( rzx );
return libspec_error;
}
/* Write the file */
length = 0;
buffer = NULL;
libspec_error = libspectrum_rzx_write(
&buffer, &length, rzx, LIBSPECTRUM_ID_SNAPSHOT_SZX, fuse_creator,
settings_current.rzx_compression, rzx_competition_mode ? &rzx_key : NULL
);
if( libspec_error != LIBSPECTRUM_ERROR_NONE ) {
libspectrum_rzx_free( rzx );
return libspec_error;
}
error = utils_write_file( filename, buffer, length );
if( error ) {
libspectrum_free( buffer );
libspectrum_rzx_free( rzx );
return error;
}
libspectrum_free( buffer );
libspectrum_rzx_free( rzx );
return 0;
}
int rzx_frame( void )
{
if( rzx_recording ) return recording_frame();
if( rzx_playback ) return playback_frame();
return 0;
}
typedef struct prune_info_t {
libspectrum_rzx_iterator it;
size_t frames;
} prune_info_t;
static void
autosave_prune( void )
{
GArray *autosaves = g_array_new( FALSE, FALSE, sizeof( prune_info_t ) );
libspectrum_rzx_iterator it;
size_t i, frames = 0;
for( it = libspectrum_rzx_iterator_begin( rzx );
it;
it = libspectrum_rzx_iterator_next( it ) ) {
libspectrum_rzx_block_id id = libspectrum_rzx_iterator_get_type( it );
switch( id ) {
case LIBSPECTRUM_RZX_INPUT_BLOCK:
frames += libspectrum_rzx_iterator_get_frames( it ); break;
case LIBSPECTRUM_RZX_SNAPSHOT_BLOCK:
if( libspectrum_rzx_iterator_snap_is_automatic( it ) ) {
prune_info_t info = { it, frames };
g_array_append_val( autosaves, info );
}
break;
default:
break;
}
}
/* Convert 'time from start' into 'time before now' */
for( i = 0; i < autosaves->len; i++ ) {
prune_info_t *info = &( g_array_index( autosaves, prune_info_t, i ) );
info->frames = frames - info->frames;
}
for( i = autosaves->len - 1; i > 0; i-- ) {
prune_info_t save1 = g_array_index( autosaves, prune_info_t, i ),
save2 = g_array_index( autosaves, prune_info_t, i - 1 );
if( ( save1.frames == 15 * 50 ||
save1.frames == 60 * 50 ||
save1.frames == 300 * 50 ) &&
save2.frames < 2 * save1.frames
)
/* FIXME: could possibly merge adjacent IRBs here */
libspectrum_rzx_iterator_delete( rzx, save1.it );
}
g_array_free( autosaves, TRUE );
}
static void
autosave_frame( void )
{
if( ++autosave_frame_count % AUTOSAVE_INTERVAL ) return;
rzx_add_snap( rzx, 1 );
libspectrum_rzx_start_input( rzx, tstates );
autosave_prune();
}
static void
autosave_reset( void )
{
libspectrum_rzx_iterator it;
size_t frames = 0;
for( it = libspectrum_rzx_iterator_begin( rzx );
it;
it = libspectrum_rzx_iterator_next( it ) ) {
libspectrum_rzx_block_id id = libspectrum_rzx_iterator_get_type( it );
switch( id ) {
case LIBSPECTRUM_RZX_INPUT_BLOCK:
frames += libspectrum_rzx_iterator_get_frames( it );
break;
case LIBSPECTRUM_RZX_SNAPSHOT_BLOCK:
if( libspectrum_rzx_iterator_snap_is_automatic( it ) ) {
frames = 0;
}
break;
default:
break;
}
}
/* Reset the frame count. Allow to prune previous points after rolling back */
autosave_frame_count = frames % AUTOSAVE_INTERVAL;
}
static int recording_frame( void )
{
libspectrum_error error;
error = libspectrum_rzx_store_frame( rzx, R + rzx_instructions_offset,
rzx_in_count, rzx_in_bytes );
if( error ) {
rzx_stop_recording();
return error;
}
/* Reset the instruction counter */
rzx_in_count = 0; counter_reset();
/* If we're in competition mode, check we're running at close to 100%
speed */
if( rzx_competition_mode &&
fabs( current_speed - 100.0 ) > SPEED_TOLERANCE ) {
rzx_stop_recording();
ui_error(
UI_ERROR_INFO,
"emulator speed is %d%%: stopping competition mode RZX recording",
(int)( current_speed )
);
}
if( !rzx_competition_mode && settings_current.rzx_autosaves )
autosave_frame();
return 0;
}
static int playback_frame( void )
{
int error, finished;
libspectrum_snap *snap;
error = libspectrum_rzx_playback_frame( rzx, &finished, &snap );
if( error ) return rzx_stop_playback( 0 );
if( finished ) {
ui_error( UI_ERROR_INFO, "Finished RZX playback" );
return rzx_stop_playback( 0 );
}
/* Move the RZX sentinel back out to 79000 tstates; the addition of
the frame length is because everything is reduced by that in
event_frame() */
event_remove_type( sentinel_event );
event_add( RZX_SENTINEL_TIME + tstates, sentinel_event );
if( snap ) {
error = snapshot_copy_from( snap );
if( error ) return rzx_stop_playback( 0 );
}
/* If we've got another frame to do, fetch the new instruction count and
continue */
rzx_instruction_count = libspectrum_rzx_instructions( rzx );
counter_reset();
return 0;
}
/* Reset the RZX counter; also, take this opportunity to normalise the
R register */
static int counter_reset( void )
{
R &= 0x7f; /* Clear all but the 7 lowest bits of the R register */
rzx_instructions_offset = -R; /* Gives us a zero count */
return 0;
}
int rzx_store_byte( libspectrum_byte value )
{
/* Get more space if we need it; allocate twice as much as we currently
have, with a minimum of 50 */
if( rzx_in_count == rzx_in_allocated ) {
libspectrum_byte *ptr; size_t new_allocated;
new_allocated = rzx_in_allocated >= 25 ? 2 * rzx_in_allocated : 50;
ptr = libspectrum_renew( libspectrum_byte, rzx_in_bytes, new_allocated );
rzx_in_bytes = ptr;
rzx_in_allocated = new_allocated;
}
rzx_in_bytes[ rzx_in_count++ ] = value;
return 0;
}
static void
rzx_end( void )
{
if( rzx_recording ) rzx_stop_recording();
if( rzx_playback ) rzx_stop_playback( 0 );
}
void
rzx_register_startup( void )
{
startup_manager_module dependencies[] = {
STARTUP_MANAGER_MODULE_DEBUGGER,
STARTUP_MANAGER_MODULE_EVENT,
STARTUP_MANAGER_MODULE_MACHINE,
STARTUP_MANAGER_MODULE_SETUID,
};
startup_manager_register( STARTUP_MANAGER_MODULE_RZX, dependencies,
ARRAY_SIZE( dependencies ), rzx_init, NULL,
rzx_end );
}
static GSList*
get_rollback_list( libspectrum_rzx *from_rzx )
{
libspectrum_rzx_iterator it;
GSList *rollback_points;
size_t frames;
it = libspectrum_rzx_iterator_begin( from_rzx );
rollback_points = NULL;
frames = 0;
while( it ) {
libspectrum_rzx_block_id id = libspectrum_rzx_iterator_get_type( it );
switch( id ) {
case LIBSPECTRUM_RZX_INPUT_BLOCK:
frames += libspectrum_rzx_iterator_get_frames( it ); break;
case LIBSPECTRUM_RZX_SNAPSHOT_BLOCK:
rollback_points = g_slist_append( rollback_points,
GINT_TO_POINTER( frames ) );
break;
default:
break;
}
it = libspectrum_rzx_iterator_next( it );
}
/* Add the final IRB in, if any */
if( frames )
rollback_points = g_slist_append( rollback_points,
GINT_TO_POINTER( frames ) );
return rollback_points;
}
static int
start_after_rollback( libspectrum_snap *snap )
{
int error;
error = snapshot_copy_from( snap );
if( error ) return error;
libspectrum_rzx_start_input( rzx, tstates );
error = counter_reset();
if( error ) return error;
if( settings_current.rzx_autosaves )
autosave_reset();
return 0;
}
int
rzx_rollback( void )
{
libspectrum_snap *snap;
int error;
error = libspectrum_rzx_rollback( rzx, &snap );
if( error ) return error;
error = start_after_rollback( snap );
if( error ) return error;
return 0;
}
int
rzx_rollback_to( void )
{
GSList *rollback_points;
libspectrum_snap *snap;
int which, error;
rollback_points = get_rollback_list( rzx );
which = ui_get_rollback_point( rollback_points );
if( which == -1 ) return 1;
error = libspectrum_rzx_rollback_to( rzx, &snap, which );
if( error ) return error;
error = start_after_rollback( snap );
if( error ) return error;
return 0;
}
static void
rzx_sentinel( libspectrum_dword ts GCC_UNUSED, int type GCC_UNUSED,
void *user_data GCC_UNUSED )
{
if( !sentinel_warning ) {
/* This message could pop up very often. Limited to once per playback */
ui_error( UI_ERROR_WARNING, "RZX frame is longer than %u tstates",
RZX_SENTINEL_TIME );
sentinel_warning = 1;
}
tstates -= RZX_SENTINEL_TIME_REDUCE;
z80.interrupts_enabled_at -= RZX_SENTINEL_TIME_REDUCE;
/* Add another sentinel event in case this frame continues a lot more after
this */
event_add( RZX_SENTINEL_TIME, sentinel_event );
}