From c1cac331a0f0d740a3485719d9625383d33aaeb6 Mon Sep 17 00:00:00 2001 From: "baby.lueshi" Date: Mon, 30 Aug 2010 07:05:47 +0000 Subject: [PATCH] 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 --- Source/Core/Core/Src/HW/EXI_DeviceIPL.cpp | 5 +- Source/Core/Core/Src/OnFrame.cpp | 220 +++++++++++++--------- Source/Core/Core/Src/OnFrame.h | 8 +- Source/Core/Core/Src/State.cpp | 9 + Source/Core/DolphinWX/Src/Frame.cpp | 1 + Source/Core/DolphinWX/Src/Frame.h | 2 + Source/Core/DolphinWX/Src/FrameTools.cpp | 58 ++++-- Source/Core/DolphinWX/Src/Globals.h | 1 + 8 files changed, 191 insertions(+), 113 deletions(-) diff --git a/Source/Core/Core/Src/HW/EXI_DeviceIPL.cpp b/Source/Core/Core/Src/HW/EXI_DeviceIPL.cpp index 29e7f71998..44f79f0043 100644 --- a/Source/Core/Core/Src/HW/EXI_DeviceIPL.cpp +++ b/Source/Core/Core/Src/HW/EXI_DeviceIPL.cpp @@ -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); diff --git a/Source/Core/Core/Src/OnFrame.cpp b/Source/Core/Core/Src/OnFrame.cpp index 17a61446a6..61d8db9a99 100644 --- a/Source/Core/Core/Src/OnFrame.cpp +++ b/Source/Core/Core/Src/OnFrame.cpp @@ -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); +} }; diff --git a/Source/Core/Core/Src/OnFrame.h b/Source/Core/Core/Src/OnFrame.h index d0d1c9bb2d..c46dd6af26 100644 --- a/Source/Core/Core/Src/OnFrame.h +++ b/Source/Core/Core/Src/OnFrame.h @@ -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); }; diff --git a/Source/Core/Core/Src/State.cpp b/Source/Core/Core/Src/State.cpp index 9f7a739c84..938228e783 100644 --- a/Source/Core/Core/Src/State.cpp +++ b/Source/Core/Core/Src/State.cpp @@ -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; diff --git a/Source/Core/DolphinWX/Src/Frame.cpp b/Source/Core/DolphinWX/Src/Frame.cpp index 56de51cfba..301a43fde7 100644 --- a/Source/Core/DolphinWX/Src/Frame.cpp +++ b/Source/Core/DolphinWX/Src/Frame.cpp @@ -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) diff --git a/Source/Core/DolphinWX/Src/Frame.h b/Source/Core/DolphinWX/Src/Frame.h index 8a41936fc9..076420c631 100644 --- a/Source/Core/DolphinWX/Src/Frame.h +++ b/Source/Core/DolphinWX/Src/Frame.h @@ -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); diff --git a/Source/Core/DolphinWX/Src/FrameTools.cpp b/Source/Core/DolphinWX/Src/FrameTools.cpp index 97fa7ec42f..a80ea00e92 100644 --- a/Source/Core/DolphinWX/Src/FrameTools.cpp +++ b/Source/Core/DolphinWX/Src/FrameTools.cpp @@ -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); diff --git a/Source/Core/DolphinWX/Src/Globals.h b/Source/Core/DolphinWX/Src/Globals.h index 5db01cf710..a4a7a90367 100644 --- a/Source/Core/DolphinWX/Src/Globals.h +++ b/Source/Core/DolphinWX/Src/Globals.h @@ -77,6 +77,7 @@ enum IDM_TOGGLE_FULLSCREEN, IDM_RECORD, IDM_PLAYRECORD, + IDM_RECORDEXPORT, IDM_FRAMESTEP, IDM_SCREENSHOT, IDM_BROWSE,