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
@@ -110,6 +110,14 @@ NodeParamViewConnectedLabel::NodeParamViewConnectedLabel(const oak::Input &input
&NodeParamViewConnectedLabel::set_value_tree_visible);
}
NodeParamViewConnectedLabel::~NodeParamViewConnectedLabel()
{
// Raw C-API subscription carries `this` as userdata; not covered by
// Qt's auto-disconnect. Unsubscribe or the next playhead event calls
// into a dead object.
set_viewer_node(nullptr);
}
void NodeParamViewConnectedLabel::set_viewer_node(OakEngineNode *viewer)
{
if (viewer_) {
@@ -37,6 +37,7 @@ class NodeParamViewConnectedLabel : public QWidget {
public:
NodeParamViewConnectedLabel(const oak::Input &input,
QWidget *parent = nullptr);
~NodeParamViewConnectedLabel() override;
void set_viewer_node(OakEngineNode *viewer);
@@ -37,8 +37,9 @@ static oak::Input ResolveGroupInput(const oak::Input &input)
char input_id[256];
int element = input.element();
const QByteArray utf = input.input_id().toUtf8();
memcpy(input_id, utf.constData(), qMin<int>(sizeof(input_id) - 1, utf.size()));
input_id[sizeof(input_id) - 1] = '\0';
const int len = qMin<int>(sizeof(input_id) - 1, utf.size());
memcpy(input_id, utf.constData(), len);
input_id[len] = '\0';
// WRAPPER-GAP: oakengine_group_resolve_input (group API has no oak:: wrapper)
if (oakengine_group_resolve_input(
node, input_id, element,
@@ -157,6 +157,20 @@ NodeParamViewKeyframeControl::NodeParamViewKeyframeControl(bool right_align,
show_buttons_from_keyframe_enable(false);
}
NodeParamViewKeyframeControl::~NodeParamViewKeyframeControl()
{
// Raw C-API subscription carries `this` as userdata; it is not covered
// by Qt's auto-disconnect. Without this, a playhead event delivered
// after destruction calls update_state() on a dead object.
if (viewer_sub_ > 0) {
oakengine_event_unsubscribe(viewer_sub_);
viewer_sub_ = 0;
}
// Drop the keyframe_* bridge subscriptions too (same raw-userdata
// mechanism underneath).
set_input(oak::Input());
}
void NodeParamViewKeyframeControl::set_input(const oak::Input &input)
{
if (input_.is_valid()) {
@@ -41,6 +41,7 @@ public:
: NodeParamViewKeyframeControl(true, parent)
{
}
~NodeParamViewKeyframeControl() override;
const oak::Input &get_connected_input() const
{
@@ -142,9 +142,9 @@ static bool ResolveGroupInput(oak::Input *input)
char input_id[256];
int element = input->element();
const QByteArray utf = input->input_id().toUtf8();
memcpy(input_id, utf.constData(),
qMin<int>(sizeof(input_id) - 1, utf.size()));
input_id[sizeof(input_id) - 1] = '\0';
const int len = qMin<int>(sizeof(input_id) - 1, utf.size());
memcpy(input_id, utf.constData(), len);
input_id[len] = '\0';
if (!oakengine_node_group_get_inner(&node, input_id, sizeof(input_id),
&element)) {
return false;