230 lines
9.4 KiB
Python
230 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Playwright harness that drives viewer_gaussian.html in headless Chromium and
|
|
prints diagnostics about why the canvas may be blank.
|
|
|
|
usage: python3 debug_viewer.py [path/to/file.ply]
|
|
"""
|
|
import json
|
|
import mimetypes
|
|
import os
|
|
import pathlib
|
|
import socketserver
|
|
import sys
|
|
import threading
|
|
import time
|
|
from http.server import SimpleHTTPRequestHandler
|
|
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
HERE = pathlib.Path(__file__).resolve().parent
|
|
VIEWER_DIR = (HERE.parent / "web").resolve()
|
|
PLY_PATH = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else pathlib.Path(
|
|
"/home/work/lito/ComfyUI/output/lito_output.ply"
|
|
)
|
|
PORT = 18432
|
|
|
|
# Allow the .ply route to map to PLY_PATH.
|
|
mimetypes.add_type("application/octet-stream", ".ply")
|
|
|
|
|
|
class Handler(SimpleHTTPRequestHandler):
|
|
def translate_path(self, p):
|
|
# Route /test.ply to our outside-the-webdir file.
|
|
clean = p.split("?", 1)[0]
|
|
if clean == "/test.ply":
|
|
return str(PLY_PATH)
|
|
return super().translate_path(p)
|
|
|
|
def log_message(self, fmt, *args):
|
|
pass # silence
|
|
|
|
|
|
def start_server():
|
|
Handler.directory = str(VIEWER_DIR)
|
|
|
|
class _H(Handler):
|
|
def __init__(self, *a, **kw):
|
|
super().__init__(*a, directory=str(VIEWER_DIR), **kw)
|
|
|
|
httpd = socketserver.TCPServer(("127.0.0.1", PORT), _H)
|
|
t = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
t.start()
|
|
print(f"[harness] static server: http://127.0.0.1:{PORT}/")
|
|
return httpd
|
|
|
|
|
|
def main():
|
|
if not PLY_PATH.exists():
|
|
print(f"[harness] PLY not found: {PLY_PATH}")
|
|
sys.exit(1)
|
|
print(f"[harness] PLY: {PLY_PATH} ({PLY_PATH.stat().st_size / 1e6:.1f} MB)")
|
|
print(f"[harness] viewer: {VIEWER_DIR}")
|
|
httpd = start_server()
|
|
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.launch(
|
|
headless=True,
|
|
args=["--use-gl=swiftshader", "--ignore-gpu-blocklist", "--enable-webgl"],
|
|
)
|
|
ctx = browser.new_context(viewport={"width": 1024, "height": 768})
|
|
page = ctx.new_page()
|
|
|
|
def on_console(m):
|
|
t = m.type
|
|
tag = f"[{t}]" if t != "log" else ""
|
|
print(f" {tag} {m.text}")
|
|
|
|
page.on("console", on_console)
|
|
page.on("pageerror", lambda e: print(f" [pageerror] {e}"))
|
|
|
|
print("[harness] navigating to viewer...")
|
|
page.goto(f"http://127.0.0.1:{PORT}/viewer_gaussian.html",
|
|
wait_until="load", timeout=30000)
|
|
page.wait_for_function("typeof window.__gs === 'object'")
|
|
|
|
print("[harness] sending LOAD_MESH_DATA...")
|
|
t0 = time.monotonic()
|
|
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',
|
|
}, '*');
|
|
}""")
|
|
print("[harness] waiting for load (timeout 180s)...")
|
|
try:
|
|
page.wait_for_function("window._mesh_loaded || window._mesh_error",
|
|
timeout=180000)
|
|
except Exception as e:
|
|
print(f"[harness] TIMED OUT waiting for load: {e}")
|
|
err = page.evaluate("window._mesh_error")
|
|
if err:
|
|
print(f"[harness] viewer reported MESH_ERROR: {err}")
|
|
print(f"[harness] roundtrip: {time.monotonic() - t0:.1f} s")
|
|
|
|
# Pump frames for a moment so any deferred upload finishes.
|
|
page.wait_for_timeout(2500)
|
|
|
|
state = page.evaluate("window.__gs.state()")
|
|
print("[harness] viewer state:\n" + json.dumps(state, indent=2))
|
|
|
|
# ---- Probe: pixel histogram ---------------------------------
|
|
hist_js = """() => {
|
|
const c = document.getElementById('canvas');
|
|
const gl = c.getContext('webgl2') || c.getContext('webgl');
|
|
if (!gl) return { error: 'no gl context' };
|
|
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),
|
|
};
|
|
}"""
|
|
print("[harness] pixel histogram:\n" +
|
|
json.dumps(page.evaluate(hist_js), indent=2))
|
|
|
|
# ---- Probe: splat data ------------------------------------
|
|
splat_js = r"""
|
|
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 {
|
|
const sb = splatMesh.splatBuffer || (splatMesh.splatBuffers && splatMesh.splatBuffers[0]);
|
|
if (sb) {
|
|
out.splatBufferKeys = Object.keys(sb).filter(k => !k.startsWith('_')).slice(0, 60);
|
|
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; }
|
|
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; }
|
|
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;
|
|
out.geomBB = bb ? { min: bb.min.toArray(), max: bb.max.toArray() } : null;
|
|
} catch (e) { out.meshErr = e.message; }
|
|
return out;
|
|
"""
|
|
print("[harness] splat probe:\n" +
|
|
json.dumps(page.evaluate(f"window.__gs.run({json.dumps(splat_js)})"), indent=2))
|
|
|
|
# ---- Probe: scene walk ------------------------------------
|
|
scene_js = r"""
|
|
const scene = viewer && viewer.threeScene;
|
|
if (!scene) return { error: "no threeScene" };
|
|
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;
|
|
"""
|
|
print("[harness] scene:\n" +
|
|
json.dumps(page.evaluate(f"window.__gs.run({json.dumps(scene_js)})"), indent=2))
|
|
|
|
# Screenshot of the iframe / page.
|
|
shot = "/tmp/viewer_debug.png"
|
|
page.screenshot(path=shot, full_page=False)
|
|
print(f"[harness] screenshot: {shot}")
|
|
|
|
browser.close()
|
|
httpd.shutdown()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|