Files
EzVibe/docs/investigation/segfault-root-cause-and-fix.md
e2hang 798e5c2f7d checkpoint: segfault root cause analysis + Live2D stability fixes
- _refresh_messages: widget pool reuse to avoid layout cascade → QWebEngineView crash
- viewer.html: QWebChannel bridge, texture resize, burst render, gl.finish
- live2d_view.py: debug checkpoints, playMotion/setExpression render-on-demand
- main.py: Chromium flags --no-sandbox --in-process-gpu --disable-gpu-rasterization --ignore-gpu-blocklist
- scheduler.py: test_reminder re-enabled
- docs: complete root cause analysis
2026-05-23 13:33:58 +08:00

6.6 KiB
Raw Permalink Blame History

EzVibe Segfault — 完整根因分析与修复方案

日期: 2026-05-23 状态: 已定位根因,已修复


一、崩溃现象

QObject::setParent: Cannot set parent, new parent is in a different thread (x12)
[JS→PY] init_start → webgl_ok → shader_ok → test_rect_ok → model_ok:d=307
[JS→PY] loading_textures:n=2 → texture_ok:0 → texture_ok:1 → textures_done:n=2
[JS→PY] building_buffers → buffers_ok:n=307 → scheduling_render
[JS→PY] ready → first_frame_ok
... 运行 20-30 秒正常 ...
Segmentation fault (core dumped)
  • Live2D 初始化全部成功WebGL 上下文、Shader、模型 307 drawables、纹理、缓冲区、首帧渲染
  • 崩溃发生在 first_frame_ok 之后 20-30 秒
  • 稳定复现100%

二、诊断过程

阶段 1排除 GPU 命令缓冲区损坏

早期崩溃在纹理加载后立即发生,伴随 GPU state invalid after WaitForGetOffsetInRange 错误。

修复: 添加 --no-sandbox --in-process-gpu Chromium 标志,消除了 GPU 进程 IPC 同步问题。

结果: GPU 命令缓冲区错误消失,但 segfault 仍然存在(只是延迟出现)。

阶段 2排除连续 WebGL 渲染循环

viewer.html 使用 setInterval(100ms) 持续渲染 WebGL 帧。每次渲染触发 gl.bufferSubData + gl.drawElements,导致 Chromium GPU 对象频繁跨线程 reparent产生额外 setParent 警告)。

修复: 将连续渲染改为单次 burst 模式(初始 10 帧后停止),按需渲染。

结果: setParent 警告数量从 23 个减少到 12 个(仅初始化阶段),且第一帧渲染成功。崩溃延迟到 ~30 秒。

阶段 3追踪崩溃时间点

通过添加 QWebChannel 调试探针([JS→PY] 消息)精确定位:

[JS→PY] first_frame_ok
(20-30 秒正常运行)
Segmentation fault

崩溃时间(启动后 ~30-40 秒,即 first_frame_ok 后 20-30 秒)精准对应 brain timer (_init_brain_timer.start(30_000))。

阶段 4隔离确认

禁用 brain timer + test_reminder 后,应用稳定运行 2 分钟+,无崩溃。

确认: brain timer每 30s或 scheduler 触发的 show_reminder_refresh_messages 是崩溃的直接触发点。


三、根因分析

崩溃路径

brain timer (30s)
  → _on_brain_tick()
    → 后台线程: asyncio loop → brain.decide_action()
      → _decide_proactive_action() → remind_water
    → QMetaObject.invokeMethod(_add_helper, "do_add", QueuedConnection)
      → 主线程: _do_add_message(text, is_self)
        → _refresh_messages()
          → deleteLater() 删除旧 QLabel
          → insertWidget() 添加新 QLabel
          → QLayout::invalidate() 向上传播到整个控件树
          → QWebEngineView 收到 resize/repaint 事件
          → Chromium compositor 尝试合成 WebGL 内容
          → 访问因 deleteLater 而处于无效状态的 GPU 资源
          → SIGSEGV

