#!/usr/bin/env node // Playwright harness: load viewer_gaussian.html in headless Chromium, push the // user's PLY through LOAD_MESH_DATA, then introspect the rendered canvas. // // Usage: node debug_viewer.mjs [path/to/file.ply] import http from 'node:http'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const VIEWER_DIR = path.resolve(__dirname, '..', 'web'); const PLY_PATH = process.argv[2] || '/home/work/lito/ComfyUI/output/lito_output.ply'; const PORT = 18432; const MIME = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', '.ply': 'application/octet-stream', }; function startStaticServer() { return new Promise((resolve) => { const server = http.createServer((req, res) => { const u = decodeURIComponent(req.url.split('?')[0]); let filePath; if (u === '/test.ply') filePath = PLY_PATH; else { filePath = path.normalize(path.join(VIEWER_DIR, u)); if (!filePath.startsWith(VIEWER_DIR)) { res.writeHead(403); res.end(); return; } } fs.stat(filePath, (err, st) => { if (err || !st.isFile()) { res.writeHead(404); res.end('not found'); return; } const ext = path.extname(filePath).toLowerCase(); res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream', 'Content-Length': st.size, 'Cache-Control': 'no-cache', }); fs.createReadStream(filePath).pipe(res); }); }); server.listen(PORT, '127.0.0.1', () => { const a = server.address(); console.log(`[harness] static server: http://127.0.0.1:${a.port}/`); resolve(server); }); }); } async function main() { const playwrightPath = process.env.PLAYWRIGHT_NODE || '/home/andrej/.local/share/uv/python/cpython-3.13.7-linux-x86_64-gnu/lib/python3.13/site-packages/playwright/driver/package'; // Resolve playwright via the python install's node bundle. const { chromium } = await import(`${playwrightPath}/lib/cjs/playwright.js`).catch(() => import('playwright')); const server = await startStaticServer(); const browser = await chromium.launch({ headless: true, args: ['--use-gl=swiftshader', '--ignore-gpu-blocklist', '--enable-webgl'], }); const ctx = await browser.newContext({ viewport: { width: 1024, height: 768 } }); const page = await ctx.newPage(); page.on('console', m => { const t = m.type(); if (t === 'log') console.log(` ${m.text()}`); else console.log(` [${t}] ${m.text()}`); }); page.on('pageerror', e => console.log(` [pageerror] ${e.message}`)); console.log('[harness] navigating to viewer...'); await page.goto(`http://127.0.0.1:${PORT}/viewer_gaussian.html`, { waitUntil: 'load' }); await page.waitForFunction(() => typeof window.__gs === 'object'); console.log(`[harness] fetching PLY: ${PLY_PATH}`); const t0 = Date.now(); const result = await page.evaluate(async () => { window._mesh_loaded = false; window._mesh_error = null; window.addEventListener('message', e => { if (e.data && e.data.type === 'MESH_LOADED') window._mesh_loaded = true; if (e.data && e.data.type === 'MESH_ERROR') window._mesh_error = e.data.error; }); const resp = await fetch('/test.ply'); if (!resp.ok) throw new Error('fetch /test.ply failed: ' + resp.status); const buf = await resp.arrayBuffer(); window.postMessage({ type: 'LOAD_MESH_DATA', data: buf, filename: 'test.ply', renderer: 'webgl2', }, '*'); return { bytes: buf.byteLength }; }); console.log(`[harness] handed off ${(result.bytes / 1e6).toFixed(1)} MB to the iframe`); console.log('[harness] waiting for MESH_LOADED or MESH_ERROR...'); try { await page.waitForFunction(() => window._mesh_loaded || window._mesh_error, { timeout: 120000 }); } catch (e) { console.log('[harness] TIMED OUT waiting for load:', e.message); } const err = await page.evaluate(() => window._mesh_error); if (err) console.log(`[harness] viewer reported MESH_ERROR: ${err}`); console.log(`[harness] load roundtrip: ${((Date.now()-t0)/1000).toFixed(1)} s`); // Let the renderer pump frames. await new Promise(r => setTimeout(r, 2500)); const state = await page.evaluate(() => window.__gs.state()); console.log('[harness] viewer state:', JSON.stringify(state, null, 2)); // ---- Probe A: pixel histogram via gl.readPixels -------------- const px = await page.evaluate(() => { const c = document.getElementById('canvas'); const gl = c.getContext('webgl2') || c.getContext('webgl'); const w = c.width, h = c.height; const pixels = new Uint8Array(w * h * 4); gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels); let nonBlack = 0, rSum = 0, gSum = 0, bSum = 0, aSum = 0, max = 0; for (let i = 0; i < pixels.length; i += 4) { const r = pixels[i], g = pixels[i+1], b = pixels[i+2], a = pixels[i+3]; if (r > 30 || g > 30 || b > 30) nonBlack++; const m = Math.max(r, g, b); if (m > max) max = m; rSum += r; gSum += g; bSum += b; aSum += a; } const total = pixels.length / 4; return { wh: [w, h], client: [c.clientWidth, c.clientHeight], nonBlackPx: nonBlack, totalPx: total, nonBlackPct: (nonBlack / total * 100).toFixed(2), avg: [rSum / total, gSum / total, bSum / total, aSum / total].map(v => +v.toFixed(2)), max, ctxLost: gl.isContextLost(), vendor: gl.getParameter(gl.VENDOR), renderer: gl.getParameter(gl.RENDERER), }; }); console.log('[harness] pixel histogram:', JSON.stringify(px, null, 2)); // ---- Probe B: splat data sanity ------------------------------ const splatSample = await page.evaluate(() => { return window.__gs.run(` if (!splatMesh) return { error: "no splatMesh" }; const n = splatMesh.getSplatCount(); if (!n) return { error: "splatCount 0" }; const v = new THREE.Vector3(); const c = new Uint8Array(4); const out = { n }; const idxs = [0, Math.floor(n/4), Math.floor(n/2), Math.floor(3*n/4), n-1]; out.samples = idxs.map(i => { let pos = null, col = null, err = null; try { splatMesh.getSplatCenter(i, v); pos = [v.x, v.y, v.z]; } catch (e) { err = 'center: ' + e.message; } try { splatMesh.getSplatColor(i, c); col = [c[0], c[1], c[2], c[3]]; } catch (e) { err = (err ? err + '; ' : '') + 'color: ' + e.message; } return { i, pos, col, err }; }); // Try getSplatScale & getSplatRotation if available try { out.hasGetSplatScale = !!splatMesh.getSplatScale; } catch (e) {} // Inspect splatBuffer for SH degree try { const sb = splatMesh.splatBuffer || splatMesh.splatBuffers && splatMesh.splatBuffers[0]; if (sb) { out.splatBufferKeys = Object.keys(sb).filter(k => !k.startsWith('_')).slice(0, 40); out.minSHDegree = sb.minSphericalHarmonicsDegree; out.maxSHDegree = sb.maxSphericalHarmonicsDegree; out.outSHDegree = sb.outSphericalHarmonicsDegree; out.sceneCount = sb.getSceneCount && sb.getSceneCount(); out.splatCount = sb.getSplatCount && sb.getSplatCount(); } } catch (e) { out.splatBufferErr = e.message; } // Check material try { const mat = splatMesh.material; out.matType = mat && mat.type; out.matVisible = mat && mat.visible; out.matTransparent = mat && mat.transparent; out.uniforms = mat && mat.uniforms ? Object.keys(mat.uniforms) : null; out.matVertexShaderLen = mat && mat.vertexShader && mat.vertexShader.length; out.matFragmentShaderLen = mat && mat.fragmentShader && mat.fragmentShader.length; out.matProgram = !!(mat && mat.program); } catch (e) { out.matErr = e.message; } // Mesh in scene? try { out.meshVisible = splatMesh.visible; out.meshInScene = !!splatMesh.parent; out.meshParentType = splatMesh.parent && splatMesh.parent.type; out.meshPos = [splatMesh.position.x, splatMesh.position.y, splatMesh.position.z]; const bb = splatMesh.geometry && splatMesh.geometry.boundingBox; if (bb) out.geomBB = { min: bb.min.toArray(), max: bb.max.toArray() }; else out.geomBB = null; } catch (e) { out.meshErr = e.message; } return out; `); }); console.log('[harness] splat data probe:', JSON.stringify(splatSample, null, 2)); // ---- Probe C: scene walk ------------------------------------ const sceneInfo = await page.evaluate(() => { return window.__gs.run(` const scene = viewer && viewer.threeScene; if (!scene) return { error: "no scene" }; const out = { childCount: scene.children.length, children: [] }; scene.traverse((o) => { out.children.push({ type: o.type, name: o.name, visible: o.visible, pos: [o.position.x, o.position.y, o.position.z], }); }); return out; `); }); console.log('[harness] three.js scene:', JSON.stringify(sceneInfo, null, 2)); const shotPath = '/tmp/viewer_debug.png'; await page.screenshot({ path: shotPath, fullPage: false }); console.log(`[harness] screenshot saved: ${shotPath}`); await browser.close(); server.close(); } main().catch(e => { console.error('[harness] FATAL:', e); process.exit(1); });