fix: node graph edge display, teardown crashes, and event/audio lifetime bugs

- capi: oakengine_node_output_connection_at/_at_ex returned the source
  node as the connection destination; the actual destination is
  conn.second.node(). Out-edge enumeration was useless, so the node
  view could only draw in-edges and randomly lost whichever edges
  needed the out-edge path (random per context build order).
  Regression test in oakengine_node_test
- project teardown: Project::clear() pre-notifies node removal while
  nodes are fully constructed (observers used to crash on
  half-destroyed nodes); childEvent suppresses the removal dance while
  clearing; Node::disconnect_all/disconnect_edge get a silent mode for
  teardown so no invalidation/events touch dying members
  (is_being_cleared); ClipBlock marker disconnect guarded against
  dead viewer/markers; ProjectCopier and PreviewAutoCacher drop
  project references on Project::destroyed instead of disconnecting
  dead objects at shutdown
- preview: add oakengine_preview_request_get_audio_sample_count; the
  viewer queried sample count by passing nullptr to get_audio_samples
  which rejects it, so all playback audio was silently dropped
- app: fix unterminated input-id memcpy in ResolveGroupInput
  (nodeparamviewitem, widgetbridge) that corrupted every parameter id
- app: unsubscribe raw C-API event subscriptions in destructors of
  NodeParamViewKeyframeControl, NodeParamViewConnectedLabel and
  ExportDialog; playhead events used to fire into dead widgets
  (crash when dragging the playhead)
- tests: preview request roundtrip (video frame + audio range) and
  free-while-active teardown coverage; env-gated OAK_DEBUG_EDGES /
  OAK_DEBUG_INVALID_INPUT diagnostics
- docs: investigation notes in docs/zh/
This commit is contained in:
2026-08-02 14:29:49 +08:00
parent 30853cbcfd
commit a59c33715f
25 changed files with 441 additions and 32 deletions
+48 -3
View File
@@ -21,6 +21,8 @@
#include "node.h"
#include <execinfo.h>
#include <QApplication>
#include <QGuiApplication>
#include <QDebug>
@@ -216,7 +218,7 @@ void Node::connect_edge(Node *output, const NodeInput &input)
}
}
void Node::disconnect_edge(Node *output, const NodeInput &input)
void Node::disconnect_edge(Node *output, const NodeInput &input, bool silent)
{
// Ensure graph is the same
Q_ASSERT(input.node()->parent() == output->parent());
@@ -232,6 +234,18 @@ void Node::disconnect_edge(Node *output, const NodeInput &input)
outputs.erase(std::find(outputs.begin(), outputs.end(),
std::pair<Node *, NodeInput>({ output, input })));
if (silent) {
// Teardown-only mode: just drop the edge from both maps. No events,
// no signals, no invalidation — the whole graph is being destroyed
// and handlers would touch half-destroyed nodes.
return;
}
if (qEnvironmentVariableIsSet("OAK_DEBUG_EDGES")) {
qWarning("EDGE-DEBUG: disconnect_edge %p -> %p (%s)", (void *)output,
(void *)input.node(), qPrintable(input.input()));
}
// Call internal events
input.node()->InputDisconnectedEvent(input.input(), input.element(),
output);
@@ -1239,6 +1253,14 @@ Node *Node::copy_node_in_graph(Node *node, MultiUndoCommand *command)
void Node::send_invalidate_cache(const TimeRange &range,
const InvalidateCacheOptions &options)
{
// During project teardown, don't propagate invalidation across edges:
// the nodes on the other side may already be half-destroyed.
if (Project *p = project()) {
if (p->is_being_cleared()) {
return;
}
}
for (const OutputConnection &conn : output_connections_) {
// Send clear cache signal to the Node
const NodeInput &in = conn.second;
@@ -1249,6 +1271,12 @@ void Node::send_invalidate_cache(const TimeRange &range,
void Node::invalidate_all(const QString &input, int element)
{
if (Project *p = project()) {
if (p->is_being_cleared()) {
return;
}
}
invalidate_cache(TimeRange(RATIONAL_MIN, RATIONAL_MAX), input, element);
}
@@ -1910,6 +1938,18 @@ void Node::report_invalid_input(const char *attempted_action, const QString &id,
qWarning()
<< "Failed to" << attempted_action << "parameter" << id << "element"
<< element << "in node" << this->id() << "- input doesn't exist";
if (qEnvironmentVariableIsSet("OAK_DEBUG_INVALID_INPUT")) {
void *frames[32];
const int n = backtrace(frames, 32);
char **symbols = backtrace_symbols(frames, n);
if (symbols) {
for (int i = 0; i < n; i++) {
qWarning("INVALID-INPUT-BT: %s", symbols[i]);
}
free(symbols);
}
}
}
NodeInputImmediate *Node::create_immediate(const QString &input)
@@ -2284,15 +2324,20 @@ bool Node::inputs_from(const QString &id, bool recursively) const
void Node::disconnect_all()
{
// During project teardown, skip the disconnect events/signals: the
// other side's handlers touch this node's members which are already
// destroyed at ~Node time (e.g. caches), which is a use-after-free.
const bool silent = project() && project()->is_being_cleared();
// Disconnect inputs (copy map since internal map will change as we disconnect)
InputConnections copy = input_connections_;
for (auto it = copy.cbegin(); it != copy.cend(); it++) {
disconnect_edge(it->second, it->first);
disconnect_edge(it->second, it->first, silent);
}
while (!output_connections_.empty()) {
OutputConnection conn = output_connections_.back();
disconnect_edge(conn.first, conn.second);
disconnect_edge(conn.first, conn.second, silent);
}
}