Major overhaul to input recording, including fixing major desyncs during playback and a small bug in the .DTM file format. Like netplay, some emulator options (specifically dual core and idle skipping) can cause desyncs, and the more your plugin options are similar to the ones used during recording, the more likely playback will sync.

Also, input movies are now linked to savestates instead of just selecting a file to save to and running a game and are exported at a later time. This allows you to easily continue a recording across sessions and makes rerecording possible.

git-svn-id: https://dolphin-emu.googlecode.com/svn/trunk@6154 8ced0084-cf51-0410-be5f-012b33b47a6e
This commit is contained in:
baby.lueshi 2010-08-30 07:05:47 +00:00
parent 6a695eff49
commit c1cac331a0
8 changed files with 191 additions and 113 deletions

View file

@ -24,6 +24,7 @@
#include "../ConfigManager.h"
#include "MemoryUtil.h"
#include "FileUtil.h"
#include "../OnFrame.h"
// english
SRAM sram_dump = {{
@ -415,7 +416,9 @@ u32 CEXIIPL::GetGCTime()
// hack in some netplay stuff
ltime = NetPlay_GetGCTime();
#endif
if (0 == ltime)
if (Frame::IsRecordingInput() || Frame::IsPlayingInput())
ltime = 1234567890; // TODO: Should you be able to set a custom time in movies?
else if (0 == ltime)
ltime = Common::Timer::GetLocalTimeSinceJan1970();
return ((u32)ltime - cJanuary2000);

View file

@ -31,20 +31,21 @@ bool g_bFrameStep = false;
bool g_bFrameStop = false;
bool g_bAutoFire = false;
u32 g_autoFirstKey = 0, g_autoSecondKey = 0;
u32 g_rerecords = 0;
bool g_bFirstKey = true;
PlayMode g_playMode = MODE_NONE;
unsigned int g_framesToSkip = 0, g_frameSkipCounter = 0;
int g_numPads = 0;
ControllerState *g_padStates;
ControllerState g_padState;
FILE *g_recordfd = NULL;
u64 g_frameCounter = 0, g_lagCounter = 0;
bool g_bPolled = false;
int g_numRerecords = 0;
std::string g_recordFile;
std::string g_recordFile = "0.dtm";
void FrameUpdate()
{
@ -67,17 +68,6 @@ void FrameUpdate()
if (g_bAutoFire)
g_bFirstKey = !g_bFirstKey;
// Dump/Read all controllers' states for this frame
if(IsRecordingInput())
fwrite(g_padStates, sizeof(ControllerState), g_numPads, g_recordfd);
else if(IsPlayingInput()) {
fread(g_padStates, sizeof(ControllerState), g_numPads, g_recordfd);
// End of recording
if(feof(g_recordfd))
EndPlayInput();
}
g_bPolled = false;
}
@ -196,11 +186,13 @@ bool IsPlayingInput()
}
// TODO: Add BeginRecordingFromSavestate
bool BeginRecordingInput(const char *filename, int controllers)
bool BeginRecordingInput(int controllers)
{
if(!filename || g_playMode != MODE_NONE || g_recordfd)
if(g_playMode != MODE_NONE || g_recordfd)
return false;
const char *filename = g_recordFile.c_str();
if(File::Exists(filename))
File::Delete(filename);
@ -215,79 +207,44 @@ bool BeginRecordingInput(const char *filename, int controllers)
fwrite(&dummy, sizeof(DTMHeader), 1, g_recordfd);
g_numPads = controllers;
g_padStates = new ControllerState[controllers];
g_frameCounter = 0;
g_lagCounter = 0;
g_playMode = MODE_RECORDING;
g_recordFile = filename;
Core::DisplayMessage("Starting movie recording", 2000);
return true;
}
void EndRecordingInput()
{
rewind(g_recordfd);
// Create the real header now and write it
DTMHeader header;
memset(&header, 0, sizeof(DTMHeader));
header.filetype[0] = 'D'; header.filetype[1] = 'T'; header.filetype[2] = 'M'; header.filetype[3] = 0x1A;
strncpy((char *)header.gameID, Core::g_CoreStartupParameter.GetUniqueID().c_str(), 6);
header.bWii = Core::g_CoreStartupParameter.bWii;
header.numControllers = g_numPads;
header.bFromSaveState = false; // TODO: add the case where it's true
header.frameCount = g_frameCounter;
header.lagCount = g_lagCounter;
// TODO
header.uniqueID = 0;
header.numRerecords = 0;
// header.author;
// header.videoPlugin;
// header.audioPlugin;
fwrite(&header, sizeof(DTMHeader), 1, g_recordfd);
fclose(g_recordfd);
g_recordfd = NULL;
delete[] g_padStates;
g_playMode = MODE_NONE;
}
void RecordInput(SPADStatus *PadStatus, int controllerID)
{
if(!IsRecordingInput() || controllerID >= g_numPads || controllerID < 0)
return;
g_padState.A = ((PadStatus->button & PAD_BUTTON_A) != 0);
g_padState.B = ((PadStatus->button & PAD_BUTTON_B) != 0);
g_padState.X = ((PadStatus->button & PAD_BUTTON_X) != 0);
g_padState.Y = ((PadStatus->button & PAD_BUTTON_Y) != 0);
g_padState.Z = ((PadStatus->button & PAD_TRIGGER_Z) != 0);
g_padState.Start = ((PadStatus->button & PAD_BUTTON_START) != 0);
g_padStates[controllerID].A = ((PadStatus->button & PAD_BUTTON_A) != 0);
g_padStates[controllerID].B = ((PadStatus->button & PAD_BUTTON_B) != 0);
g_padStates[controllerID].X = ((PadStatus->button & PAD_BUTTON_X) != 0);
g_padStates[controllerID].Y = ((PadStatus->button & PAD_BUTTON_Y) != 0);
g_padStates[controllerID].Z = ((PadStatus->button & PAD_TRIGGER_Z) != 0);
g_padStates[controllerID].Start = ((PadStatus->button & PAD_BUTTON_START) != 0);
g_padState.DPadUp = ((PadStatus->button & PAD_BUTTON_UP) != 0);
g_padState.DPadDown = ((PadStatus->button & PAD_BUTTON_DOWN) != 0);
g_padState.DPadLeft = ((PadStatus->button & PAD_BUTTON_LEFT) != 0);
g_padState.DPadRight = ((PadStatus->button & PAD_BUTTON_RIGHT) != 0);
g_padStates[controllerID].DPadUp = ((PadStatus->button & PAD_BUTTON_UP) != 0);
g_padStates[controllerID].DPadDown = ((PadStatus->button & PAD_BUTTON_DOWN) != 0);
g_padStates[controllerID].DPadLeft = ((PadStatus->button & PAD_BUTTON_LEFT) != 0);
g_padStates[controllerID].DPadRight = ((PadStatus->button & PAD_BUTTON_RIGHT) != 0);
g_padState.L = PadStatus->triggerLeft;
g_padState.R = PadStatus->triggerRight;
g_padStates[controllerID].L = PadStatus->triggerLeft;
g_padStates[controllerID].R = PadStatus->triggerRight;
g_padState.AnalogStickX = PadStatus->stickX;
g_padState.AnalogStickY = PadStatus->stickY;
g_padStates[controllerID].AnalogStickX = PadStatus->stickX;
g_padStates[controllerID].AnalogStickY = PadStatus->stickY;
g_padState.CStickX = PadStatus->substickX;
g_padState.CStickY = PadStatus->substickY;
g_padStates[controllerID].CStickX = PadStatus->substickX;
g_padStates[controllerID].CStickY = PadStatus->substickY;
PlayController(PadStatus, controllerID);
fwrite(&g_padState, sizeof(ControllerState), 1, g_recordfd);
}
bool PlayInput(const char *filename)
@ -330,9 +287,7 @@ bool PlayInput(const char *filename)
*/
g_numPads = header.numControllers;
g_padStates = new ControllerState[g_numPads];
g_numRerecords = header.numRerecords;
g_recordFile = filename;
g_playMode = MODE_PLAYING;
@ -344,61 +299,146 @@ cleanup:
return false;
}
void LoadInput(const char *filename)
{
FILE *t_record = fopen(filename, "rb");
DTMHeader header;
fread(&header, sizeof(DTMHeader), 1, t_record);
if(header.filetype[0] != 'D' || header.filetype[1] != 'T' || header.filetype[2] != 'M' || header.filetype[3] != 0x1A) {
PanicAlert("Savestate movie %s is corrupted, movie recording stopping...", filename);
fclose(t_record);
EndPlayInput();
return;
}
if (g_rerecords == 0)
g_rerecords = header.numRerecords;
g_numPads = header.numControllers;
fclose(t_record);
if (g_recordfd)
fclose(g_recordfd);
File::Delete(g_recordFile.c_str());
File::Copy(filename, g_recordFile.c_str());
g_recordfd = fopen(g_recordFile.c_str(), "r+b");
fseek(g_recordfd, 0, SEEK_END);
g_rerecords++;
Core::DisplayMessage("Resuming movie recording", 2000);
g_playMode = MODE_RECORDING;
}
void PlayController(SPADStatus *PadStatus, int controllerID)
{
// Correct playback is entirely dependent on the emulator polling the controllers
// in the same order done during recording
if(!IsPlayingInput() || controllerID >= g_numPads || controllerID < 0)
return;
memset(PadStatus, 0, sizeof(SPADStatus));
fread(&g_padState, sizeof(ControllerState), 1, g_recordfd);
PadStatus->button |= PAD_USE_ORIGIN;
if(g_padStates[controllerID].A) {
if(g_padState.A) {
PadStatus->button |= PAD_BUTTON_A;
PadStatus->analogA = 0xFF;
}
if(g_padStates[controllerID].B) {
if(g_padState.B) {
PadStatus->button |= PAD_BUTTON_B;
PadStatus->analogB = 0xFF;
}
if(g_padStates[controllerID].X)
if(g_padState.X)
PadStatus->button |= PAD_BUTTON_X;
if(g_padStates[controllerID].Y)
if(g_padState.Y)
PadStatus->button |= PAD_BUTTON_Y;
if(g_padStates[controllerID].Z)
if(g_padState.Z)
PadStatus->button |= PAD_TRIGGER_Z;
if(g_padStates[controllerID].Start)
if(g_padState.Start)
PadStatus->button |= PAD_BUTTON_START;
if(g_padStates[controllerID].DPadUp)
if(g_padState.DPadUp)
PadStatus->button |= PAD_BUTTON_UP;
if(g_padStates[controllerID].DPadDown)
if(g_padState.DPadDown)
PadStatus->button |= PAD_BUTTON_DOWN;
if(g_padStates[controllerID].DPadLeft)
if(g_padState.DPadLeft)
PadStatus->button |= PAD_BUTTON_LEFT;
if(g_padStates[controllerID].DPadRight)
if(g_padState.DPadRight)
PadStatus->button |= PAD_BUTTON_RIGHT;
PadStatus->triggerLeft = g_padStates[controllerID].L;
PadStatus->triggerLeft = g_padState.L;
if(PadStatus->triggerLeft > 230)
PadStatus->button |= PAD_TRIGGER_L;
PadStatus->triggerRight = g_padStates[controllerID].R;
PadStatus->triggerRight = g_padState.R;
if(PadStatus->triggerRight > 230)
PadStatus->button |= PAD_TRIGGER_R;
PadStatus->stickX = g_padStates[controllerID].AnalogStickX;
PadStatus->stickY = g_padStates[controllerID].AnalogStickY;
PadStatus->stickX = g_padState.AnalogStickX;
PadStatus->stickY = g_padState.AnalogStickY;
PadStatus->substickX = g_padStates[controllerID].CStickX;
PadStatus->substickY = g_padStates[controllerID].CStickY;
PadStatus->substickX = g_padState.CStickX;
PadStatus->substickY = g_padState.CStickY;
if(feof(g_recordfd))
{
Core::DisplayMessage("Movie End", 2000);
// TODO: read-only mode
//EndPlayInput();
g_playMode = MODE_RECORDING;
}
}
void EndPlayInput() {
fclose(g_recordfd);
if (g_recordfd)
fclose(g_recordfd);
g_recordfd = NULL;
g_numPads = 0;
delete[] g_padStates;
g_numPads = g_rerecords = 0;
g_frameCounter = g_lagCounter = 0;
g_playMode = MODE_NONE;
}
void SaveRecording(const char *filename)
{
rewind(g_recordfd);
// Create the real header now and write it
DTMHeader header;
memset(&header, 0, sizeof(DTMHeader));
header.filetype[0] = 'D'; header.filetype[1] = 'T'; header.filetype[2] = 'M'; header.filetype[3] = 0x1A;
strncpy((char *)header.gameID, Core::g_CoreStartupParameter.GetUniqueID().c_str(), 6);
header.bWii = Core::g_CoreStartupParameter.bWii;
header.numControllers = g_numPads;
header.bFromSaveState = false; // TODO: add the case where it's true
header.frameCount = g_frameCounter;
header.lagCount = g_lagCounter;
header.numRerecords = g_rerecords;
// TODO
header.uniqueID = 0;
// header.author;
// header.videoPlugin;
// header.audioPlugin;
fwrite(&header, sizeof(DTMHeader), 1, g_recordfd);
fclose(g_recordfd);
if (File::Copy(g_recordFile.c_str(), filename))
Core::DisplayMessage(StringFromFormat("DTM %s saved", filename).c_str(), 2000);
else
Core::DisplayMessage(StringFromFormat("Failed to save %s", filename).c_str(), 2000);
g_recordfd = fopen(g_recordFile.c_str(), "r+b");
fseek(g_recordfd, 0, SEEK_END);
}
};

View file

@ -40,11 +40,12 @@ struct ControllerState {
bool Start:1, A:1, B:1, X:1, Y:1, Z:1; // Binary buttons, 6 bits
bool DPadUp:1, DPadDown:1, // Binary D-Pad buttons, 4 bits
DPadLeft:1, DPadRight:1;
bool reserved:6; // Reserved bits used for padding, 6 bits
u8 L, R; // Triggers, 16 bits
u8 AnalogStickX, AnalogStickY; // Main Stick, 16 bits
u8 CStickX, CStickY; // Sub-Stick, 16 bits
bool reserved:6; // Reserved bits, 6 bits
}; // Total: 58 + 6 = 64 bits per frame
#pragma pack(pop)
@ -110,13 +111,14 @@ void FrameSkipping();
void ModifyController(SPADStatus *PadStatus, int controllerID);
bool BeginRecordingInput(const char *filename, int controllers);
bool BeginRecordingInput(int controllers);
void RecordInput(SPADStatus *PadStatus, int controllerID);
void EndRecordingInput();
bool PlayInput(const char *filename);
void LoadInput(const char *filename);
void PlayController(SPADStatus *PadStatus, int controllerID);
void EndPlayInput();
void SaveRecording(const char *filename);
};