关键矛盾

  • Live2D WebGL 渲染 + 聊天布局修改 两者单独运行时都稳定
  • 两者同时启用时,任何消息添加brain timer, scheduler, 用户输入)都会触发 _refresh_messages,从而在 WebGL compositor 活跃时修改 Qt 控件树
  • deleteLater() + insertWidget() 的组合导致以下竞态:
    1. Qt 删除待销毁控件的内部引用
    2. Chromium compositor 在同一帧尝试合成 WebGL 内容
    3. compositor 访问已被删除的 Qt 内部对象

为什么首帧渲染后不立即崩溃

首帧渲染发生在 ~t=10s。此时没有消息触发 _refresh_messages。第一次可触发崩溃的 _refresh_messages 是:

  • t=11s: scheduler test_reminder如果启用
  • t=30s: brain timer remind_water

前者在测试中被禁用,后者精准对应崩溃时间。


四、修复方案

核心修复:_refresh_messages 控件复用

原理: 避免在 WebGL compositor 活跃时调用 deleteLater() / insertWidget() 修改 QLayout。

实现:

  • 维护气泡 QLabel 缓存池 _bubble_widgets
  • _refresh_messages 仅更新已有控件的 setText(), setStyleSheet(), show()/hide()
  • 新控件仅在池容量不足时一次性创建(此时 WebGL 尚未渲染首帧)
  • 完全消除 deleteLater() 调用

辅助修复

修复 文件 作用
纹理缩放至 1024×1024 viewer.html GPU 内存从 128MB 降至 8MB
无 mipmap viewer.html 减少 33% 纹理内存
QWebChannel JS 桥接 viewer.html + qwebchannel.js JS→PY 调试探针
--no-sandbox --in-process-gpu main.py GPU 进程稳定性
gl.finish() 在首帧后 viewer.html 确保 GPU 操作完成后再发信号

五、验证结果

测试场景 时长 结果
brain timer 禁用 2 min+ 无崩溃
brain timer 启用 + 控件复用 2 min+ 无崩溃,消息正常显示

六、残余问题(已知)

12 个 QObject::setParent 警告(无害)

QObject::setParent: Cannot set parent, new parent is in a different thread (x12)

来自 QWebEngineView 内部 Chromium 线程在初始化 GPU surface proxy 时的跨线程 reparent。Qt WebEngine 6.x 内部实现细节,不影响稳定性。

Live2D 白屏

如果 WebGL 上下文创建失败或纹理加载失败Live2D 区域显示为透明/白色。当前通过 test_rect_ok 探针验证 WebGL 管线正常。


七、相关文件清单

文件 修改
ui/pet_window.py _refresh_messages 控件复用;_scroll_to_bottom 直接引用;showEvent 显式 override
assets/live2d/march7/viewer.html QWebChannel 集成纹理缩放Burst 渲染模式WebGL 错误检查
ui/live2d_view.py cleanup() 简化;onJsMessage 调试日志;playMotion/setExpression 按需渲染
main.py Chromium flags: --no-sandbox --in-process-gpu --disable-gpu-rasterization --ignore-gpu-blocklist
agent/scheduler.py (无最终修改,仅测试期间禁用 test_reminder)

八、经验教训

  1. 不要在有 GPU compositor 的控件树中调用 deleteLater() + insertWidget()。Chromium 的合成器持有对其 Qt 控件的内部引用;在合成器活跃时销毁/创建控件会导致悬空指针。

  2. --in-process-gpu 虽然避免了跨进程 IPC 崩溃,但会使 GPU 崩溃直接杀死整个进程。更好的做法是:先通过探针验证 WebGL 管线正常,然后减少 GPU 操作频率。

  3. 控件复用总是比删除+重建更安全。对于聊天消息列表等动态内容,维护一个控件池并更新文本/可见性,而不是销毁和重建 QLabel。