Close the collapse's two dropped guarantees: latch applyMode against re-entrancy, pin its project once; correct three overclaiming doc lines
This commit is contained in:
+46
-16
@@ -26,6 +26,7 @@
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_CountTracks
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_GetPlayStateEx
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
@@ -37,6 +38,7 @@
|
||||
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
||||
#define REAPERAPI_WANT_UpdateArrange
|
||||
#define REAPERAPI_WANT_UpdateTimeline
|
||||
#define REAPERAPI_WANT_ValidatePtr2
|
||||
// Lane minting: item-side lane reads/writes assigning each item to its mode's lane.
|
||||
#define REAPERAPI_WANT_CountTrackMediaItems
|
||||
#define REAPERAPI_WANT_GetTrackMediaItem
|
||||
@@ -85,6 +87,19 @@ struct UiRefreshHold {
|
||||
UiRefreshHold& operator=(const UiRefreshHold&) = delete;
|
||||
};
|
||||
|
||||
// applyMode is reachable from the load tick, the actions and render_in_place, and its
|
||||
// TrackFX_SetOffline calls are assumed to pump the message loop — so any of them can
|
||||
// re-enter it mid-apply, planning against the same model the outer loop is iterating.
|
||||
// An inner restoreTrack consuming a snapshot the outer sits mid-park over leaves that
|
||||
// track flags-restored, chain-offline and snapshotless: refused forever. Fail closed.
|
||||
bool g_applying = false;
|
||||
struct ApplyLatch {
|
||||
ApplyLatch() { g_applying = true; }
|
||||
~ApplyLatch() { g_applying = false; }
|
||||
ApplyLatch(const ApplyLatch&) = delete;
|
||||
ApplyLatch& operator=(const ApplyLatch&) = delete;
|
||||
};
|
||||
|
||||
// C_LANESCOLLAPSED=2: render a tool-split track like a normal single-lane
|
||||
// track showing only the playing lane (SDK: 1=collapsed, 2=hidden-lanes-exist
|
||||
// but displays as non-fixed-lane).
|
||||
@@ -147,11 +162,6 @@ MediaTrack* resolve(const TrackByGuid& byGuid, const std::string& guid) {
|
||||
return it == byGuid.end() ? nullptr : it->second; // stale/deleted GUID — pruned by being skipped
|
||||
}
|
||||
|
||||
// Managed-lane application: LanePlayOps are keyed by the lane's DURABLE name but
|
||||
// C_LANEPLAYS:N by current ordinal, which renumbers on reorder — so every write
|
||||
// re-resolves durable key -> ordinal first, and a lane whose name lacks the
|
||||
// managed prefix never enters the map and so can never be driven.
|
||||
|
||||
// Lane `laneIdx`'s durable name (P_LANENAME:n) on `tr`, or empty if unnamed /
|
||||
// unavailable (non-fixed-lane track).
|
||||
std::string laneName(MediaTrack* tr, int laneIdx) {
|
||||
@@ -231,9 +241,6 @@ LaneApplyResult applyLaneOps(const TrackByGuid& handleByGuid,
|
||||
return out;
|
||||
}
|
||||
|
||||
// Managed-lane minting: the DECISION (which tracks split, which lanes, which item
|
||||
// goes where) is planLaneMinting's; this shell only reads, calls and applies.
|
||||
|
||||
// One pass, so the assign loop needs no per-item re-scan.
|
||||
std::map<std::string, MediaItem*> itemHandlesByGuid(MediaTrack* tr) {
|
||||
std::map<std::string, MediaItem*> byGuid;
|
||||
@@ -367,11 +374,22 @@ bool transportBlocksModeSwitch(ReaProject* proj) {
|
||||
return (GetPlayStateEx(proj) & kTransportMoving) != 0;
|
||||
}
|
||||
|
||||
bool modeApplyInProgress() { return g_applying; }
|
||||
|
||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
|
||||
if (g_applying) return false; // see ApplyLatch; same fail-closed shape as the guards below
|
||||
ApplyLatch latch;
|
||||
|
||||
if (!model.modes().contains(targetModeId)) {
|
||||
return false; // reject before touching the project — no partial apply
|
||||
}
|
||||
|
||||
// Pinned ONCE: begin and end sit a whole apply apart with the pump premise between
|
||||
// them, so resolving "current project" independently at each would let a tab switch
|
||||
// mid-apply open the block on one project and close it on another.
|
||||
ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0);
|
||||
if (!ValidatePtr2(nullptr, project, "ReaProject*")) return false;
|
||||
|
||||
// The discriminator behind both halves of the contract in view.h. A gate placed
|
||||
// unconditionally here would break tagging and the project-load reapply during
|
||||
// playback; a solo round on every reapply would flicker the user's solo on every
|
||||
@@ -379,13 +397,13 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
const std::string outgoingModeId = model.activeModeId();
|
||||
const bool realSwitch = targetModeId != outgoingModeId;
|
||||
|
||||
if (realSwitch && transportBlocksModeSwitch(proj)) {
|
||||
if (realSwitch && transportBlocksModeSwitch(project)) {
|
||||
return false; // same fail-closed shape as the mode-exists guard above
|
||||
}
|
||||
|
||||
TrackHandles handleByGuid;
|
||||
std::vector<TrackFolderEntry> entries = readFolderEntries(proj, handleByGuid);
|
||||
const TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
||||
std::vector<TrackFolderEntry> entries = readFolderEntries(project, handleByGuid);
|
||||
TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
||||
FolderTree tree = buildFolderTree(entries);
|
||||
|
||||
// Prune snapshots for tracks no longer in the live enumeration before
|
||||
@@ -417,7 +435,7 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
FxRestoreDrops fxDrops;
|
||||
int fxDropTracks = 0;
|
||||
|
||||
Undo_BeginBlock2(proj);
|
||||
Undo_BeginBlock2(project);
|
||||
{
|
||||
UiRefreshHold uiHold; // every write below lands with the TCP/MCP frozen
|
||||
|
||||
@@ -438,7 +456,7 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
// Re-validated per track, not once at resolve: offlining a plugin is
|
||||
// ASSUMED to pump the message loop, so a track deleted during an
|
||||
// earlier iteration's FX writes would leave this handle dangling.
|
||||
if (!tr || !trackStillLive(proj, tr)) continue; // stale/deleted GUID — prune
|
||||
if (!tr || !trackStillLive(project, tr)) continue; // stale/deleted GUID — prune
|
||||
|
||||
const TrackApplyResult r = parkTrack(model, guid, tr, tp.flags);
|
||||
if (r.refused) { refusedParkNames.push_back(trackDisplayName(tr)); continue; }
|
||||
@@ -450,7 +468,7 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
if (tp.flags.empty()) continue;
|
||||
const std::string& guid = tp.flags.front().guid;
|
||||
MediaTrack* tr = resolve(trackByGuid, guid);
|
||||
if (!tr || !trackStillLive(proj, tr)) continue; // stale/deleted GUID — prune
|
||||
if (!tr || !trackStillLive(project, tr)) continue; // stale/deleted GUID — prune
|
||||
|
||||
const TrackApplyResult r = restoreTrack(model, guid, tr, tp.fxOffline, tp.flags);
|
||||
if (r.wrote) wrote = true;
|
||||
@@ -458,6 +476,16 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
if (r.drops.total() > 0) { fxDrops.add(r.drops); ++fxDropTracks; }
|
||||
}
|
||||
|
||||
// The FX writes above are the only thing here assumed to pump the message
|
||||
// loop, so this is the one point a handle taken before them can have died.
|
||||
// Re-validated for the whole enumeration rather than per consumer: lane ops,
|
||||
// parent visibility and the solo replay all write through pre-FX handles.
|
||||
for (auto& kv : handleByGuid) {
|
||||
if (trackStillLive(project, kv.second)) continue;
|
||||
trackByGuid.erase(kv.first);
|
||||
kv.second = nullptr; // view_solo's writers already skip a null handle
|
||||
}
|
||||
|
||||
// MANAGED LANES: the active mode's lane plays+shows, every other managed
|
||||
// lane is silenced+hidden. Empty on a D1-only project — byte-identical there.
|
||||
const LaneApplyResult lanes = applyLaneOps(trackByGuid, plan.lanes);
|
||||
@@ -502,11 +530,11 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
if (laneModeChanged) UpdateTimeline();
|
||||
|
||||
const bool mint = applyMintsUndoPoint(realSwitch, wrote);
|
||||
Undo_EndBlock2(proj, mint ? undoLabel.c_str() : "", mint ? undoMask : 0);
|
||||
Undo_EndBlock2(project, mint ? undoLabel.c_str() : "", mint ? undoMask : 0);
|
||||
|
||||
// After the undo block and the UI hold: a report is not a project write, and
|
||||
// it must not join what a Ctrl-Z rolls back.
|
||||
reportRefusedParks(proj, refusedParkNames);
|
||||
reportRefusedParks(project, refusedParkNames);
|
||||
reportFxRestoreDrops(fxDrops, fxDropTracks);
|
||||
return true;
|
||||
}
|
||||
@@ -524,6 +552,8 @@ bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||
|
||||
const TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
||||
|
||||
// `proj` deliberately NOT pinned the way applyMode pins it: nothing between this
|
||||
// begin and its end pumps the message loop, so no tab switch can land between them.
|
||||
Undo_BeginBlock2(proj);
|
||||
const bool changed = applyMintPlan(model, plan, trackByGuid);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user