View file

@ -23,6 +23,7 @@
#include "StringUtil.h"
#include "Thread.h"
#include "CoreTiming.h"
#include "OnFrame.h"
#include "HW/HW.h"
#include "PowerPC/PowerPC.h"
#include "PowerPC/JitCommon/JitBase.h"
@ -255,6 +256,9 @@ void SaveStateCallback(u64 userdata, int cyclesLate)
saveStruct *saveData = new saveStruct;
saveData->buffer = buffer;
saveData->size = sz;
if (Frame::IsRecordingInput())
Frame::SaveRecording(StringFromFormat("%s.dtm", cur_filename.c_str()).c_str());
Core::DisplayMessage("Saving State...", 1000);
@ -369,6 +373,11 @@ void LoadStateCallback(u64 userdata, int cyclesLate)
Core::DisplayMessage("Unable to Load : Can't load state from other revisions !", 4000);
delete[] buffer;
if (File::Exists(StringFromFormat("%s.dtm", cur_filename.c_str()).c_str()))
Frame::LoadInput(StringFromFormat("%s.dtm", cur_filename.c_str()).c_str());
else
Frame::EndPlayInput();
state_op_in_progress = false;

View file

@ -246,6 +246,7 @@ EVT_MENU(IDM_STOP, CFrame::OnStop)
EVT_MENU(IDM_RESET, CFrame::OnReset)
EVT_MENU(IDM_RECORD, CFrame::OnRecord)
EVT_MENU(IDM_PLAYRECORD, CFrame::OnPlayRecording)
EVT_MENU(IDM_RECORDEXPORT, CFrame::OnRecordExport)
EVT_MENU(IDM_FRAMESTEP, CFrame::OnFrameStep)
EVT_MENU(IDM_LUA, CFrame::OnOpenLuaWindow)
EVT_MENU(IDM_SCREENSHOT, CFrame::OnScreenshot)

