From 145f5be848a7f84757df3914605bbd83ead5c3ef Mon Sep 17 00:00:00 2001 From: SuuperW Date: Sun, 21 Dec 2025 14:21:42 -0600 Subject: [PATCH 1/8] refactor of some pause code --- src/BizHawk.Client.EmuHawk/MainForm.cs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 8264587aaf7..963c3ec1f12 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -1082,14 +1082,21 @@ 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 void OnPauseToggle(bool newPauseState) + { + if (!newPauseState) // Unpausing + { + InitializeFpsData(); } + + Tools.OnPauseToggle(newPauseState); + SetPauseStatusBarIcon(); } public bool BlockFrameAdvance { get; set; } @@ -1341,13 +1348,11 @@ public bool RebootCore() public void PauseEmulator() { EmulatorPaused = true; - SetPauseStatusBarIcon(); } public void UnpauseEmulator() { EmulatorPaused = false; - SetPauseStatusBarIcon(); } public void TogglePause() From e57b8f797cf89914e7f31a60910fd994ad4684ca Mon Sep 17 00:00:00 2001 From: SuuperW Date: Sun, 21 Dec 2025 14:27:59 -0600 Subject: [PATCH 2/8] Fix #4591 --- src/BizHawk.Client.EmuHawk/MainForm.cs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 963c3ec1f12..bb7696d8121 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -1076,7 +1076,7 @@ protected override void Dispose(bool disposing) private bool _emulatorPaused; public bool EmulatorPaused { - get => _emulatorPaused; + get => _emulatorPaused && !_unpauseByFrameAdvance; private set { @@ -1088,6 +1088,23 @@ private set } } + 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 @@ -2935,13 +2952,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 @@ -2949,7 +2967,7 @@ private void StepRunLoop_Core(bool force = false) if (_runloopFrameAdvance) { // handle release of frame advance - PauseEmulator(); + UnpauseByFrameAdvance = false; } _runloopFrameProgress = false; } From 22b963d5076765b49ac12330e9cbc1a39abd9401 Mon Sep 17 00:00:00 2001 From: SuuperW Date: Wed, 24 Dec 2025 01:05:17 -0600 Subject: [PATCH 3/8] Allow Lua to reliably restore pause state after a seek. --- src/BizHawk.Client.Common/Api/Classes/EmuClientApi.cs | 2 +- src/BizHawk.Client.Common/Api/Interfaces/IEmuClientApi.cs | 5 ++++- src/BizHawk.Client.Common/IMainFormForApi.cs | 2 +- src/BizHawk.Client.Common/lua/CommonLibs/ClientLuaLibrary.cs | 4 ++-- src/BizHawk.Client.EmuHawk/IMainFormForTools.cs | 2 +- src/BizHawk.Client.EmuHawk/MainForm.cs | 4 +++- .../RetroAchievements/RAIntegration.cs | 2 +- 7 files changed, 13 insertions(+), 8 deletions(-) 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..09b180314f8 100644 --- a/src/BizHawk.Client.Common/lua/CommonLibs/ClientLuaLibrary.cs +++ b/src/BizHawk.Client.Common/lua/CommonLibs/ClientLuaLibrary.cs @@ -304,8 +304,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 bb7696d8121..3e2ba8920ac 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -1367,9 +1367,11 @@ public void PauseEmulator() EmulatorPaused = true; } - public void UnpauseEmulator() + public bool UnpauseEmulator() { + bool ret = _emulatorPaused; EmulatorPaused = false; + return ret; } public void TogglePause() 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 => From 5e166762be68338787c7eefa9a54d2d8aec51c2d Mon Sep 17 00:00:00 2001 From: SuuperW Date: Wed, 24 Dec 2025 01:13:02 -0600 Subject: [PATCH 4/8] Make TAStudio use the new return value of UnpauseEmulator, restore pausing behavior. --- .../tools/TAStudio/TAStudio.Navigation.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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) { From 4807bbda2b91595d8aa62a3837b8e8b04268e684 Mon Sep 17 00:00:00 2001 From: SuuperW Date: Wed, 24 Dec 2025 01:19:30 -0600 Subject: [PATCH 5/8] Fix Lua script that used client.seekframe. --- Assets/Lua/GBA/SonicAdvance_CamHack.lua | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Assets/Lua/GBA/SonicAdvance_CamHack.lua b/Assets/Lua/GBA/SonicAdvance_CamHack.lua index c861f29c72d..188ae937053 100644 --- a/Assets/Lua/GBA/SonicAdvance_CamHack.lua +++ b/Assets/Lua/GBA/SonicAdvance_CamHack.lua @@ -6,6 +6,25 @@ local addr_offY = 0x5B98 local addr_camX = 0x59D0 local addr_camY = 0x59D2 +-- Seek forward to a given frame. +-- This will unpause emulation, and restore the users' desired pause state when seeking is completed. +local function 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 + while true do client.invisibleemulation(true) local memorystate = memorysavestate.savecorestate() @@ -21,9 +40,9 @@ while true do mainmemory.write_u16_le(addr_camX, Xval) mainmemory.write_u16_le(addr_camY, Yval) - client.seekframe(emu.framecount()+1) + seek_frame(emu.framecount()+1) client.invisibleemulation(false) - client.seekframe(emu.framecount()+1) + seek_frame(emu.framecount()+1) client.invisibleemulation(true) memorysavestate.loadcorestate(memorystate) memorysavestate.removestate(memorystate) From 28fddb7f9b5eeb85e182bac0a86eec63a843372e Mon Sep 17 00:00:00 2001 From: SuuperW Date: Fri, 26 Dec 2025 01:05:38 -0600 Subject: [PATCH 6/8] Make invisible emulation behave more as expected. --- Assets/Lua/GBA/SonicAdvance_CamHack.lua | 3 +- .../Api/Classes/EmuClientApi.cs | 2 +- src/BizHawk.Client.Common/IMainFormForApi.cs | 2 +- .../IMainFormForTools.cs | 2 +- src/BizHawk.Client.EmuHawk/MainForm.cs | 43 ++++++++++++------- .../tools/BasicBot/BasicBot.cs | 6 +-- 6 files changed, 35 insertions(+), 23 deletions(-) diff --git a/Assets/Lua/GBA/SonicAdvance_CamHack.lua b/Assets/Lua/GBA/SonicAdvance_CamHack.lua index 188ae937053..d125aba0da9 100644 --- a/Assets/Lua/GBA/SonicAdvance_CamHack.lua +++ b/Assets/Lua/GBA/SonicAdvance_CamHack.lua @@ -25,6 +25,8 @@ local function seek_frame(frame) if pause then client.pause() end end +event.onexit(function() client.invisibleemulation(false) end) + while true do client.invisibleemulation(true) local memorystate = memorysavestate.savecorestate() @@ -46,6 +48,5 @@ while true do client.invisibleemulation(true) memorysavestate.loadcorestate(memorystate) memorysavestate.removestate(memorystate) --- client.invisibleemulation(false) emu.frameadvance() end \ No newline at end of file diff --git a/src/BizHawk.Client.Common/Api/Classes/EmuClientApi.cs b/src/BizHawk.Client.Common/Api/Classes/EmuClientApi.cs index 000ea4f229b..70159863d77 100644 --- a/src/BizHawk.Client.Common/Api/Classes/EmuClientApi.cs +++ b/src/BizHawk.Client.Common/Api/Classes/EmuClientApi.cs @@ -135,7 +135,7 @@ public void FrameSkip(int numFrames) public int GetWindowSize() => _config.GetWindowScaleFor(Emulator.SystemId); - public void InvisibleEmulation(bool invisible) => _mainForm.InvisibleEmulation = invisible; + public void InvisibleEmulation(bool invisible) => _mainForm.InvisibleEmulateNextFrame = invisible; public bool IsPaused() => _mainForm.EmulatorPaused; diff --git a/src/BizHawk.Client.Common/IMainFormForApi.cs b/src/BizHawk.Client.Common/IMainFormForApi.cs index e964b5494c4..6ec9548d969 100644 --- a/src/BizHawk.Client.Common/IMainFormForApi.cs +++ b/src/BizHawk.Client.Common/IMainFormForApi.cs @@ -17,7 +17,7 @@ public interface IMainFormForApi bool EmulatorPaused { get; } /// only referenced from - bool InvisibleEmulation { get; set; } + bool InvisibleEmulateNextFrame { get; set; } /// only referenced from bool IsSeeking { get; } diff --git a/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs b/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs index 867b0210a90..993f4e6a74d 100644 --- a/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs +++ b/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs @@ -25,7 +25,7 @@ public interface IMainFormForTools : IDialogController bool HoldFrameAdvance { get; set; } /// only referenced from - bool InvisibleEmulation { get; set; } + bool InvisibleEmulateNextFrame { get; set; } /// only referenced from bool IsFastForwarding { get; } diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 3e2ba8920ac..9d71b26924a 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 @@ -1145,7 +1149,9 @@ private void OnPauseToggle(bool newPauseState) /// /// /// - public bool InvisibleEmulation { get; set; } + public bool InvisibleEmulateNextFrame { get; set; } + + private bool _invisibleEmulation; private long MouseWheelTracker; @@ -1170,7 +1176,7 @@ private void OnPauseToggle(bool newPauseState) 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 || _invisibleEmulation; public bool IsFastForwarding => InputManager.ClientControls["Fast Forward"] || IsTurboing; public bool IsRewinding { get; private set; } @@ -2976,11 +2982,11 @@ private void StepRunLoop_Core(bool force = 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; } @@ -2994,15 +3000,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(); @@ -3022,13 +3033,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; @@ -3073,7 +3084,7 @@ 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); @@ -3114,12 +3125,12 @@ private void StepRunLoop_Core(bool force = false) } } - if (!PauseAvi && newFrame && !InvisibleEmulation) + if (!PauseAvi && newFrame && !_invisibleEmulation) { AvFrameAdvance(); } - if (newFrame) + if (newFrame && !_invisibleEmulation) { _framesSinceLastFpsUpdate++; diff --git a/src/BizHawk.Client.EmuHawk/tools/BasicBot/BasicBot.cs b/src/BizHawk.Client.EmuHawk/tools/BasicBot/BasicBot.cs index b32b7e5c327..bbc781a6efc 100644 --- a/src/BizHawk.Client.EmuHawk/tools/BasicBot/BasicBot.cs +++ b/src/BizHawk.Client.EmuHawk/tools/BasicBot/BasicBot.cs @@ -1047,8 +1047,8 @@ private void StartBot() if (InvisibleEmulationCheckBox.Checked) { - _previousInvisibleEmulation = MainForm.InvisibleEmulation; - MainForm.InvisibleEmulation = true; + _previousInvisibleEmulation = MainForm.InvisibleEmulateNextFrame; + MainForm.InvisibleEmulateNextFrame = true; } UpdateBotStatusIcon(); @@ -1103,7 +1103,7 @@ private void StopBot() private void RestoreConfigFlags() { Config.DisplayMessages = _previousDisplayMessage; - MainForm.InvisibleEmulation = _previousInvisibleEmulation; + MainForm.InvisibleEmulateNextFrame = _previousInvisibleEmulation; var movie = MovieSession.Movie; if (movie.IsRecording()) movie.IsCountingRerecords = _oldCountingSetting; } From 246d29c2a643b1e68c5f6f48d239cac780d27fb2 Mon Sep 17 00:00:00 2001 From: SuuperW Date: Sun, 28 Dec 2025 01:41:29 -0600 Subject: [PATCH 7/8] Block more stuffs during invisible emulation, especially to make it work better with TAStudio. --- Assets/Lua/GBA/SonicAdvance_CamHack.lua | 23 ++-------------- Assets/Lua/seek.lua | 26 +++++++++++++++++++ .../Api/Classes/EmuClientApi.cs | 2 +- src/BizHawk.Client.Common/IMainFormForApi.cs | 2 +- .../IMainFormForTools.cs | 2 +- src/BizHawk.Client.EmuHawk/MainForm.cs | 15 +++++++---- .../tools/BasicBot/BasicBot.cs | 6 ++--- .../tools/TAStudio/TAStudio.IToolForm.cs | 4 ++- 8 files changed, 47 insertions(+), 33 deletions(-) create mode 100644 Assets/Lua/seek.lua diff --git a/Assets/Lua/GBA/SonicAdvance_CamHack.lua b/Assets/Lua/GBA/SonicAdvance_CamHack.lua index d125aba0da9..3710c7efff3 100644 --- a/Assets/Lua/GBA/SonicAdvance_CamHack.lua +++ b/Assets/Lua/GBA/SonicAdvance_CamHack.lua @@ -6,25 +6,6 @@ local addr_offY = 0x5B98 local addr_camX = 0x59D0 local addr_camY = 0x59D2 --- Seek forward to a given frame. --- This will unpause emulation, and restore the users' desired pause state when seeking is completed. -local function 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 - event.onexit(function() client.invisibleemulation(false) end) while true do @@ -42,9 +23,9 @@ while true do mainmemory.write_u16_le(addr_camX, Xval) mainmemory.write_u16_le(addr_camY, Yval) - seek_frame(emu.framecount()+1) + emu.frameadvance() client.invisibleemulation(false) - seek_frame(emu.framecount()+1) + emu.frameadvance() client.invisibleemulation(true) memorysavestate.loadcorestate(memorystate) memorysavestate.removestate(memorystate) 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 70159863d77..000ea4f229b 100644 --- a/src/BizHawk.Client.Common/Api/Classes/EmuClientApi.cs +++ b/src/BizHawk.Client.Common/Api/Classes/EmuClientApi.cs @@ -135,7 +135,7 @@ public void FrameSkip(int numFrames) public int GetWindowSize() => _config.GetWindowScaleFor(Emulator.SystemId); - public void InvisibleEmulation(bool invisible) => _mainForm.InvisibleEmulateNextFrame = invisible; + public void InvisibleEmulation(bool invisible) => _mainForm.InvisibleEmulation = invisible; public bool IsPaused() => _mainForm.EmulatorPaused; diff --git a/src/BizHawk.Client.Common/IMainFormForApi.cs b/src/BizHawk.Client.Common/IMainFormForApi.cs index 6ec9548d969..e964b5494c4 100644 --- a/src/BizHawk.Client.Common/IMainFormForApi.cs +++ b/src/BizHawk.Client.Common/IMainFormForApi.cs @@ -17,7 +17,7 @@ public interface IMainFormForApi bool EmulatorPaused { get; } /// only referenced from - bool InvisibleEmulateNextFrame { get; set; } + bool InvisibleEmulation { get; set; } /// only referenced from bool IsSeeking { get; } diff --git a/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs b/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs index 993f4e6a74d..867b0210a90 100644 --- a/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs +++ b/src/BizHawk.Client.EmuHawk/IMainFormForTools.cs @@ -25,7 +25,7 @@ public interface IMainFormForTools : IDialogController bool HoldFrameAdvance { get; set; } /// only referenced from - bool InvisibleEmulateNextFrame { get; set; } + bool InvisibleEmulation { get; set; } /// only referenced from bool IsFastForwarding { get; } diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 9d71b26924a..61b0a7a347e 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -1149,9 +1149,11 @@ private void OnPauseToggle(bool newPauseState) /// /// /// - public bool InvisibleEmulateNextFrame { get; set; } + public bool InvisibleEmulation { get => _invisibleUpdate; set => _invisibleEmulateNextFrame = value; } + private bool _invisibleEmulateNextFrame; private bool _invisibleEmulation; + private bool _invisibleUpdate; private long MouseWheelTracker; @@ -1176,7 +1178,7 @@ private void OnPauseToggle(bool newPauseState) 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; } @@ -3000,7 +3002,7 @@ 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; + _invisibleEmulation = _invisibleEmulateNextFrame; var isFastForwarding = IsFastForwarding; var isFastForwardingOrRewinding = isFastForwarding || isRewinding || Config.Unthrottled; @@ -3066,7 +3068,7 @@ private void StepRunLoop_Core(bool force = false) RA?.OnFrameAdvance(); - if (Config.AutosaveSaveRAM) + if (Config.AutosaveSaveRAM && !_invisibleEmulation) { AutoFlushSaveRamIn--; if (AutoFlushSaveRamIn <= 0) @@ -3087,7 +3089,7 @@ private void StepRunLoop_Core(bool force = false) 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) { @@ -3124,6 +3126,7 @@ private void StepRunLoop_Core(bool force = false) UpdateToolsAfter(); } } + _invisibleUpdate = _invisibleEmulation; if (!PauseAvi && newFrame && !_invisibleEmulation) { @@ -4411,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/tools/BasicBot/BasicBot.cs b/src/BizHawk.Client.EmuHawk/tools/BasicBot/BasicBot.cs index bbc781a6efc..b32b7e5c327 100644 --- a/src/BizHawk.Client.EmuHawk/tools/BasicBot/BasicBot.cs +++ b/src/BizHawk.Client.EmuHawk/tools/BasicBot/BasicBot.cs @@ -1047,8 +1047,8 @@ private void StartBot() if (InvisibleEmulationCheckBox.Checked) { - _previousInvisibleEmulation = MainForm.InvisibleEmulateNextFrame; - MainForm.InvisibleEmulateNextFrame = true; + _previousInvisibleEmulation = MainForm.InvisibleEmulation; + MainForm.InvisibleEmulation = true; } UpdateBotStatusIcon(); @@ -1103,7 +1103,7 @@ private void StopBot() private void RestoreConfigFlags() { Config.DisplayMessages = _previousDisplayMessage; - MainForm.InvisibleEmulateNextFrame = _previousInvisibleEmulation; + MainForm.InvisibleEmulation = _previousInvisibleEmulation; var movie = MovieSession.Movie; if (movie.IsRecording()) movie.IsCountingRerecords = _oldCountingSetting; } 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; From f3580298567f90c6eeb4e2b01a4c9d5987059c30 Mon Sep 17 00:00:00 2001 From: SuuperW Date: Sun, 28 Dec 2025 01:44:44 -0600 Subject: [PATCH 8/8] Update Lua documentation for invisible emulation. --- .../lua/CommonLibs/ClientLuaLibrary.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/BizHawk.Client.Common/lua/CommonLibs/ClientLuaLibrary.cs b/src/BizHawk.Client.Common/lua/CommonLibs/ClientLuaLibrary.cs index 09b180314f8..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);