- _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
6.6 KiB
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()的组合导致以下竞态:- Qt 删除待销毁控件的内部引用
- Chromium compositor 在同一帧尝试合成 WebGL 内容
- 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) |
八、经验教训
-
不要在有 GPU compositor 的控件树中调用
deleteLater()+insertWidget()。Chromium 的合成器持有对其 Qt 控件的内部引用;在合成器活跃时销毁/创建控件会导致悬空指针。 -
--in-process-gpu虽然避免了跨进程 IPC 崩溃,但会使 GPU 崩溃直接杀死整个进程。更好的做法是:先通过探针验证 WebGL 管线正常,然后减少 GPU 操作频率。 -
控件复用总是比删除+重建更安全。对于聊天消息列表等动态内容,维护一个控件池并更新文本/可见性,而不是销毁和重建 QLabel。