View file

@ -117,6 +117,7 @@ class CFrame : public CRenderFrame
void InitBitmaps();
void DoPause();
void DoStop();
void DoRecordingSave();
bool bRenderToMain;
bool bNoWiimoteMsg;
void UpdateGUI();
@ -272,6 +273,7 @@ class CFrame : public CRenderFrame
void OnReset(wxCommandEvent& event);
void OnRecord(wxCommandEvent& event);
void OnPlayRecording(wxCommandEvent& event);
void OnRecordExport(wxCommandEvent& event);
void OnChangeDisc(wxCommandEvent& event);
void OnScreenshot(wxCommandEvent& event);
void OnActive(wxActivateEvent& event);

View file

@ -131,8 +131,9 @@ void CFrame::CreateMenu()
emulationMenu->AppendSeparator();
emulationMenu->Append(IDM_TOGGLE_FULLSCREEN, GetMenuLabel(HK_FULLSCREEN));
emulationMenu->AppendSeparator();
emulationMenu->Append(IDM_RECORD, _T("Start Re&cording..."));
emulationMenu->Append(IDM_RECORD, _T("Start Re&cording"));
emulationMenu->Append(IDM_PLAYRECORD, _T("P&lay Recording..."));
emulationMenu->Append(IDM_RECORDEXPORT, _T("Export Recording..."));
emulationMenu->AppendSeparator();
emulationMenu->Append(IDM_CHANGEDISC, _T("Change &Disc"));
@ -627,23 +628,8 @@ void CFrame::OnChangeDisc(wxCommandEvent& WXUNUSED (event))
void CFrame::OnRecord(wxCommandEvent& WXUNUSED (event))
{
wxString path = wxFileSelector(
_T("Select The Recording File"),
wxEmptyString, wxEmptyString, wxEmptyString,
wxString::Format
(
_T("Dolphin TAS Movies (*.dtm)|*.dtm|All files (%s)|%s"),
wxFileSelectorDefaultWildcardStr,
wxFileSelectorDefaultWildcardStr
),
wxFD_SAVE | wxFD_PREVIEW,
this);
if(path.IsEmpty())
return;
// TODO: Take controller settings from Gamecube Configuration menu
if(Frame::BeginRecordingInput(path.mb_str(), 1))
if(Frame::BeginRecordingInput(1))
BootGame(std::string(""));
}
@ -668,6 +654,11 @@ void CFrame::OnPlayRecording(wxCommandEvent& WXUNUSED (event))
BootGame(std::string(""));
}
void CFrame::OnRecordExport(wxCommandEvent& WXUNUSED (event))
{
DoRecordingSave();
}
void CFrame::OnPlay(wxCommandEvent& WXUNUSED (event))
{
if (Core::GetState() != Core::CORE_UNINITIALIZED)
@ -901,8 +892,8 @@ void CFrame::DoStop()
// TODO: Show the author/description dialog here
if(Frame::IsRecordingInput())
Frame::EndRecordingInput();
if(Frame::IsPlayingInput())
DoRecordingSave();
if(Frame::IsPlayingInput() || Frame::IsRecordingInput())
Frame::EndPlayInput();
// These windows cause segmentation faults if they are open when the emulator
@ -946,6 +937,34 @@ void CFrame::DoStop()
}
}
void CFrame::DoRecordingSave()
{
bool paused = (Core::GetState() == Core::CORE_PAUSE);
if (!paused)
DoPause();
wxString path = wxFileSelector(
_T("Select The Recording File"),
wxEmptyString, wxEmptyString, wxEmptyString,
wxString::Format
(
_T("Dolphin TAS Movies (*.dtm)|*.dtm|All files (%s)|%s"),
wxFileSelectorDefaultWildcardStr,
wxFileSelectorDefaultWildcardStr
),
wxFD_SAVE | wxFD_PREVIEW,
this);
if(path.IsEmpty())
return;
Frame::SaveRecording(path.mb_str());
if (!paused)
DoPause();
}
void CFrame::OnStop(wxCommandEvent& WXUNUSED (event))
{
m_bGameLoading = false;
@ -1281,6 +1300,7 @@ void CFrame::UpdateGUI()
GetMenuBar()->FindItem(IDM_RESET)->Enable(Running || Paused);
GetMenuBar()->FindItem(IDM_RECORD)->Enable(!Initialized);
GetMenuBar()->FindItem(IDM_PLAYRECORD)->Enable(!Initialized);
GetMenuBar()->FindItem(IDM_RECORDEXPORT)->Enable(Frame::IsRecordingInput());
GetMenuBar()->FindItem(IDM_FRAMESTEP)->Enable(Running || Paused);
GetMenuBar()->FindItem(IDM_SCREENSHOT)->Enable(Running || Paused);
GetMenuBar()->FindItem(IDM_TOGGLE_FULLSCREEN)->Enable(Running || Paused);

View file

@ -77,6 +77,7 @@ enum
IDM_TOGGLE_FULLSCREEN,
IDM_RECORD,
IDM_PLAYRECORD,
IDM_RECORDEXPORT,
IDM_FRAMESTEP,
IDM_SCREENSHOT,
IDM_BROWSE,