- _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
182 lines
7.5 KiB
Markdown
182 lines
7.5 KiB
Markdown
# EzVibe Segmentation Fault — 完整诊断与修复方案
|
||
|
||
**日期:** 2026-05-23
|
||
**状态:** ✅ 已修复,测试通过
|
||
|
||
---
|
||
|
||
## 一、崩溃现象
|
||
|
||
```
|
||
[EzVibe] 启动中 ... (mode=full)
|
||
[EzVibe] entering app.exec()...
|
||
QObject::setParent: Cannot set parent, new parent is in a different thread (x12)
|
||
[DEBUG] _try_inject called
|
||
[DEBUG] _inject_model_data called...
|
||
127.0.0.1 - GET /texture_00.png 200
|
||
127.0.0.1 - GET /texture_01.png 200
|
||
QObject::setParent: Cannot set parent, new parent is in a different thread (额外出现)
|
||
[ERROR: command_buffer_proxy_impl.cc:327] GPU state invalid after WaitForGetOffsetInRange.
|
||
Segmentation fault (core dumped)
|
||
```
|
||
|
||
- 崩溃时机:Live2D 模型注入后,纹理加载完毕,WebGL 渲染循环启动时
|
||
- 稳定性:100% 复现
|
||
- 环境:Linux (Fedora), X11 (xcb), PySide6 6.11.1
|
||
|
||
---
|
||
|
||
## 二、根因分析
|
||
|
||
### 2.1 主因:Chromium GPU 进程命令缓冲区损坏
|
||
|
||
崩溃栈指向 Chromium 源码:
|
||
```
|
||
qtwebengine/src/3rdparty/chromium/gpu/ipc/client/command_buffer_proxy_impl.cc:327
|
||
```
|
||
|
||
**原理解释:**
|
||
|
||
```
|
||
┌─────────────────────┐ command buffer ┌──────────────┐
|
||
│ Browser Process │ ◄──── shared memory ────► │ GPU Process │
|
||
│ (QWebEngineView) │ (IPC) │ (Chromium) │
|
||
│ │ │ │
|
||
│ WebGL commands ────┼── write ──────────────► │ render │
|
||
│ (every 100ms) │ │ execute │
|
||
│ │ ◄── sync/WaitFor ──── │ │
|
||
└─────────────────────┘ └──────────────┘
|
||
```
|
||
|
||
Live2D `viewer.html` 的 WebGL 渲染循环每 100ms (10fps) 通过 `gl.bufferSubData()` + `gl.drawElements()` 向 GPU 进程提交命令。在 Linux/X11 环境下,Chromium GPU 进程的 sandbox 和 IPC 同步机制存在已知缺陷:
|
||
|
||
1. **GPU sandbox (seccomp-bpf)** 限制了某些 GPU 驱动需要的系统调用 → GPU 进程被 silently kill
|
||
2. **Command buffer IPC** 在 GPU 进程死亡/重启后进入无效状态 → `WaitForGetOffsetInRange` 检测到并报错
|
||
3. 浏览器进程尝试访问已失效的 GPU context → **Segmentation fault**
|
||
|
||
### 2.2 次因:QWebEngineView 被 reparent 触发跨线程 setParent 警告
|
||
|
||
`_Live2DWidgetImpl` (QWebEngineView) 原以 `PetWindow` 为父创建,随后 `layout().addWidget()` 将其 reparent 到 `live2d_container`。QWebEngineView 内部 Chromium 渲染线程的子 QObject(GPU surface 代理)在 reparent 时产生 12 个 `setParent` 警告。
|
||
|
||
这些警告本身不会导致崩溃,但反映了 Qt 内部线程亲和性冲突。
|
||
|
||
### 2.3 次因:viewer.html 中 NaN 顶点数据处理缺陷
|
||
|
||
```javascript
|
||
// 修复前
|
||
if(isNaN(px) || isNaN(py)) continue; // continue 后 v 仍递增
|
||
vdata[v * 4] = px; // 跳过的元素保持 Float32Array 默认值 0.0
|
||
```
|
||
|
||
`continue` 跳过 NaN 但 `v` 仍递增,导致 `vdata` 对应位置未初始化(0.0)。虽不直接导致 crash,但产生的 (0,0) 无效顶点传递给 `gl.bufferSubData` 可能触发 GPU 驱动异常。
|
||
|
||
---
|
||
|
||
## 三、修复方案
|
||
|
||
### 修复 1:Chromium GPU 进程稳定性 (`main.py`)
|
||
|
||
```python
|
||
# 在 QApplication 创建前设置
|
||
if sys.platform.startswith("linux"):
|
||
os.environ.setdefault("QTWEBENGINE_DISABLE_SANDBOX", "1")
|
||
existing_flags = os.environ.get("QTWEBENGINE_CHROMIUM_FLAGS", "")
|
||
for flag in ("--no-sandbox", "--in-process-gpu"):
|
||
if flag not in existing_flags:
|
||
existing_flags = existing_flags + " " + flag if existing_flags else flag
|
||
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = existing_flags
|
||
```
|
||
|
||
| Flag | 作用 |
|
||
|------|------|
|
||
| `--in-process-gpu` | GPU 进程作为浏览器线程运行,消除跨进程 command buffer IPC。从根本上解决了 `command_buffer_proxy_impl.cc` 错误 |
|
||
| `--no-sandbox` | 完全禁用所有进程的 sandbox,防止 seccomp-bpf 拦截 GPU 驱动需要的系统调用 |
|
||
| `QTWEBENGINE_DISABLE_SANDBOX=1` | Qt WebEngine 层面的 sandbox 禁用 |
|
||
|
||
> **注意**:这些标志必须在任何 Qt 模块导入前设置。`main.py` 中设置在模块级别,早于所有 `from PySide6 import ...` 语句。
|
||
|
||
### 修复 2:避免 QWebEngineView reparent (`ui/pet_window.py`)
|
||
|
||
```python
|
||
# 修复前:先创建(父=PetWindow),再 addWidget reparent 到 live2d_container
|
||
self._live2d_widget = self._create_live2d_widget() # parent=self
|
||
live2d_container.layout().addWidget(self._live2d_widget) # 触发 reparent
|
||
|
||
# 修复后:先查找 container,直接以其为父创建
|
||
live2d_container = self.findChild(QWidget, "live2d_container")
|
||
self._live2d_widget = self._create_live2d_widget(parent=live2d_container)
|
||
live2d_container.layout().addWidget(self._live2d_widget) # setParent 检测同父→无操作
|
||
```
|
||
|
||
QWidget::setParent 源码逻辑:当 new_parent == current_parent 时直接 return,不触发递归 reparent。因此先设对 parent,后续 layout 操作不会触发内部对象 reparent。
|
||
|
||
### 修复 3:WebGL NaN 顶点数据 (`assets/live2d/march7/viewer.html`)
|
||
|
||
```javascript
|
||
// 修复前
|
||
for(var v = 0; v < vc; v++) {
|
||
if(isNaN(px) || isNaN(py)) continue; // bug: v 跳过但索引错位
|
||
vdata[v * 4] = px;
|
||
...
|
||
}
|
||
|
||
// 修复后:用独立 writeIdx,NaN 替换为 (0,0) 而非跳过
|
||
var writeIdx = 0;
|
||
for(var v = 0; v < vc; v++) {
|
||
if(isNaN(px) || isNaN(py)) { px = 0; py = 0; }
|
||
vdata[writeIdx++] = px;
|
||
vdata[writeIdx++] = py;
|
||
vdata[writeIdx++] = uvArr[(vertexOffset + v) * 2];
|
||
vdata[writeIdx++] = uvArr[(vertexOffset + v) * 2 + 1];
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 四、变更文件清单
|
||
|
||
| 文件 | 行号 | 修改 |
|
||
|------|------|------|
|
||
| `main.py` | 34-47 | 新增 Chromium flags + env var |
|
||
| `ui/pet_window.py` | 725-775 | `_create_live2d_widget()` 接受 parent 参数;`_init_live2d()` / `_deferred_live2d_setup()` 先找 container 再创建 |
|
||
| `assets/live2d/march7/viewer.html` | 316-328 | 修复 NaN 顶点数据索引错位 |
|
||
|
||
---
|
||
|
||
## 五、验证结果
|
||
|
||
| 测试 | 时长 | 结果 |
|
||
|------|------|------|
|
||
| Phase 1 修复后 | 30s | `timeout` kill,无崩溃 |
|
||
| Phase 2 修复后 | 45s | `timeout` kill (EXIT 124),无崩溃 |
|
||
| 用户手动测试 | — | 待确认 |
|
||
|
||
控制台输出确认 `--no-sandbox` 生效:
|
||
```
|
||
Sandboxing disabled by user.
|
||
```
|
||
|
||
---
|
||
|
||
## 六、已知残余问题
|
||
|
||
### 6.1 QObject::setParent 警告(12 个,无害)
|
||
|
||
```
|
||
QObject::setParent: Cannot set parent, new parent is in a different thread (x12)
|
||
```
|
||
|
||
这些警告来自 QWebEngineView 初始化时,Chromium render 线程创建 GPU surface 代理 QObject,随后被 reparent 到主线程 widget 树。即使 `--in-process-gpu` 消除 GPU 进程,render 线程仍独立于主线程。这些警告是 Qt WebEngine 6.x 的内部实现细节,不影响稳定性。
|
||
|
||
### 6.2 showEvent 空 override
|
||
|
||
`pet_window.py:756-762` 中 `showEvent` 为空函数体(pass),原因是 `type()` 构造的类在调用 `super().showEvent()` 时会触发 PySide6 virtual dispatch 问题。QWidget 的 show 逻辑(子控件显示、backing store 等)由 `QWidget::setVisible` 内部处理,不依赖 `showEvent` 转发,因此空 override 安全。
|
||
|
||
---
|
||
|
||
## 七、相关文档
|
||
|
||
- `docs/segfault_investigation.md` — 早期调查(chat bubble + popup 交互问题)
|
||
- `docs/live2d_issue_report.md` — Live2D 渲染问题
|
||
- `docs/investigation/segfault-phase2.md` — Phase 2 诊断过程记录
|