Files
2026-05-15 08:18:29 +02:00

210 lines
7.9 KiB
Python

#!/usr/bin/env python3
"""Probe the loaded viewer: gating flags, sort state, splat tree, geometry, and
force-render. Pass the probe JS as a JSON-encoded string to avoid wrestling
with shell + JS-template-literal escaping."""
import json
import sys
import time
import pathlib
import socketserver
import threading
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 = 18435
class _H(SimpleHTTPRequestHandler):
def translate_path(self, p):
clean = p.split("?", 1)[0]
if clean == "/test.ply":
return str(PLY_PATH)
return super().translate_path(p)
def log_message(self, *a): pass
def __init__(self, *a, **kw):
super().__init__(*a, directory=str(VIEWER_DIR), **kw)
PROBE_JS = r"""
const out = {};
const r = viewer.renderer;
// Renderer baseline
out.rendererInfo = JSON.parse(JSON.stringify(r.info));
out.renderTarget = !!r.getRenderTarget();
out.viewport = r.getViewport(new THREE.Vector4()).toArray();
out.size = r.getSize(new THREE.Vector2()).toArray();
out.dprSize = r.getDrawingBufferSize(new THREE.Vector2()).toArray();
out.autoClear = r.autoClear;
// Gating flags
out.gates = {
initialized: viewer.initialized,
splatRenderReady: viewer.splatRenderReady,
disposing: viewer.disposing,
disposed: viewer.disposed,
sortRunning: viewer.sortRunning,
selfDrivenModeRunning: viewer.selfDrivenModeRunning,
};
// Camera
out.camera = {
pos: viewer.camera.position.toArray(),
quat: viewer.camera.quaternion.toArray(),
near: viewer.camera.near, far: viewer.camera.far,
fov: viewer.camera.fov, aspect: viewer.camera.aspect,
};
// splatMesh geometry
const g = splatMesh.geometry;
out.geom = {
type: splatMesh.type,
geomType: g.type,
drawRangeBefore: { start: g.drawRange.start, count: g.drawRange.count },
instanceCountBefore: g.instanceCount,
visible: splatMesh.visible,
inScene: !!splatMesh.parent,
frustumCulled: splatMesh.frustumCulled,
};
out.geom.attrs = {};
for (const k of Object.keys(g.attributes || {})) {
const a = g.attributes[k];
out.geom.attrs[k] = { itemSize: a.itemSize, count: a.count, instanced: !!a.isInstancedBufferAttribute };
}
// Sort state
out.sortState = {
splatRenderCount: viewer.splatRenderCount,
lastSplatSortCount: viewer.lastSplatSortCount,
lastSortTime: viewer.lastSortTime,
gpuAcceleratedSort: viewer.gpuAcceleratedSort,
};
// Splat tree
try {
const tree = splatMesh.getSplatTree && splatMesh.getSplatTree();
if (!tree) {
out.splatTree = "NULL (sorter falls back to whole-buffer path)";
} else {
out.splatTree = {
subTrees: (tree.subTrees || []).map(function (st, i) {
const nodes = st.nodesWithIndexes || [];
const sample = nodes[0] ? {
center: nodes[0].center && nodes[0].center.toArray(),
min: nodes[0].min && nodes[0].min.toArray(),
max: nodes[0].max && nodes[0].max.toArray(),
indexesLen: nodes[0].data && nodes[0].data.indexes && nodes[0].data.indexes.length,
} : null;
let withIdx = 0, totalIdx = 0;
for (const n of nodes) {
if (n.data && n.data.indexes) { withIdx++; totalIdx += n.data.indexes.length; }
}
return { subTreeIdx: i, nodeCount: nodes.length, nodesWithIndexes: withIdx, totalIndexes: totalIdx, sample };
}),
};
}
} catch (e) {
out.splatTreeErr = e.message;
}
// Probe a known-good draw (proves the renderer/canvas pipeline is healthy)
try {
const probeGeo = new THREE.PlaneGeometry(2, 2);
const probeMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const probeMesh = new THREE.Mesh(probeGeo, probeMat);
const probeScene = new THREE.Scene();
probeScene.add(probeMesh);
const probeCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
probeCam.position.z = 1;
r.info.reset();
r.render(probeScene, probeCam);
out.probeRender = JSON.parse(JSON.stringify(r.info.render));
} catch (e) { out.probeRenderErr = e.message; }
// Force a sort and wait for the worker to finish
return new Promise(function (resolve) {
viewer.runSplatSort(true, true).then(async function (sortRunning) {
out.runSplatSortReturned = sortRunning;
const t0 = performance.now();
while (performance.now() - t0 < 3000) {
if (!viewer.sortRunning && g.drawRange.count > 0) break;
await new Promise(function (rr) { setTimeout(rr, 50); });
}
out.afterForcedSort = {
splatRenderCount: viewer.splatRenderCount,
lastSplatSortCount: viewer.lastSplatSortCount,
lastSortTime: viewer.lastSortTime,
sortRunning: viewer.sortRunning,
drawRangeAfter: { start: g.drawRange.start, count: g.drawRange.count },
instanceCountAfter: g.instanceCount,
};
// Force one render now that the sort should have populated drawRange
r.info.reset();
try { r.render(splatMesh, viewer.camera); } catch (e) { out.afterRenderErr = e.message; }
out.afterForcedSortRender = JSON.parse(JSON.stringify(r.info.render));
out.matProgramFinal = !!(splatMesh.material && splatMesh.material.program);
resolve(out);
});
});
"""
def main():
socketserver.TCPServer.allow_reuse_address = True
httpd = socketserver.TCPServer(("127.0.0.1", PORT), _H)
threading.Thread(target=httpd.serve_forever, daemon=True).start()
with sync_playwright() as p:
browser = p.chromium.launch(headless=True,
args=["--use-gl=swiftshader", "--ignore-gpu-blocklist"])
ctx = browser.new_context(viewport={"width": 1024, "height": 768})
page = ctx.new_page()
page.on("console", lambda m: print(f" {m.text}") if "frames" not in m.text else None)
page.goto(f"http://127.0.0.1:{PORT}/viewer_gaussian.html", wait_until="load")
page.wait_for_function("typeof window.__gs === 'object'")
page.evaluate("""async () => {
window._loaded = false; window._error = null;
addEventListener('message', e => {
if (e.data?.type === 'MESH_LOADED') window._loaded = true;
if (e.data?.type === 'MESH_ERROR') window._error = e.data.error;
});
const r = await fetch('/test.ply'); const buf = await r.arrayBuffer();
postMessage({ type:'LOAD_MESH_DATA', data: buf, filename:'t.ply', renderer:'webgl2' }, '*');
}""")
page.wait_for_function("window._loaded || window._error", timeout=180000)
page.wait_for_timeout(1500)
# Pass probe JS as a function arg -> no template-literal headaches.
probe = page.evaluate("(src) => window.__gs.run(src)", PROBE_JS)
print("[probe]\n" + json.dumps(probe, indent=2))
hist = page.evaluate("""() => {
const c = document.getElementById('canvas');
const gl = c.getContext('webgl2') || c.getContext('webgl');
const pixels = new Uint8Array(c.width * c.height * 4);
gl.readPixels(0, 0, c.width, c.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
let nb = 0, max = 0;
for (let i = 0; i < pixels.length; i += 4) {
const m = Math.max(pixels[i], pixels[i+1], pixels[i+2]);
if (m > 30) nb++; if (m > max) max = m;
}
return { nonBlackPx: nb, totalPx: pixels.length/4, max };
}""")
print("[hist after manual render]:", hist)
page.screenshot(path="/tmp/viewer_probe.png")
print("[shot] /tmp/viewer_probe.png")
browser.close()
httpd.shutdown()
if __name__ == "__main__":
main()