Game crashes when opening map view from vessels orbiting Minmus, or clicking on them in the tracking station

This issue has been tracked since 2022-02-17.

Good morning, I have been encountering an error after doing an Apollo-style Minmus landing in the stock solar system in a science save. When attempting to switch back to the command module in orbit through the tracking staion, the game freezes, then crashes without any message (essentially, closes itself). Ascending to orbit in the lander and then switching to map view to create a flight plan crashes the game instantly. A satellite from an earlier mission orbiting Minmus produces the same result.
I have multiple part mods such as BDB, and environment mods like Parallax and EVE. The solar system is the stock one and not unmodified, but all vessels this occurs with contained modded parts from BDB.

All files relevant are in this gist here.

I upgraded from Hardy and played on the new release multiple times before this crash occurred, and it does not seem to occur with Kerbin or the Mun or interplanetary space; I have not tested other planets.
In the stderr file, I can see something about a duplicated time mismatch, and googling about this in context with KSP brought me nothing. I hope this can be helped :)

pleroy wrote this answer on 2022-02-17

Thank you for reporting. From the look of your logs it seems that some data structure related to trajectories got corrupted. However, there is not enough information for us to figure out how it got into that state.

Is this a problem that is reproducible? If so, could you provide us with a journal (instructions here)? That would help us trace how/why/where the corruption happened.

columbus15 wrote this answer on 2022-02-18

Unfortunately I cannot put the journal file into a gist, so it has to be on my Google Drive which I hope is acceptable?

The crash is consistently reproducable when clicking on either of the orbiting vessels and does not occur when the vessel is landed. Also, a reinstall of Principia which I did after taking this journal did not fix the error.

pleroy wrote this answer on 2022-02-20

I was able to reproduce the crash using the above journal, although things seem somewhat nondeterministic. No idea what's happening yet.

Decoded strack trace:

