diff --git a/Assets/Lua/GBA/SonicAdvance_CamHack.lua b/Assets/Lua/GBA/SonicAdvance_CamHack.lua index c861f29c72d..3710c7efff3 100644 --- a/Assets/Lua/GBA/SonicAdvance_CamHack.lua +++ b/Assets/Lua/GBA/SonicAdvance_CamHack.lua @@ -6,6 +6,8 @@ local addr_offY = 0x5B98 local addr_camX = 0x59D0 local addr_camY = 0x59D2 +event.onexit(function() client.invisibleemulation(false) end) + while true do client.invisibleemulation(true) local memorystate = memorysavestate.savecorestate() @@ -21,12 +23,11 @@ while true do mainmemory.write_u16_le(addr_camX, Xval) mainmemory.write_u16_le(addr_camY, Yval) - client.seekframe(emu.framecount()+1) + emu.frameadvance() client.invisibleemulation(false) - client.seekframe(emu.framecount()+1) + emu.frameadvance() client.invisibleemulation(true) memorysavestate.loadcorestate(memorystate) memorysavestate.removestate(memorystate) --- client.invisibleemulation(false) emu.frameadvance() end \ No newline at end of file diff --git a/Assets/Lua/seek.lua b/Assets/Lua/seek.lua new file mode 100644 index 00000000000..6b3e7d5ed16 --- /dev/null +++ b/Assets/Lua/seek.lua @@ -0,0 +1,26 @@ +-- Seek forward to a given frame. +-- This will unpause emulation, and restore the users' desired pause state when seeking is completed. +-- Note that this may interfere with TAStudio's seeking behavior. +local function force_seek_frame(frame) + local pause = client.unpause() + while emu.framecount() < frame do + -- The user may pause mid-seek, perhaps even by accident. + -- In this case, we will unpause but remember that the user wants to pause at the end. + if client.ispaused() then + pause = true + client.unpause() + end + -- Yield, not frameadvance. With frameadvance we cannot detect pauses, since frameadvance would not return. + -- This is true even if we have just called client.unpause. + emu.yield() + end + + if pause then client.pause() end +end + +-- Seek but without touching the pause state. Function will not return if the given frame is never reached due to the user manaully pausing/rewinding. +local function seek_frame(frame) + while emu.framecount() < frame do + emu.frameadvance() + end +end diff --git a/src/BizHawk.Client.Common/Api/Classes/EmuClientApi.cs b/src/BizHawk.Client.Common/Api/Classes/EmuClientApi.cs index ed58b2a16d8..000ea4f229b 100644 --- a/src/BizHawk.Client.Common/Api/Classes/EmuClientApi.cs +++ b/src/BizHawk.Client.Common/Api/Classes/EmuClientApi.cs @@ -228,7 +228,7 @@ public void SpeedMode(int percent) public Point TransformPoint(Point point) => _displayManager.TransformPoint(point); - public void Unpause() => _mainForm.UnpauseEmulator(); + public bool Unpause() => _mainForm.UnpauseEmulator(); public void UnpauseAv() => _mainForm.PauseAvi = false; diff --git a/src/BizHawk.Client.Common/Api/Interfaces/IEmuClientApi.cs b/src/BizHawk.Client.Common/Api/Interfaces/IEmuClientApi.cs index d27353282d7..6ce5392a945 100644 --- a/src/BizHawk.Client.Common/Api/Interfaces/IEmuClientApi.cs +++ b/src/BizHawk.Client.Common/Api/Interfaces/IEmuClientApi.cs @@ -154,7 +154,10 @@ public interface IEmuClientApi : IDisposable, IExternalApi Point TransformPoint(Point point); - void Unpause(); + /// True if should be called if you want to restore the previous pause state. + ///
Note that this is not the same as checking before unpausing. + /// If the user was holding frame advance, emulation will have already been unpaused and releasing frame advance will not pause.
+ bool Unpause(); void UnpauseAv(); diff --git a/src/BizHawk.Client.Common/IMainFormForApi.cs b/src/BizHawk.Client.Common/IMainFormForApi.cs index 3306b601e87..e964b5494c4 100644 --- a/src/BizHawk.Client.Common/IMainFormForApi.cs +++ b/src/BizHawk.Client.Common/IMainFormForApi.cs @@ -128,7 +128,7 @@ public interface IMainFormForApi void ToggleSound(); /// only referenced from - void UnpauseEmulator(); + bool UnpauseEmulator(); /// only referenced from event BeforeQuickLoadEventHandler QuicksaveLoad; diff --git a/src/BizHawk.Client.Common/lua/CommonLibs/ClientLuaLibrary.cs b/src/BizHawk.Client.Common/lua/CommonLibs/ClientLuaLibrary.cs index a411dee0199..cc336da7fba 100644 --- a/src/BizHawk.Client.Common/lua/CommonLibs/ClientLuaLibrary.cs +++ b/src/BizHawk.Client.Common/lua/CommonLibs/ClientLuaLibrary.cs @@ -95,7 +95,14 @@ public string GetLuaEngine() => "NLua+Lua"; [LuaMethodExample("client.invisibleemulation( true );")] - [LuaMethod("invisibleemulation", "Enters/exits turbo mode and disables/enables most emulator updates.")] + [LuaMethod( + name: "invisibleemulation", + description: "Disables/enables invisible emulation, starting on the next frame. During invisible emulation:" + + " (1) All rendering and sound is disabled, including A/V capture." + + " (2) Emulation runs at maximum speed with turbo enabled, regardless of pause state." + + " (3) Frame rate is not calculated." + + " (4) With one additional frame of delay, state capture for rewind+TAStudio is disabled." + )] public void InvisibleEmulation(bool invisible) => APIs.EmuClient.InvisibleEmulation(invisible); @@ -304,8 +311,8 @@ public LuaTable TransformPoint(int x, int y) { } [LuaMethodExample("client.unpause( );")] - [LuaMethod("unpause", "Unpauses the emulator")] - public void Unpause() + [LuaMethod("unpause", "Unpauses the emulator. Returns True if client.pause should be called if you want to restore the previous pause state. Note that this is not the same as checking client.ispaused before unpausing. If the user was holding frame advance, emulation will have already been unpaused and releasing frame advance will not pause.")] + public bool Unpause() => APIs.EmuClient.Unpause(); [LuaMethodExample("client.unpause_av( );")] diff --git a/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs b/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs index 88f31370942..867b0210a90 100644 --- a/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs +++ b/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs @@ -85,7 +85,7 @@ public interface IMainFormForTools : IDialogController void TogglePause(); /// referenced by 3 or more tools - void UnpauseEmulator(); + bool UnpauseEmulator(); /// only referenced from void Unthrottle(); diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 8264587aaf7..61b0a7a347e 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -1002,8 +1002,12 @@ public int ProgramRunLoop() Tools.GeneralUpdateActiveExtTools(); StepRunLoop_Core(); - Render(); - StepRunLoop_Throttle(); + + if (!_invisibleEmulation) + { + Render(); + StepRunLoop_Throttle(); + } // HACK: RAIntegration might peek at memory during messages // we need this to allow memory access here, otherwise it will deadlock @@ -1076,20 +1080,44 @@ protected override void Dispose(bool disposing) private bool _emulatorPaused; public bool EmulatorPaused { - get => _emulatorPaused; + get => _emulatorPaused && !_unpauseByFrameAdvance; private set { _didMenuPause = false; // overwritten where relevant if (_emulatorPaused == value) return; - if (_emulatorPaused && !value) // Unpausing - { - InitializeFpsData(); - } - - if (value != _emulatorPaused) Tools.OnPauseToggle(value); _emulatorPaused = value; + + OnPauseToggle(value); + } + } + + private bool _unpauseByFrameAdvance; + + /// + /// Avoids using EmulatorPaused to handle frame advance, thus allowing Lua to unpause during a frame advance. + /// + private bool UnpauseByFrameAdvance + { + get => _unpauseByFrameAdvance; + set + { + if (_unpauseByFrameAdvance == value) return; + _unpauseByFrameAdvance = value; + + OnPauseToggle(!value); + } + } + + private void OnPauseToggle(bool newPauseState) + { + if (!newPauseState) // Unpausing + { + InitializeFpsData(); } + + Tools.OnPauseToggle(newPauseState); + SetPauseStatusBarIcon(); } public bool BlockFrameAdvance { get; set; } @@ -1121,7 +1149,11 @@ private set /// /// /// - public bool InvisibleEmulation { get; set; } + public bool InvisibleEmulation { get => _invisibleUpdate; set => _invisibleEmulateNextFrame = value; } + + private bool _invisibleEmulateNextFrame; + private bool _invisibleEmulation; + private bool _invisibleUpdate; private long MouseWheelTracker; @@ -1146,7 +1178,7 @@ private set public bool IsSeeking => PauseOnFrame.HasValue; private bool IsTurboSeeking => PauseOnFrame.HasValue && Config.TurboSeek; - public bool IsTurboing => InputManager.ClientControls["Turbo"] || IsTurboSeeking || InvisibleEmulation; + public bool IsTurboing => InputManager.ClientControls["Turbo"] || IsTurboSeeking || _invisibleUpdate; public bool IsFastForwarding => InputManager.ClientControls["Fast Forward"] || IsTurboing; public bool IsRewinding { get; private set; } @@ -1341,13 +1373,13 @@ public bool RebootCore() public void PauseEmulator() { EmulatorPaused = true; - SetPauseStatusBarIcon(); } - public void UnpauseEmulator() + public bool UnpauseEmulator() { + bool ret = _emulatorPaused; EmulatorPaused = false; - SetPauseStatusBarIcon(); + return ret; } public void TogglePause() @@ -2930,13 +2962,14 @@ private void StepRunLoop_Core(bool force = false) // handle the initial trigger of a frame advance runFrame = true; _frameAdvanceTimestamp = currentTimestamp; + // Pausing is inconsistent with the behavior of TAStudio while seeking, but it's always been this way so. PauseEmulator(); } else if (frameProgressTimeElapsed) { runFrame = true; _runloopFrameProgress = true; - UnpauseEmulator(); + UnpauseByFrameAdvance = true; } } else @@ -2944,18 +2977,18 @@ private void StepRunLoop_Core(bool force = false) if (_runloopFrameAdvance) { // handle release of frame advance - PauseEmulator(); + UnpauseByFrameAdvance = false; } _runloopFrameProgress = false; } _runloopFrameAdvance = frameAdvance; + bool unpaused = !EmulatorPaused || _invisibleEmulation; #if BIZHAWKBUILD_SUPERHAWK - if (!EmulatorPaused && (!Config.SuperHawkThrottle || InputManager.ClientControls.AnyInputHeld)) -#else - if (!EmulatorPaused) + unpaused = unpaused && (!Config.SuperHawkThrottle || InputManager.ClientControls.AnyInputHeld); #endif + if (unpaused) { runFrame = true; } @@ -2969,15 +3002,20 @@ private void StepRunLoop_Core(bool force = false) // BlockFrameAdvance (true when input it being editted in TAStudio) supercedes all other frame advance conditions if ((runFrame || force) && !BlockFrameAdvance) { + _invisibleEmulation = _invisibleEmulateNextFrame; + var isFastForwarding = IsFastForwarding; var isFastForwardingOrRewinding = isFastForwarding || isRewinding || Config.Unthrottled; - if (isFastForwardingOrRewinding != _lastFastForwardingOrRewinding) + if (!_invisibleEmulation) { - InitializeFpsData(); - } + if (isFastForwardingOrRewinding != _lastFastForwardingOrRewinding) + { + InitializeFpsData(); + } - _lastFastForwardingOrRewinding = isFastForwardingOrRewinding; + _lastFastForwardingOrRewinding = isFastForwardingOrRewinding; + } // client input-related duties OSD.ClearGuiText(); @@ -2997,13 +3035,13 @@ private void StepRunLoop_Core(bool force = false) Tools.UpdateToolsBefore(); } - if (!InvisibleEmulation) + if (!_invisibleEmulation) { CaptureRewind(isRewinding); } // Set volume, if enabled - if (Config.SoundEnabledNormal && !InvisibleEmulation) + if (Config.SoundEnabledNormal && !_invisibleEmulation) { atten = Config.SoundVolume / 100.0f; @@ -3030,7 +3068,7 @@ private void StepRunLoop_Core(bool force = false) RA?.OnFrameAdvance(); - if (Config.AutosaveSaveRAM) + if (Config.AutosaveSaveRAM && !_invisibleEmulation) { AutoFlushSaveRamIn--; if (AutoFlushSaveRamIn <= 0) @@ -3048,10 +3086,10 @@ private void StepRunLoop_Core(bool force = false) } bool atTurboSeekEnd = IsTurboSeeking && Emulator.Frame == PauseOnFrame.Value - 1; - bool render = !InvisibleEmulation && (!_throttle.skipNextFrame || _currAviWriter?.UsesVideo is true || atTurboSeekEnd); + bool render = !_invisibleEmulation && (!_throttle.skipNextFrame || _currAviWriter?.UsesVideo is true || atTurboSeekEnd); bool newFrame = Emulator.FrameAdvance(InputManager.ControllerOutput, render, renderSound); - MovieSession.HandleFrameAfter(ToolBypassingMovieEndAction is not null); + if (!_invisibleUpdate) MovieSession.HandleFrameAfter(ToolBypassingMovieEndAction is not null); if (returnToRecording) { @@ -3088,13 +3126,14 @@ private void StepRunLoop_Core(bool force = false) UpdateToolsAfter(); } } + _invisibleUpdate = _invisibleEmulation; - if (!PauseAvi && newFrame && !InvisibleEmulation) + if (!PauseAvi && newFrame && !_invisibleEmulation) { AvFrameAdvance(); } - if (newFrame) + if (newFrame && !_invisibleEmulation) { _framesSinceLastFpsUpdate++; @@ -4375,6 +4414,8 @@ private bool Rewind(ref bool runFrame, long currentTimestamp, out bool returnToR returnToRecording = false; + if (_invisibleEmulation) return false; + if (ToolControllingRewind is { } rewindTool) { if (InputManager.ClientControls["Rewind"] || PressRewind) diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegration.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegration.cs index e53479612f3..f1e4e7f80f6 100644 --- a/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegration.cs +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RAIntegration.cs @@ -137,7 +137,7 @@ public RAIntegration( clientVer: $"{VersionInfo.MainVersion}{(VersionInfo.DeveloperBuild ? "-dev" : string.Empty)}"); _isActive = () => !Emu.IsNull(); - _unpause = _mainForm.UnpauseEmulator; + _unpause = () => _ = _mainForm.UnpauseEmulator(); _pause = _mainForm.PauseEmulator; _rebuildMenu = RebuildMenu; _estimateTitle = buffer => diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.IToolForm.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.IToolForm.cs index 062edf5707e..efe2ae8510a 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.IToolForm.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.IToolForm.cs @@ -71,7 +71,7 @@ protected override void GeneralUpdate() protected override void UpdateAfter() { - if (!IsHandleCreated || IsDisposed || CurrentTasMovie == null) + if (!IsHandleCreated || IsDisposed || CurrentTasMovie == null || MainForm.InvisibleEmulation) { return; } @@ -109,6 +109,8 @@ protected override void UpdateAfter() protected override void FastUpdateAfter() { + if (MainForm.InvisibleEmulation) return; + if (_seekingTo != -1 && Emulator.Frame >= _seekingTo) { bool smga = _shouldMoveGreenArrow; diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.Navigation.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.Navigation.cs index d2e5c92abfc..41ad1abef2b 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.Navigation.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.Navigation.cs @@ -17,11 +17,6 @@ public void GoToFrame(int frame, bool OnLeftMouseDown = false, bool skipLoadStat return; } - // Unpausing after a seek may seem like we aren't really seeking at all: - // what is the significance of a seek to frame if we don't pause? - // Answer: We use this in order to temporarily disable recording mode when the user navigates to a frame. (to avoid recording between whatever is the most recent state and the user-specified frame) - // Other answer: turbo seek, navigating while unpaused - _pauseAfterSeeking = MainForm.EmulatorPaused || (_seekingTo != -1 && _pauseAfterSeeking); WasRecording = CurrentTasMovie.IsRecording() || WasRecording; TastudioPlayMode(); @@ -37,9 +32,13 @@ public void GoToFrame(int frame, bool OnLeftMouseDown = false, bool skipLoadStat _seekStartFrame = Emulator.Frame; _seekingByEdit = false; + // Unpausing after a seek may seem like we aren't really seeking at all: + // what is the significance of a seek to frame if we don't pause? + // Answer: We use this in order to temporarily disable recording mode when the user navigates to a frame. (to avoid recording between whatever is the most recent state and the user-specified frame) + // Other answer: turbo seek, navigating while unpaused + _pauseAfterSeeking = MainForm.UnpauseEmulator() || (_seekingTo != -1 && _pauseAfterSeeking); _seekingTo = frame; MainForm.PauseOnFrame = int.MaxValue; // This being set is how MainForm knows we are seeking, and controls TurboSeek. - MainForm.UnpauseEmulator(); if (_seekingTo - _seekStartFrame > 1) {