From bc6aecaa5239ed5b18d84c5f192702411719f793 Mon Sep 17 00:00:00 2001 From: SuuperW Date: Tue, 16 Jun 2026 20:44:57 -0500 Subject: [PATCH 1/5] make reserving states on branch frames optional --- .../movie/interfaces/ITasMovie.cs | 2 ++ .../movie/tasproj/TasMovie.cs | 22 ++++++++++++++++++- .../tools/TAStudio/TAStudio.MenuItems.cs | 2 ++ .../tools/TAStudio/TAStudio.cs | 3 +++ .../TAStudio/TAStudioSettingsForm.Designer.cs | 14 ++++++++++++ .../tools/TAStudio/TAStudioSettingsForm.cs | 2 ++ .../tools/TAStudio/TAStudioSettingsForm.resx | 4 ++++ 7 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/BizHawk.Client.Common/movie/interfaces/ITasMovie.cs b/src/BizHawk.Client.Common/movie/interfaces/ITasMovie.cs index dd7ab7254a5..020c06d72c0 100644 --- a/src/BizHawk.Client.Common/movie/interfaces/ITasMovie.cs +++ b/src/BizHawk.Client.Common/movie/interfaces/ITasMovie.cs @@ -20,6 +20,8 @@ public interface ITasMovie : IMovie, IDisposable int LastEditedFrame { get; } bool LastEditWasRecording { get; } + bool ReserveBranchFrames { get; set; } + /// /// Called whenever the movie is modified in a way that could invalidate savestates in the movie's state history. /// Called regardless of whether any states were actually invalidated. diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs index a20e6eae643..ecb38406693 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs @@ -84,6 +84,26 @@ public override bool StartsFromSavestate public Action GreenzoneInvalidated { get; set; } + private bool _reserveBranchFrames; + public bool ReserveBranchFrames + { + get => _reserveBranchFrames; + set + { + _reserveBranchFrames = value; + if (!value) + { + foreach (TasBranch branch in Branches) + { + if (!IsReserved(branch.Frame)) + { + TasStateManager.Unreserve(branch.Frame); + } + } + } + } + } + public ITasMovieRecord this[int index] { get @@ -327,7 +347,7 @@ public bool IsReserved(int frame) // because we always navigate to the frame before and emulate 1 frame so that we ensure a proper frame buffer on the screen // users want instant navigation to markers, so to do this, we need to reserve the frame before the marker, not the marker itself return Markers.Exists(m => m.WantsState && m.Frame - 1 == frame) - || Branches.Any(b => b.Frame == frame); // Branches should already be in the reserved list, but it doesn't hurt to check + || (ReserveBranchFrames && Branches.Any(b => b.Frame == frame)); } public void Dispose() diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs index d614d22f26e..11cdc48d273 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs @@ -1098,6 +1098,8 @@ private void TAStudioSettingsToolStripMenuItem_Click(object sender, EventArgs e) _inputRolls[i].ScrollSpeed = Settings.ScrollSpeed; } + CurrentTasMovie.ReserveBranchFrames = Settings.StateOnBranchFrame; + UpdateAutoFire(); if (CurrentTasMovie.TasStateManager.Settings != s.CurrentStateManagerSettings) diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs index f33f7e42028..cb500f56517 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs @@ -105,6 +105,7 @@ public TAStudioSettings() LoadBranchOnDoubleClick = true; CopyIncludesFrameNo = false; AutoadjustInput = false; + StateOnBranchFrame = false; // default to taseditor fashion DenoteStatesWithIcons = false; @@ -144,6 +145,7 @@ public TAStudioSettings() public int RewindStepFast { get; set; } = 4; public bool ScrollSync { get; set; } = true; public bool StatesForMarkers { get; set; } = true; + public bool StateOnBranchFrame { get; set; } public PatternPaintModeEnum PatternPaintMode { get; set; } = TAStudioSettings.PatternPaintModeEnum.Never; public PatternSelectionEnum PatternSelection { get; set; } = TAStudioSettings.PatternSelectionEnum.Hold; public Font TasViewFont { get; set; } = new Font("Arial", 8.25F, FontStyle.Bold, GraphicsUnit.Point, 0); @@ -711,6 +713,7 @@ private bool StartNewMovieWrapper(ITasMovie movie, bool isNew) movie.BindMarkersToInput = Settings.BindMarkersToInput; movie.GreenzoneInvalidated = (f) => _ = FrameEdited(f); movie.ChangeLog.MaxSteps = Settings.MaxUndoSteps; + movie.ReserveBranchFrames = Settings.StateOnBranchFrame; movie.ChangesChanged += TasMovie_OnChangesChanged; System.Collections.Specialized.NotifyCollectionChangedEventHandler refreshOnMarker = (_, _) => RefreshDialog(); diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.Designer.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.Designer.cs index 76293a442e0..d9862bb06a3 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.Designer.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.Designer.cs @@ -104,6 +104,7 @@ private void InitializeComponent() this.ScrollSpeedNum = new System.Windows.Forms.NumericUpDown(); this.label2 = new System.Windows.Forms.Label(); this.toolTip1 = new System.Windows.Forms.ToolTip(this.components); + this.StateOnBranchFrameCheckbox = new System.Windows.Forms.CheckBox(); this.EditInvisibleColumnsCheckbox = new System.Windows.Forms.CheckBox(); this.ScrollSyncCheckbox = new System.Windows.Forms.CheckBox(); this.StatesForMarkersCheckbox = new System.Windows.Forms.CheckBox(); @@ -734,6 +735,7 @@ private void InitializeComponent() // tabPage3 // this.tabPage3.Controls.Add(this.StatesForMarkersCheckbox); + this.tabPage3.Controls.Add(this.StateOnBranchFrameCheckbox); this.tabPage3.Controls.Add(this.OldBranchesCheckbox); this.tabPage3.Controls.Add(this.BranchDoubleClickCheckbox); this.tabPage3.Controls.Add(this.FastRewindNum); @@ -926,6 +928,17 @@ private void InitializeComponent() this.toolTip1.SetToolTip(this.StatesForMarkersCheckbox, resources.GetString("StatesForMarkersCheckbox.ToolTip")); this.StatesForMarkersCheckbox.UseVisualStyleBackColor = true; // + // StateOnBranchFrameCheckbox + // + this.StateOnBranchFrameCheckbox.AutoSize = true; + this.StateOnBranchFrameCheckbox.Location = new System.Drawing.Point(12, 223); + this.StateOnBranchFrameCheckbox.Name = "StateOnBranchFrameCheckbox"; + this.StateOnBranchFrameCheckbox.Size = new System.Drawing.Size(168, 17); + this.StateOnBranchFrameCheckbox.TabIndex = 518; + this.StateOnBranchFrameCheckbox.Text = "Keep a state on branch fames"; + this.toolTip1.SetToolTip(this.StateOnBranchFrameCheckbox, resources.GetString("StateOnBranchFrameCheckbox.ToolTip")); + this.StateOnBranchFrameCheckbox.UseVisualStyleBackColor = true; + // // tabPage6 // this.tabPage6.Controls.Add(this.ScrollSyncCheckbox); @@ -1185,5 +1198,6 @@ private void InitializeComponent() private System.Windows.Forms.NumericUpDown ScrollSpeedNum; private System.Windows.Forms.Label label2; private System.Windows.Forms.CheckBox StatesForMarkersCheckbox; + private System.Windows.Forms.CheckBox StateOnBranchFrameCheckbox; } } \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.cs index 5ba583c9192..bef26207775 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.cs @@ -102,6 +102,7 @@ private void TAStudioSettingsForm_Load(object sender, EventArgs e) FastRewindNum.Value = _settings.GeneralClientSettings.RewindStepFast; ScrollSpeedNum.Value = _settings.GeneralClientSettings.ScrollSpeed; StatesForMarkersCheckbox.Checked = _settings.GeneralClientSettings.StatesForMarkers; + StateOnBranchFrameCheckbox.Checked = _settings.GeneralClientSettings.StateOnBranchFrame; // patterns foreach (var button in _controllerDef.BoolButtons) @@ -504,6 +505,7 @@ private void ApplyButton_Click(object sender, EventArgs e) _settings.GeneralClientSettings.RewindStep = (int)RewindNum.Value; _settings.GeneralClientSettings.RewindStepFast = (int)FastRewindNum.Value; _settings.GeneralClientSettings.StatesForMarkers = StatesForMarkersCheckbox.Checked; + _settings.GeneralClientSettings.StateOnBranchFrame = StateOnBranchFrameCheckbox.Checked; if (ScrollToViewRadio.Checked) _settings.GeneralClientSettings.FollowCursorScrollMethod = "near"; else if (ScrollToTopRadio.Checked) _settings.GeneralClientSettings.FollowCursorScrollMethod = "top"; diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.resx b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.resx index bdf8ac85214..941205f6a8b 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.resx +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.resx @@ -131,6 +131,10 @@ Note: The savestate needs to be on the frame before the marker. Thus, making a marker on the current frame will not automatically create a state. (The reason for the state being on the frame before is so that TAStudio does not need to store a screenshot with the savestate.) + + + When enabled, TAStudio will keep a savestate on every frame for which a branch exists, ensuring you can +always quickly seek to 1 frame after said frame. This is separate from the branch's own savestate. When disabled, inputs that are not visible on the active input roll cannot be edited. From 3abaf283ddd4433ebec4c854491cebc72c20ed21 Mon Sep 17 00:00:00 2001 From: SuuperW Date: Mon, 29 Jun 2026 15:36:10 -0500 Subject: [PATCH 2/5] allow old state manager to capture on branch load when StateOnBranchFrame is false --- .../movie/tasproj/IStateManager.cs | 23 ++++++++++++++++--- .../movie/tasproj/TasMovie.cs | 5 +++- .../movie/tasproj/ZwinderStateManager.cs | 6 ++--- .../tools/TAStudio/PagedStateManager.cs | 8 +++---- .../tools/TAStudio/TAStudio.ListView.cs | 4 ++-- .../tools/TAStudio/TAStudio.cs | 7 +++++- .../Movie/FakeStateManager.cs | 2 +- .../Movie/PagedStateManagerTests.cs | 8 +++---- 8 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/BizHawk.Client.Common/movie/tasproj/IStateManager.cs b/src/BizHawk.Client.Common/movie/tasproj/IStateManager.cs index 2c2890962cc..4d6b5aca874 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/IStateManager.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/IStateManager.cs @@ -6,14 +6,31 @@ namespace BizHawk.Client.Common { public interface IStateManager : IDisposable { + public enum CaptureType + { + /// + /// Let the manager decide if capturing is appropriate and when the state should be removed. + /// + Normal, + + /// + /// Force the state to be captured, but allow this state to be removed as soon as the next capture happens. + /// + LastEditedFrame, + + /// + /// Force the state to be captured as a reserved state. + /// + Reserve, + } + IStateManagerSettings Settings { get; } /// /// Requests that the current emulator state be captured - /// Unless force is true, the state may or may not be captured depending on the logic employed by "green-zone" management + /// With , the state may or may not be captured depending on the logic employed by "green-zone" management /// - /// If true, the state will be temporarily captured. If it would not have otherwise been captured, it may be deleted as soon as another state is force captured. - void Capture(int frame, IStatable source, bool force = false); + void Capture(int frame, IStatable source, CaptureType type = CaptureType.Normal); /// /// Tell the state manager we no longer wish to reserve the state for the given frame. diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs index ecb38406693..99589befe38 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs @@ -211,7 +211,10 @@ public void GreenzoneCurrentFrame() LagLog[Emulator.Frame] = _inputPollable.IsLagFrame; // We will forcibly capture a state for the last edited frame (requested by https://github.com/TASEmulators/BizHawk/issues/916 for case of "platforms with analog stick") - TasStateManager.Capture(Emulator.Frame, Emulator.AsStatable(), Emulator.Frame == LastEditedFrame - 1); + TasStateManager.Capture( + Emulator.Frame, + Emulator.AsStatable(), + Emulator.Frame == LastEditedFrame - 1 ? IStateManager.CaptureType.LastEditedFrame : IStateManager.CaptureType.Normal); } diff --git a/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs b/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs index 1220ec91616..2724062a002 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs @@ -355,7 +355,7 @@ private bool ShouldKeepForAncient(int frame) return nextState - previousState > _ancientInterval; } - public void Capture(int frame, IStatable source, bool force = false) + public void Capture(int frame, IStatable source, IStateManager.CaptureType type = IStateManager.CaptureType.Normal) { // We already have this state, no need to capture if (StateCache.Contains(frame)) @@ -363,7 +363,7 @@ public void Capture(int frame, IStatable source, bool force = false) return; } - if (_reserveCallback(frame)) + if (type == IStateManager.CaptureType.Reserve || _reserveCallback(frame)) { CaptureReserved(frame, source); return; @@ -376,7 +376,7 @@ public void Capture(int frame, IStatable source, bool force = false) } // We use the gap buffer for forced capture to avoid crowding the "current" buffer and thus reducing it's actual span of covered frames. - if (NeedsGap(frame) || force) + if (NeedsGap(frame) || type == IStateManager.CaptureType.LastEditedFrame) { CaptureGap(frame, source); return; diff --git a/src/BizHawk.Client.Common/tools/TAStudio/PagedStateManager.cs b/src/BizHawk.Client.Common/tools/TAStudio/PagedStateManager.cs index 1a680926aac..4e0146a345a 100644 --- a/src/BizHawk.Client.Common/tools/TAStudio/PagedStateManager.cs +++ b/src/BizHawk.Client.Common/tools/TAStudio/PagedStateManager.cs @@ -468,13 +468,13 @@ private void InternalCapture(int frame, IStatable source, StateGroup destination } } - public void Capture(int frame, IStatable source, bool force = false) + public void Capture(int frame, IStatable source, IStateManager.CaptureType type = IStateManager.CaptureType.Normal) { Debug.Assert(_states.Contains(new(0)), "State manager cannot be used until engaged."); if (HasState(frame)) return; - if (force) + if (type == IStateManager.CaptureType.LastEditedFrame) { if (HasState(_lastForceCapture)) { @@ -486,7 +486,7 @@ public void Capture(int frame, IStatable source, bool force = false) } StateGroup group = StateGroup.None; - if (_reserveCallback(frame)) + if (type == IStateManager.CaptureType.Reserve || _reserveCallback(frame)) { group = StateGroup.Old; } @@ -542,7 +542,7 @@ public void Capture(int frame, IStatable source, bool force = false) if (group != StateGroup.None) InternalCapture(frame, source, group); - else if (force) + else if (type == IStateManager.CaptureType.LastEditedFrame) { _lastForceCapture = frame; InternalCapture(frame, source, StateGroup.Old); diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.ListView.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.ListView.cs index 6dc89295a1f..ec1855d5da0 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.ListView.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.ListView.cs @@ -1089,12 +1089,12 @@ public bool FrameEdited(int frame) { // In this case our regular capture logic won't get the chance // to do a force capture for this edited frame. So do it here. - CurrentTasMovie.TasStateManager.Capture(Emulator.Frame, Emulator.AsStatable(), true); + CurrentTasMovie.TasStateManager.Capture(Emulator.Frame, Emulator.AsStatable(), IStateManager.CaptureType.LastEditedFrame); } else if (!CurrentTasMovie.TasStateManager.HasState(frame - 1) && Emulator.Frame == frame) { // A less-than-ideal frame to be captured, but still useful for autorestore. - CurrentTasMovie.TasStateManager.Capture(Emulator.Frame, Emulator.AsStatable(), true); + CurrentTasMovie.TasStateManager.Capture(Emulator.Frame, Emulator.AsStatable(), IStateManager.CaptureType.LastEditedFrame); } _batchEditMinFrame = -1; } diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs index cb500f56517..a36cbecbec7 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs @@ -1338,7 +1338,12 @@ public void LoadBranch(TasBranch branch) _suspendEditLogic = false; LoadState(new(branch.Frame, new MemoryStream(branch.CoreData, false)), CurrentTasMovie.Branches.IndexOf(branch)); - CurrentTasMovie.TasStateManager.Capture(Emulator.Frame, Emulator.AsStatable()); + // We capture this state to ensure edits at or after the branch frame will not require seeking from prior greenzone + // (which might be very far back). Zwinder manager requires it be reserved since it cannot capture out of order. + // There may be benefits to reserve capture with Paged manager too. + // (Note: IsReserved may say it is not reserved, if StateOnBranchFrame is false. + // We won't need it reserved on re-greenzoning, but we still want to capture it here.) + CurrentTasMovie.TasStateManager.Capture(Emulator.Frame, Emulator.AsStatable(), IStateManager.CaptureType.Reserve); QuickBmpFile.Copy(new BitmapBufferVideoProvider(branch.CoreFrameBuffer), VideoProvider); if (Settings.OldControlSchemeForBranches && TasPlaybackBox.RecordingMode) diff --git a/src/BizHawk.Tests.Client.Common/Movie/FakeStateManager.cs b/src/BizHawk.Tests.Client.Common/Movie/FakeStateManager.cs index de63c5c9292..390bc5717bf 100644 --- a/src/BizHawk.Tests.Client.Common/Movie/FakeStateManager.cs +++ b/src/BizHawk.Tests.Client.Common/Movie/FakeStateManager.cs @@ -14,7 +14,7 @@ internal class FakeStateManager : IStateManager public int Last => throw new NotImplementedException(); - public void Capture(int frame, IStatable source, bool force) => throw new NotImplementedException(); + public void Capture(int frame, IStatable source, IStateManager.CaptureType force = IStateManager.CaptureType.Normal) => throw new NotImplementedException(); public void Clear() => throw new NotImplementedException(); public void Dispose() => throw new NotImplementedException(); public void Engage(byte[] frameZeroState) { /* nothing */ } diff --git a/src/BizHawk.Tests.Client.Common/Movie/PagedStateManagerTests.cs b/src/BizHawk.Tests.Client.Common/Movie/PagedStateManagerTests.cs index ddb92f1040f..ecf100f72f4 100644 --- a/src/BizHawk.Tests.Client.Common/Movie/PagedStateManagerTests.cs +++ b/src/BizHawk.Tests.Client.Common/Movie/PagedStateManagerTests.cs @@ -275,7 +275,7 @@ public void TestKeepsAtLeastAncientInterval() { // Load branch with frame number almost at two old intervals int branchFrame = manager.Settings.FramesBetweenOldStates * 2 - 1; - manager.Capture(branchFrame, ss, true); + manager.Capture(branchFrame, ss, IStateManager.CaptureType.Reserve); // Rewind to frame 0, play far enough that it will kick states before the branch. for (int i = 0; i < 2000; i++) manager.Capture(i, ss); @@ -514,10 +514,10 @@ public void ForceCapturesAreTemporary() for (int i = 1; i <= lastNonForce; i++) manager.Capture(i, ss); - manager.Capture(lastNonForce + 1, ss, true); + manager.Capture(lastNonForce + 1, ss, IStateManager.CaptureType.LastEditedFrame); // act - manager.Capture(lastNonForce + 2, ss, true); + manager.Capture(lastNonForce + 2, ss, IStateManager.CaptureType.LastEditedFrame); // assert Assert.IsFalse(manager.HasState(lastNonForce + 1)); @@ -538,7 +538,7 @@ public void ForceCaptures() manager.Engage(ss.CloneSavestate()); // act - manager.Capture(1, ss, true); + manager.Capture(1, ss, IStateManager.CaptureType.LastEditedFrame); // assert Assert.IsTrue(manager.HasState(1)); From 78fe9e4c6d380c51acc244668046041de7ae7f52 Mon Sep 17 00:00:00 2001 From: SuuperW Date: Mon, 29 Jun 2026 16:42:44 -0500 Subject: [PATCH 3/5] change branch greenzone states to be on the frame before the branch, so they can fulfill their supposed purpose of allowing quick seeks to branch frames --- src/BizHawk.Client.Common/movie/tasproj/TasBranch.cs | 8 ++++---- src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs | 6 +++--- .../tools/TAStudio/BookmarksBranchesBox.cs | 2 -- src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs | 3 +-- .../tools/TAStudio/TAStudioSettingsForm.Designer.cs | 2 +- .../tools/TAStudio/TAStudioSettingsForm.resx | 5 +++-- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasBranch.cs b/src/BizHawk.Client.Common/movie/tasproj/TasBranch.cs index 7cd4d243510..c38dc4867e6 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/TasBranch.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/TasBranch.cs @@ -87,8 +87,8 @@ public void Replace(TasBranch old, TasBranch newBranch) newBranch.Uuid = old.Uuid; if (newBranch.UserText.Length is 0) newBranch.UserText = old.UserText; this[index] = newBranch; - if (!_movie.IsReserved(old.Frame)) - _movie.TasStateManager.Unreserve(old.Frame); + if (!_movie.IsReserved(old.Frame - 1)) + _movie.TasStateManager.Unreserve(old.Frame - 1); _movie.FlagChanges(); } @@ -121,8 +121,8 @@ public void Replace(TasBranch old, TasBranch newBranch) var result = base.Remove(item); if (result) { - if (!_movie.IsReserved(item!.Frame)) - _movie.TasStateManager.Unreserve(item.Frame); + if (!_movie.IsReserved(item!.Frame - 1)) + _movie.TasStateManager.Unreserve(item.Frame - 1); _movie.FlagChanges(); } diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs index 99589befe38..2299f7a86a0 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs @@ -95,9 +95,9 @@ public bool ReserveBranchFrames { foreach (TasBranch branch in Branches) { - if (!IsReserved(branch.Frame)) + if (!IsReserved(branch.Frame - 1)) { - TasStateManager.Unreserve(branch.Frame); + TasStateManager.Unreserve(branch.Frame - 1); } } } @@ -350,7 +350,7 @@ public bool IsReserved(int frame) // because we always navigate to the frame before and emulate 1 frame so that we ensure a proper frame buffer on the screen // users want instant navigation to markers, so to do this, we need to reserve the frame before the marker, not the marker itself return Markers.Exists(m => m.WantsState && m.Frame - 1 == frame) - || (ReserveBranchFrames && Branches.Any(b => b.Frame == frame)); + || (ReserveBranchFrames && Branches.Any(b => b.Frame - 1 == frame)); } public void Dispose() diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/BookmarksBranchesBox.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/BookmarksBranchesBox.cs index 947a77723bc..6dcd76c1649 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/BookmarksBranchesBox.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/BookmarksBranchesBox.cs @@ -172,7 +172,6 @@ public void Branch() BranchView.RowCount = Branches.Count; Branches.Current = Branches.Count - 1; Movie.TasSession.UpdateValues(Tastudio.Emulator.Frame, Branches.Current); - Movie.TasStateManager.Capture(Tastudio.Emulator.Frame, new BufferedStatable(branch.CoreData)); BranchView.ScrollToIndex(Branches.Current); BranchView.DeselectAll(); Select(Branches.Current, true); @@ -281,7 +280,6 @@ private void UpdateBranchToolStripMenuItem_Click(object sender, EventArgs e) BranchView.ScrollToIndex(Branches.Current); var branch = CreateBranch(); Branches.Replace(SelectedBranch, branch); - Movie.TasStateManager.Capture(Tastudio.Emulator.Frame, new BufferedStatable(branch.CoreData)); Tastudio.RefreshDialog(); Tastudio.BranchSavedCallback?.Invoke(Branches.Current); Tastudio.MainForm.AddOnScreenMessage($"Saved branch {Branches.Current + 1}"); diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs index a36cbecbec7..ff915a8211d 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs @@ -1341,8 +1341,7 @@ public void LoadBranch(TasBranch branch) // We capture this state to ensure edits at or after the branch frame will not require seeking from prior greenzone // (which might be very far back). Zwinder manager requires it be reserved since it cannot capture out of order. // There may be benefits to reserve capture with Paged manager too. - // (Note: IsReserved may say it is not reserved, if StateOnBranchFrame is false. - // We won't need it reserved on re-greenzoning, but we still want to capture it here.) + // (Note: IsReserved will say not reserved. We won't need it reserved on re-greenzoning, but we still want to capture it here.) CurrentTasMovie.TasStateManager.Capture(Emulator.Frame, Emulator.AsStatable(), IStateManager.CaptureType.Reserve); QuickBmpFile.Copy(new BitmapBufferVideoProvider(branch.CoreFrameBuffer), VideoProvider); diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.Designer.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.Designer.cs index d9862bb06a3..91b31323b5d 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.Designer.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.Designer.cs @@ -935,7 +935,7 @@ private void InitializeComponent() this.StateOnBranchFrameCheckbox.Name = "StateOnBranchFrameCheckbox"; this.StateOnBranchFrameCheckbox.Size = new System.Drawing.Size(168, 17); this.StateOnBranchFrameCheckbox.TabIndex = 518; - this.StateOnBranchFrameCheckbox.Text = "Keep a state on branch fames"; + this.StateOnBranchFrameCheckbox.Text = "Enable \"jump to branch frame\" states"; this.toolTip1.SetToolTip(this.StateOnBranchFrameCheckbox, resources.GetString("StateOnBranchFrameCheckbox.ToolTip")); this.StateOnBranchFrameCheckbox.UseVisualStyleBackColor = true; // diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.resx b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.resx index 941205f6a8b..6918de801e2 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.resx +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudioSettingsForm.resx @@ -133,8 +133,9 @@ a marker on the current frame will not automatically create a state. does not need to store a screenshot with the savestate.) - When enabled, TAStudio will keep a savestate on every frame for which a branch exists, ensuring you can -always quickly seek to 1 frame after said frame. This is separate from the branch's own savestate. + When enabled, TAStudio will keep savestates for each branch the same way +it does for markers, allowing you to quickly seek to a branch frame. +This is separate from the savestate that's loaded when you load a branch. When disabled, inputs that are not visible on the active input roll cannot be edited. From cfe71d80e4415ef379321c295e8199039e96a5ac Mon Sep 17 00:00:00 2001 From: SuuperW Date: Wed, 8 Jul 2026 18:55:40 -0500 Subject: [PATCH 4/5] prevent "leaking" semi-reserved branch frame states --- .../movie/tasproj/TasBranch.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasBranch.cs b/src/BizHawk.Client.Common/movie/tasproj/TasBranch.cs index c38dc4867e6..8b054751529 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/TasBranch.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/TasBranch.cs @@ -87,8 +87,7 @@ public void Replace(TasBranch old, TasBranch newBranch) newBranch.Uuid = old.Uuid; if (newBranch.UserText.Length is 0) newBranch.UserText = old.UserText; this[index] = newBranch; - if (!_movie.IsReserved(old.Frame - 1)) - _movie.TasStateManager.Unreserve(old.Frame - 1); + DoUnreserve(old); _movie.FlagChanges(); } @@ -121,8 +120,7 @@ public void Replace(TasBranch old, TasBranch newBranch) var result = base.Remove(item); if (result) { - if (!_movie.IsReserved(item!.Frame - 1)) - _movie.TasStateManager.Unreserve(item.Frame - 1); + DoUnreserve(item); _movie.FlagChanges(); } @@ -277,6 +275,16 @@ public void Load(ZipStateLoader bl, ITasMovie movie) nusertext.Increment(); } } + + private void DoUnreserve(TasBranch branch) + { + // The regularly reserved frame + if (!_movie.IsReserved(branch.Frame - 1)) + _movie.TasStateManager.Unreserve(branch.Frame - 1); + // The potentially semi-reserved frame from when a branch was loaded + if (!_movie.IsReserved(branch.Frame)) + _movie.TasStateManager.Unreserve(branch.Frame); + } } public static class TasBranchExtensions From dab0d0826301129d25ee7028bb92975006d0a9a2 Mon Sep 17 00:00:00 2001 From: SuuperW Date: Wed, 8 Jul 2026 19:30:12 -0500 Subject: [PATCH 5/5] delay removing states until the space is needed in some situations, to help avoid potential premature removal of states --- .../tools/TAStudio/PagedStateManager.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/BizHawk.Client.Common/tools/TAStudio/PagedStateManager.cs b/src/BizHawk.Client.Common/tools/TAStudio/PagedStateManager.cs index 4e0146a345a..b833f7d4199 100644 --- a/src/BizHawk.Client.Common/tools/TAStudio/PagedStateManager.cs +++ b/src/BizHawk.Client.Common/tools/TAStudio/PagedStateManager.cs @@ -161,6 +161,8 @@ public Stream MakeReadStream(PagedStateManager manager) private readonly SortedSet _midStates = new(); private readonly SortedSet _newStates = new(); + private readonly Queue _statesPendingRemoval = new(); + private readonly Func _reserveCallback; private bool _bufferIsFull = false; @@ -350,6 +352,16 @@ private int FreePage(int frame) _bufferIsFull = true; + while (_statesPendingRemoval.Count > 0) + { + StateInfo si = _statesPendingRemoval.Dequeue(); + if (_states.Contains(si)) + { + RemoveState(si); + return si.FirstPage; + } + } + while (true) { // A very special case: We have no mid or new states. @@ -480,7 +492,7 @@ public void Capture(int frame, IStatable source, IStateManager.CaptureType type { StateInfo state = _states.GetViewBetween(new(_lastForceCapture), new(_lastForceCapture)).Min; if (!_reserveCallback(_lastForceCapture) && !ShouldKeepForOld(_lastForceCapture)) - RemoveState(state); + _statesPendingRemoval.Enqueue(state); } _lastForceCapture = -1; // This will get set if the frame is actually captured because of force. } @@ -608,7 +620,7 @@ public void Unreserve(int frame) // Remove the state if it's an old state we don't need. if (!_newStates.Contains(state) && !_midStates.Contains(state) && !ShouldKeepForOld(frame)) { - RemoveState(state); + _statesPendingRemoval.Enqueue(state); } }