int* const vertex_count) {
journal::Method<journal::PlanetariumPlotPsychohistory> m(
{planetarium,
plugin,
vessel_guid,
max_history_length,
vertices,
vertices_size},
{vertex_count});
CHECK_NOTNULL(plugin);
CHECK_NOTNULL(planetarium);
*vertex_count = 0;
// Do not plot the psychohistory when there is a target vessel as it is
// misleading.
if (plugin->renderer().HasTargetVessel()) {
return m.Return();
} else {
auto const vessel = plugin->GetVessel(vessel_guid);
auto const& trajectory = vessel->trajectory();
auto const& psychohistory = vessel->psychohistory();
Instant const desired_first_time =
plugin->CurrentTime() - max_history_length * Second;
// Since we would want to plot starting from |desired_first_time|, ask the
// reanimator to reconstruct the past. That may take a while, during which
// time the history will be shorter than desired.
vessel->RequestReanimation(desired_first_time);
void Vessel::RequestReanimation(Instant const& desired_t_min) {
reanimator_.Start();
// No locking here because vessel reanimation is only invoked from the main
// thread.
// If the reanimator is asked to do significantly less work (in terms of
// checkpoints to reanimate) than it is currently doing, interrupt it. Note
// that this is fundamentally racy: for instance the reanimator may not have
// picked the last input given by Put. But it helps if the user was doing a
// very long reanimation and wants to shorten it.
bool const must_restart =
last_desired_t_min_.has_value() &&
checkpointer_->checkpoint_at_or_before(last_desired_t_min_.value()) <
checkpointer_->checkpoint_at_or_before(desired_t_min);
LOG_IF(WARNING, must_restart)
<< "Restarting reanimator because desired t_min went from "
<< last_desired_t_min_.value() << " to " << desired_t_min;
last_desired_t_min_ = desired_t_min;
if (must_restart) {
reanimator_.Restart();
}
{
absl::MutexLock l(&lock_);
if (DesiredTMinReachedOrFullyReanimated(desired_t_min)) {
Instant const& desired_t_min) {
lock_.AssertReaderHeld();
// Consume the reanimated trajectories and merge them into this trajectory.
// This is the only place where the reanimation becomes externally visible,
// thereby ensuring that the trajectory doesn't change, say, while clients
// iterate over it.
while (!reanimated_trajectories_.empty()) {
trajectory_.Merge(std::move(reanimated_trajectories_.front()));
void DiscreteTrajectory<Frame>::Merge(DiscreteTrajectory<Frame> trajectory) {
auto sit_s = trajectory.segments_->begin(); // Source iterator.
auto sit_t = segments_->begin(); // Target iterator.
for (;;) {
if (sit_s != trajectory.segments_->end() && sit_t != segments_->end()) {
// Record the existing left endpoint to update the time-to-segment map as
// needed.
const std::optional<Instant> left_endpoint =
sit_t->empty() ? std::nullopt
: std::make_optional(sit_t->front().time);
// Merge corresponding segments.
sit_t->Merge(std::move(*sit_s));
// If the left endpoint of |sit_t| has changed, remove its entry from the
// time-to-segment map, if any.
if (left_endpoint.has_value() &&
sit_t->front().time < left_endpoint.value()) {
auto const it = segment_by_left_endpoint_.find(left_endpoint.value());
if (it != segment_by_left_endpoint_.end() && it->second == sit_t) {
segment_by_left_endpoint_.erase(left_endpoint.value());
}
}
// Insert a new entry in the time-to-segment map if the segment is not
// empty. This entry will be overwritten by any future entry at the same
// time, thereby enforcing the invariants of the time-to-segment map.
if (!sit_t->empty()) {
segment_by_left_endpoint_.insert_or_assign(sit_t->front().time, sit_t);
}
++sit_s;
++sit_t;
} else if (sit_s != trajectory.segments_->end()) {
// No more segments in the target. We splice the segments of the source.
// The |end| iterator keeps pointing at the end after the splice.
// Instead, we track the iterator to the last segment.
auto const last_before_splice = --segments_->end();
segments_->splice(segments_->end(),
*trajectory.segments_,
sit_s,
trajectory.segments_->end());
auto const end_before_splice = std::next(last_before_splice);
AdjustAfterSplicing(/*from=*/trajectory,
/*to=*/*this,
/*to_segments_begin=*/end_before_splice);
break;
} else if (sit_t != segments_->end()) {
// No more segments in the source. Make sure that we restore the time-to-
// segment map for the segments that follows the last one in the source.
if (!sit_t->empty()) {
segment_by_left_endpoint_.insert_or_assign(sit_t->front().time, sit_t);
}
++sit_t;
} else {
// Both lists done.
break;
}
}
CHECK_OK(ConsistencyStatus());

pleroy wrote this answer on 2022-02-21

We have an empty segment (segment 472) in the middle of the trajectory for vessel Sarnus II CF (Hermes I) Ship (15f6cf28-fc3e-4997-9ab4-93de951ab9c2). That's a violation of our invariants: empty segments are fine at the beginning of trajectories, not in the middle.

How did this come to be? This segment is the one immediately before the beginning of the "explicitly stored" part of the trajectory. We conjecture that it is non-collapsible, so it should be extracted from a checkpoint. However, there is no checkpoint matching the end time of that segment, 2000-10-24T16:27:13,1125793159008026 (TT) (that time falls between checkpoints 225 and 226). Normally we should always take a checkpoint when creating a non-collapsible segment, but there is worrisome code here:

// This test is needed because in some cornercases we might try to create
// multiple checkpoints at the same time. See #3280.
if (checkpointer_->newest_checkpoint() < checkpoint) {
LOG(INFO) << "Writing " << ShortDebugString()
<< " to checkpoint at: " << checkpoint;
checkpointer_->WriteToCheckpoint(checkpoint);
}
// If there are no checkpoints in the current trajectory (this would
// happen if we restored the last part of trajectory and it didn't overlap
// with a checkpoint and no reanimation happened) then the
// |oldest_reanimated_checkpoint_| need to be updated to reflect the newly
// created checkpoint.
{
absl::MutexLock l(&lock_);
if (oldest_reanimated_checkpoint_ == InfiniteFuture) {
oldest_reanimated_checkpoint_ = checkpoint;
} else {
CHECK_LT(oldest_reanimated_checkpoint_, checkpoint);
}
}
}
auto psychohistory = trajectory_.DetachSegments(psychohistory_);
backstory_ = trajectory_.NewSegment();

Notice how we skip creating the checkpoint if there is one at the same time, but we still create a segment.

This is somewhat inconsistent, but wouldn't be a disaster if there wasn't a nasty interaction with WriteToMessage: if given an iterator that designates a time at which a 1-point segment (or several) exists, WriteToMessage skips these 1-point segments and starts writing at the first segment that has multiple points. The skipped 1-point segments will be reconstructed by integration (if they are collapsible) or from a checkpoint (if they are not). The problem here is that we don't have a checkpoint (because of the above code) so we have no way to reconstruct the missing segment.

Having written this I am not completely sure how it should be fixed. More thought will be needed...

More Details About Repo
Owner Name mockingbirdnest
Repo Name Principia
Full Name mockingbirdnest/Principia
Language C++
Created Date 2014-02-08
Updated Date 2023-03-29
Star Count 664
Watcher Count 29
Fork Count 62
Issue Count 109

YOU MAY BE INTERESTED

Issue Title Created Date Updated Date