Files
EzVibe/assets/live2d/march7/viewer.html
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

563 lines
20 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
* {margin:0;padding:0;box-sizing:border-box;}
body {background:rgba(0,0,0,0);overflow:hidden;width:100%;height:100%;}
canvas {width:100%;height:100%;display:block;}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="live2dcubismcore.min.js"></script>
<script src="qwebchannel.js"></script>
<script>
(function(){
var canvas = document.getElementById('canvas');
var gl = null;
var model = null;
var program = null;
var ready = false;
var _assetsDir = '';
var _alreadyInitialized = false;
var _renderRequested = false;
// Max texture dimension to avoid GPU memory exhaustion (4096^2*4*2 = 128MB)
var MAX_TEXTURE_SIZE = 1024;
function _glError(tag) {
var err = gl.getError();
if(err !== gl.NO_ERROR) {
console.error('[Live2D] GL error after ' + tag + ': 0x' + err.toString(16));
return true;
}
return false;
}
// Simple WebGL shader for Live2D rendering
var VERTEX_SHADER = [
'precision mediump float;',
'attribute vec2 a_position;',
'attribute vec2 a_uv;',
'varying vec2 v_uv;',
'uniform mat4 u_mvp;',
'void main() {',
' gl_Position = u_mvp * vec4(a_position, 0.0, 1.0);',
' v_uv = a_uv;',
'}'
].join('\n');
var FRAGMENT_SHADER = [
'precision mediump float;',
'varying vec2 v_uv;',
'uniform sampler2D u_texture;',
'uniform vec4 u_color;',
'void main() {',
' vec4 c = texture2D(u_texture, v_uv);',
' gl_FragColor = c * u_color;',
'}'
].join('\n');
function createShader(type, source) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('[Live2D] Shader compile error:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(vs, fs) {
var p = gl.createProgram();
gl.attachShader(p, vs);
gl.attachShader(p, fs);
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
console.error('[Live2D] Program link error:', gl.getProgramInfoLog(p));
return null;
}
return p;
}
function base64ToArrayBuffer(base64) {
var binary = atob(base64);
var len = binary.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
return bytes.buffer;
}
// Draw a test rectangle to verify WebGL pipeline works
function _renderTestRect() {
console.log('[Live2D] Rendering test rectangle...');
var testVerts = new Float32Array([
-0.5, 0.5, 0, 0,
0.5, 0.5, 1, 0,
-0.5, -0.5, 0, 1,
0.5, -0.5, 1, 1
]);
var testIdx = new Uint16Array([0, 1, 2, 1, 3, 2]);
var vbuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbuf);
gl.bufferData(gl.ARRAY_BUFFER, testVerts, gl.STATIC_DRAW);
var ibuf = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuf);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, testIdx, gl.STATIC_DRAW);
gl.useProgram(program);
var posLoc = gl.getAttribLocation(program, 'a_position');
var uvLoc = gl.getAttribLocation(program, 'a_uv');
gl.enableVertexAttribArray(posLoc);
gl.enableVertexAttribArray(uvLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 16, 0);
gl.vertexAttribPointer(uvLoc, 2, gl.FLOAT, false, 16, 8);
var mvp = new Float32Array([2,0,0,0, 0,-2,0,0, 0,0,1,0, 0,0,0,1]);
gl.uniformMatrix4fv(gl.getUniformLocation(program, 'u_mvp'), false, mvp);
// 1x1 white pixel texture
var whiteTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, whiteTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
new Uint8Array([255,255,255,255]));
gl.uniform1i(gl.getUniformLocation(program, 'u_texture'), 0);
gl.uniform4f(gl.getUniformLocation(program, 'u_color'), 1, 1, 1, 1);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuf);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
gl.deleteBuffer(vbuf);
gl.deleteBuffer(ibuf);
gl.deleteTexture(whiteTex);
console.log('[Live2D] Test rectangle rendered OK');
}
// Resize texture image to avoid GPU memory exhaustion
function _resizeTexture(img, maxSize, callback) {
if(img.width <= maxSize && img.height <= maxSize) {
callback(null, img);
return;
}
var scale = Math.min(maxSize / img.width, maxSize / img.height);
var w = Math.floor(img.width * scale);
var h = Math.floor(img.height * scale);
console.log('[Live2D] Resizing texture from ' + img.width + 'x' + img.height + ' to ' + w + 'x' + h);
var c = document.createElement('canvas');
c.width = w;
c.height = h;
var ctx = c.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
callback(null, c);
}
window.resizeLive2D = function(w, h) {
if(gl) {
canvas.width = w;
canvas.height = h;
gl.viewport(0, 0, w, h);
}
};
window.playMotion = function(name) {
console.log('[Live2D] playMotion:', name);
if(window.requestRender) window.requestRender();
};
window.setExpression = function(name) {
console.log('[Live2D] setExpression:', name);
if(window.requestRender) window.requestRender();
};
window.setRandomMotion = function() {
console.log('[Live2D] setRandomMotion');
if(window.requestRender) window.requestRender();
};
window._initLive2DAsync = async function(modelJson, mocBase64, mocFilename, assetsDir) {
if(_alreadyInitialized) {
console.log('[Live2D] Already initialized, skipping');
return;
}
_alreadyInitialized = true;
console.log('[Live2D] _initLive2DAsync called');
if(window.pybridge) window.pybridge.onJsMessage('init_start');
_assetsDir = assetsDir || '';
if(typeof Live2DCubismCore === 'undefined') {
console.error('[Live2D] Cubism Core not loaded');
if(window.pybridge) window.pybridge.onJsMessage('ERR:cubism_not_loaded');
return;
}
// Init WebGL
canvas.width = 200;
canvas.height = 200;
gl = canvas.getContext('webgl', {alpha: true, premultipliedAlpha: true})
|| canvas.getContext('experimental-webgl', {alpha: true, premultipliedAlpha: true});
if(!gl) {
console.error('[Live2D] WebGL not available');
if(window.pybridge) window.pybridge.onJsMessage('ERR:no_webgl');
return;
}
console.log('[Live2D] WebGL initialized, renderer:', gl.getParameter(gl.RENDERER));
if(window.pybridge) window.pybridge.onJsMessage('webgl_ok:' + gl.getParameter(gl.RENDERER));
gl.viewport(0, 0, 200, 200);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
_glError('init');
// Create shader program
var vs = createShader(gl.VERTEX_SHADER, VERTEX_SHADER);
var fs = createShader(gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
if(!vs || !fs) {
console.error('[Live2D] Shader creation failed');
if(window.pybridge) window.pybridge.onJsMessage('ERR:shader_compile');
return;
}
program = createProgram(vs, fs);
if(!program) {
console.error('[Live2D] Program creation failed');
if(window.pybridge) window.pybridge.onJsMessage('ERR:program_link');
return;
}
console.log('[Live2D] Shader program created');
if(window.pybridge) window.pybridge.onJsMessage('shader_ok');
// Verify WebGL works with a test rectangle
try {
_renderTestRect();
_glError('testRect');
if(window.pybridge) window.pybridge.onJsMessage('test_rect_ok');
} catch(e) {
console.error('[Live2D] Test rect failed:', e.message);
if(window.pybridge) window.pybridge.onJsMessage('ERR:test_rect:' + e.message);
return;
}
// Decode moc
console.log('[Live2D] Decoding moc...');
if(window.pybridge) window.pybridge.onJsMessage('decoding_moc');
var mocBuf = base64ToArrayBuffer(mocBase64);
console.log('[Live2D] moc buffer size:', mocBuf.byteLength);
var moc = Live2DCubismCore.Moc.fromArrayBuffer(mocBuf);
if(!moc) {
console.error('[Live2D] Moc load failed');
if(window.pybridge) window.pybridge.onJsMessage('ERR:moc_load');
return;
}
model = Live2DCubismCore.Model.fromMoc(moc);
if(!model) {
console.error('[Live2D] Model build failed');
if(window.pybridge) window.pybridge.onJsMessage('ERR:model_build');
return;
}
window._live2d_model = model;
window._live2d_drawables = model.drawables;
console.log('[Live2D] Model created, drawables:', model.drawables.count);
if(window.pybridge) window.pybridge.onJsMessage('model_ok:d=' + model.drawables.count);
// Load textures (resized to avoid GPU memory exhaustion)
var textureUrls = modelJson.FileReferences.Textures;
console.log('[Live2D] Loading', textureUrls.length, 'textures...');
if(window.pybridge) window.pybridge.onJsMessage('loading_textures:n=' + textureUrls.length);
window._loadedTextures = [];
for(var i = 0; i < textureUrls.length; i++) {
var url = (_assetsDir ? _assetsDir + '/' : '') + textureUrls[i];
console.log('[Live2D] Loading texture ' + i + ': ' + url);
try {
var rawImg = await new Promise(function(resolve, reject) {
var img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function() { resolve(img); };
img.onerror = function(e) { reject(e); };
img.src = url;
});
console.log('[Live2D] Texture ' + i + ' raw size:', rawImg.width, 'x', rawImg.height);
// Resize to avoid GPU memory pressure (4096x4096 = 64MB per texture)
var texSource = await new Promise(function(resolve, reject) {
_resizeTexture(rawImg, MAX_TEXTURE_SIZE, function(err, src) {
if(err) reject(err); else resolve(src);
});
});
rawImg = null; // release full-size 64MB bitmap for GC
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texSource);
if(_glError('texture' + i)) {
console.error('[Live2D] GL error uploading texture ' + i);
}
window._loadedTextures.push(tex);
console.log('[Live2D] Texture', i, 'uploaded OK');
if(window.pybridge) window.pybridge.onJsMessage('texture_ok:' + i);
// Small delay between textures to let GPU breathe
await new Promise(function(r) { setTimeout(r, 50); });
} catch(e) {
console.error('[Live2D] Texture load error:', e);
if(window.pybridge) window.pybridge.onJsMessage('ERR:texture:' + i + ':' + e);
}
}
console.log('[Live2D] Textures loaded: ' + window._loadedTextures.length);
if(window.pybridge) window.pybridge.onJsMessage('textures_done:n=' + window._loadedTextures.length);
// Pre-build cached buffers
if(window.pybridge) window.pybridge.onJsMessage('building_buffers');
try {
_buildCachedBuffers();
console.log('[Live2D] Cached buffers built: ' + _cachedBuffers.length);
if(window.pybridge) window.pybridge.onJsMessage('buffers_ok:n=' + _cachedBuffers.length);
} catch(e) {
console.error('[Live2D] Buffer build error:', e.message);
if(window.pybridge) window.pybridge.onJsMessage('ERR:buffers:' + e.message);
return;
}
ready = true;
document.title = 'LIVE2D_OK:d=' + model.drawables.count;
console.log('[Live2D] Ready, scheduling first render...');
if(window.pybridge) window.pybridge.onJsMessage('scheduling_render');
// Render first frame via setTimeout to avoid GPU calls during init
setTimeout(function() {
if(ready && gl && model) {
try {
renderFrame();
console.log('[Live2D] First frame rendered OK');
// Flush GPU command buffer and finish all pending operations.
// If the crash is from an async GPU operation (compositing/present),
// this will move the crash into this callback, before first_frame_ok is sent.
gl.flush();
gl.finish();
console.log('[Live2D] GPU flush+finish complete');
if(window.pybridge) window.pybridge.onJsMessage('first_frame_ok');
} catch(e) {
console.error('[Live2D] First frame error:', e.message);
if(window.pybridge) window.pybridge.onJsMessage('ERR:first_frame:' + e.message);
}
}
}, 50);
// Notify Python side
if(window.pybridge) {
window.pybridge.onJsMessage('ready');
}
};
var _cachedBuffers = [];
function _buildCachedBuffers() {
console.log('[Live2D] Building cached buffers for ' + model.drawables.count + ' drawables...');
var drawables = model.drawables;
_cachedBuffers = [];
var successCount = 0;
for(var i = 0; i < drawables.count; i++) {
try {
var vc = drawables.vertexCounts[i];
var ic = drawables.indexCounts[i];
if(vc <= 0 || ic <= 0) {
_cachedBuffers.push(null);
continue;
}
var vertexOffset = 0;
var indexOffset = 0;
for(var j = 0; j < i; j++) {
vertexOffset += drawables.vertexCounts[j];
indexOffset += drawables.indexCounts[j];
}
var vdata = new Float32Array(vc * 4);
var idata = new Uint16Array(ic);
var posArr = drawables.vertexPositions;
var uvArr = drawables.vertexUvs;
var idxArr = drawables.indices;
for(var v = 0; v < vc; v++) {
var px = posArr[(vertexOffset + v) * 2];
var py = posArr[(vertexOffset + v) * 2 + 1];
if(isNaN(px) || isNaN(py)) { px = 0; py = 0; }
vdata[v * 4] = px;
vdata[v * 4 + 1] = py;
vdata[v * 4 + 2] = uvArr[(vertexOffset + v) * 2];
vdata[v * 4 + 3] = uvArr[(vertexOffset + v) * 2 + 1];
}
for(var ii = 0; ii < ic; ii++) {
idata[ii] = idxArr[indexOffset + ii];
}
var vbuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbuf);
gl.bufferData(gl.ARRAY_BUFFER, vdata, gl.DYNAMIC_DRAW);
var ibuf = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuf);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, idata, gl.STATIC_DRAW);
_cachedBuffers.push({vbuf: vbuf, ibuf: ibuf, vc: vc, ic: ic});
successCount++;
} catch(e) {
console.error('[Live2D] Buffer build error for drawable ' + i + ':', e.message);
_cachedBuffers.push(null);
}
}
console.log('[Live2D] Built ' + successCount + '/' + drawables.count + ' buffers');
_glError('buildBuffers');
}
function renderFrame() {
if(!ready || !gl || !model) return;
try {
var w = canvas.width || 200;
var h = canvas.height || 200;
gl.viewport(0, 0, w, h);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
var drawables = model.drawables;
if(!drawables || !drawables.count || drawables.count <= 0) return;
if(!drawables.vertexPositions || !drawables.vertexUvs || !drawables.indices) return;
if(_cachedBuffers.length !== drawables.count) {
_buildCachedBuffers();
}
var count = drawables.count;
var sortedIndices = [];
for(var i = 0; i < count; i++) sortedIndices.push(i);
sortedIndices.sort(function(a, b) {
return drawables.drawOrders[a] - drawables.drawOrders[b];
});
var posArr = drawables.vertexPositions;
var uvArr = drawables.vertexUvs;
// Cache uniform/program locations once per frame
gl.useProgram(program);
var posLoc = gl.getAttribLocation(program, 'a_position');
var uvLoc = gl.getAttribLocation(program, 'a_uv');
var mvpLoc = gl.getUniformLocation(program, 'u_mvp');
var colorLoc = gl.getUniformLocation(program, 'u_color');
var texLoc = gl.getUniformLocation(program, 'u_texture');
var sx = 2.0 / w;
var sy = 2.0 / h;
var mvp = new Float32Array([sx, 0, 0, 0, 0, -sy, 0, 0, 0, 0, 1, 0, -1, 1, 0, 1]);
gl.uniformMatrix4fv(mvpLoc, false, mvp);
gl.uniform1i(texLoc, 0);
var renderedCount = 0;
for(var si = 0; si < sortedIndices.length; si++) {
var i = sortedIndices[si];
var vc = drawables.vertexCounts[i];
var ic = drawables.indexCounts[i];
var texIdx = drawables.textureIndices[i];
var opacity = drawables.opacities[i];
if(vc <= 0 || ic <= 0) continue;
if(texIdx < 0 || texIdx >= window._loadedTextures.length) continue;
if(opacity <= 0) continue;
var cached = _cachedBuffers[i];
if(!cached) continue;
var tex = window._loadedTextures[texIdx];
if(!tex) continue;
gl.bindBuffer(gl.ARRAY_BUFFER, cached.vbuf);
gl.enableVertexAttribArray(posLoc);
gl.enableVertexAttribArray(uvLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 16, 0);
gl.vertexAttribPointer(uvLoc, 2, gl.FLOAT, false, 16, 8);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cached.ibuf);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.uniform4f(colorLoc, 1, 1, 1, opacity);
gl.drawElements(gl.TRIANGLES, ic, gl.UNSIGNED_SHORT, 0);
renderedCount++;
}
if(renderedCount === 0) {
console.warn('[Live2D] Nothing rendered (check drawables, textures, opacities)');
}
} catch(e) {
console.error('[Live2D] Render error:', e.message, e.stack);
}
}
// Render on demand (no continuous timer)
window.requestRender = function() {
if(!ready || _renderRequested) return;
_renderRequested = true;
setTimeout(function() {
_renderRequested = false;
if(ready && gl && model) {
try { renderFrame(); } catch(e) { console.error('[Live2D] requestRender error:', e.message); }
}
}, 16);
};
// Auto-init: wait for model data then call init
function tryAutoInit() {
if(typeof Live2DCubismCore === 'undefined') {
setTimeout(tryAutoInit, 100);
return;
}
if(!window._modelJson || !window._mocBase64) {
setTimeout(tryAutoInit, 100);
return;
}
console.log('[Live2D] Auto-init: calling _initLive2DAsync');
window._initLive2DAsync(window._modelJson, window._mocBase64, window._mocFileName || '', window._assetsDir || '');
}
// Initialize QWebChannel bridge (qt.webChannelTransport auto-injected by Qt)
function _setupChannel() {
if(typeof QWebChannel === 'undefined') {
// qwebchannel.js not loaded yet; retry
setTimeout(_setupChannel, 50);
return;
}
if(!window.qt || !window.qt.webChannelTransport) {
setTimeout(_setupChannel, 50);
return;
}
try {
new QWebChannel(qt.webChannelTransport, function(channel) {
window.pybridge = channel.objects.pybridge;
console.log('[Live2D] QWebChannel ready, pybridge=' + typeof window.pybridge);
// Now safe to auto-init
tryAutoInit();
});
} catch(e) {
console.error('[Live2D] QWebChannel setup error:', e);
}
}
_setupChannel();
// Expose for Python (called from runJavaScript injection)
window.startLive2D = function() {
if(window._modelJson && window._mocBase64) {
window._initLive2DAsync(window._modelJson, window._mocBase64, window._mocFileName || '', window._assetsDir || '');
}
};
})();
</script>
</body>
</html>