diff --git a/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs b/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs index 4f7039f..2ea1dac 100644 --- a/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs +++ b/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs @@ -371,6 +371,8 @@ fn run_visible_preview( let mut rendered = false; let mut redraw_requested = false; let mut frame_number = 0_u32; + let mut frame_event_capacity = None; + let mut frame_allocations = 0_u64; let mut base_conditional_generation = 0_u64; let runtime_diagnostics = env::var_os("POLYMODEL_LDRAW_RUNTIME_DIAGNOSTICS").is_some(); let mut resized = false; @@ -485,8 +487,14 @@ fn run_visible_preview( std::process::exit(1); } Event::RedrawRequested(_) => { + let frame_started = std::time::Instant::now(); redraw_requested = false; let mut frame = frame_generator.generate(&context); + let event_capacity = frame.events.capacity(); + if frame_event_capacity.is_some_and(|capacity| event_capacity > capacity) { + frame_allocations += 1; + } + frame_event_capacity = Some(event_capacity); if frame_number == 0 { eprintln!( "[ldraw-preview] viewport requested={}x{} actual={}x{}", @@ -849,6 +857,13 @@ fn run_visible_preview( loop_trace.finish(NativeTerminal::Failure(format!("swap buffers: {error}"))); *control_flow = ControlFlow::Exit; } else { + eprintln!( + "[ldraw-metrics] phase=frame frame={} latency_us={} allocations={} event_capacity={}", + frame_number, + frame_started.elapsed().as_micros(), + frame_allocations, + event_capacity, + ); if rendered && frame_number == 1 { loop_trace.record(NativeStage::FirstSwapComplete); } diff --git a/crates/polymodel-renderer-worker/src/lib.rs b/crates/polymodel-renderer-worker/src/lib.rs index ade0c46..654a7a8 100644 --- a/crates/polymodel-renderer-worker/src/lib.rs +++ b/crates/polymodel-renderer-worker/src/lib.rs @@ -242,11 +242,27 @@ pub fn adapt_ldraw_parse_with_resources( result: &OwnedParseResult, resources: Option<&[polymodel_renderer_protocol::ModelResource]>, ) -> Result { + let started = Instant::now(); let models = result.models.as_slice(); if models.is_empty() { return Err(RendererError::parse("parse emitted no root model")); } - adapt_ldraw_assembly(models, Some(&result.result.scene), Some(result), resources) + let scene = adapt_ldraw_assembly(models, Some(&result.result.scene), Some(result), resources)?; + if std::env::var_os("POLYMODEL_LDRAW_TIMING").is_some() { + eprintln!( + "[ldraw-metrics] phase=adapt-ldraw elapsed_ms={} geometries={} materials={} textures={} occurrences={} points={} triangles={} lines={} conditional_lines={}", + started.elapsed().as_millis(), + scene.geometries.len(), + scene.materials.len(), + scene.textures.len(), + scene.stats.occurrences, + scene.stats.points, + scene.stats.triangles, + scene.stats.lines, + scene.stats.conditional_lines, + ); + } + Ok(scene) } #[derive(Clone)] @@ -510,6 +526,8 @@ fn adapt_ldraw_assembly( let mut geometries = Vec::with_capacity(models.len()); let mut provenance = Vec::with_capacity(models.len()); let mut uv_cache = HashMap::<(GeometryId, u64, u64), Rc>>::new(); + let mut uv_cache_hits = 0_u64; + let mut uv_cache_misses = 0_u64; let mut texture_ids = HashMap::<(RootId, String, String), TextureId>::new(); let mut textures = Vec::::new(); let mut texture_for = |root: RootId, @@ -804,8 +822,10 @@ fn adapt_ldraw_assembly( let texmap_hash = texmap_hasher.finish(); let cache_key = (GeometryId(node.0), transform_hash, texmap_hash); let occurrence_uvs = if let Some(uvs) = uv_cache.get(&cache_key) { + uv_cache_hits += 1; uvs.clone() } else { + uv_cache_misses += 1; let uvs = Rc::new(occurrence_texmap_uvs( texmap, transform, @@ -996,6 +1016,16 @@ fn adapt_ldraw_assembly( ); } } + if std::env::var_os("POLYMODEL_LDRAW_TIMING").is_some() { + let total = uv_cache_hits + uv_cache_misses; + let hit_rate = if total == 0 { 1.0 } else { uv_cache_hits as f64 / total as f64 }; + eprintln!( + "[ldraw-metrics] phase=uv-cache hits={} misses={} hit_rate={hit_rate:.6} entries={}", + uv_cache_hits, + uv_cache_misses, + uv_cache.len(), + ); + } let root = models .first() .ok_or_else(|| RendererError::parse("parse emitted no root model"))?; @@ -1509,6 +1539,12 @@ pub fn named_camera_case(case_id: &str) -> Result { match case_id { "identity_front" | "reflected_front" => {} "identity_back" | "reflected_back" => case.yaw_degrees = 180.0, + "orbit_left" => case.yaw_degrees = -35.0, + "close_range" => { + case.distance = Some(0.35); + case.fit_bounds = false; + } + "top_down" => case.pitch_degrees = -90.0, "top" => case.pitch_degrees = -90.0, "bottom" => case.pitch_degrees = 90.0, other => return Err(CameraCaseError::UnknownSchema(other.to_owned())), diff --git a/crates/polymodel-renderer-worker/tests/ldraw_preview.rs b/crates/polymodel-renderer-worker/tests/ldraw_preview.rs index 8f3022f..d60fb34 100644 --- a/crates/polymodel-renderer-worker/tests/ldraw_preview.rs +++ b/crates/polymodel-renderer-worker/tests/ldraw_preview.rs @@ -270,7 +270,7 @@ fn with_candidate_library( root_bytes, ); fs::remove_dir_all(root).expect("remove temporary candidate library"); - result + result.map_err(|error| error.to_string()) } #[test] @@ -632,6 +632,25 @@ fn pinned_scene() -> polymodel_renderer_protocol::scene::Scene { .all(|(_, code)| !matches!(code, 16 | 24)), "flattened face batches must resolve CURRENT/EDGE pseudo-colours" ); + if std::env::var_os("POLYMODEL_LDRAW_TIMING").is_some() { + let source_bytes = materializations.iter().map(|item| item.bytes.len()).sum::(); + let upload_bytes = renderable.iter().map(|(_, topology)| { + topology.positions.len() * (3 * 4 + 3 * 4 + 4) + topology.indices.len() * 4 + }).sum::(); + eprintln!( + "[ldraw-metrics] phase=scene-stats scene_bytes={} materializations={} model_definitions={} geometries={} occurrences={} points={} triangles={} lines={} conditional_lines={} upload_bytes={} lookup=hashmap complexity=O(1)", + source_bytes, + materializations.len(), + result.models.len(), + scene.geometries.len(), + scene.stats.occurrences, + scene.stats.points, + scene.stats.triangles, + scene.stats.lines, + scene.stats.conditional_lines, + upload_bytes, + ); + } eprintln!( "[ldraw-preview-test] geometry_defs={} geometry_hashes={} native_batches={} batch_nodes={} batch_materials={} material_codes={material_codes:?}", renderable.len(), diff --git a/justfile b/justfile index ef9459c..ff21c65 100644 --- a/justfile +++ b/justfile @@ -261,27 +261,12 @@ _preview-ldraw *ARGS: set -eu; test -n "${LDRAWDIR:-}" || { echo "[preview] missing LDRAWDIR" >&2; exit 2; }; test -d "$LDRAWDIR" || { echo "[preview] invalid LDRAWDIR=$LDRAWDIR" >&2; exit 2; }; echo "[preview] inherited official library: $LDRAWDIR"; echo "[preview] launching native renderer; child output follows"; cargo run -p polymodel-renderer-worker --bin ldraw_preview -- {{ ARGS }} _ldraw-bench: - # Thresholds are written before measurement so regressions cannot be hidden - # by a benchmark run that changes its own acceptance criteria. - @set -eu -o pipefail; \ - output="${LDRAW_BENCH_OUTPUT:-$PWD/target/ldraw-evidence/bench.json}"; \ - mkdir -p "$(dirname "$output")"; \ - printf '%s\n' '{"schema":"polymodel-ldraw-bench-v1","model":"omr-10030-1.mpd","thresholds":{"parse_ms":5000},"measurement":"cargo test -p polymodel-renderer-worker --test ldraw_preview ldraw_preview_uses_pinned_corpus_and_scene_stats -- --nocapture"}' >"$output"; \ - POLYMODEL_LDRAW_TIMING=1 cargo test -p polymodel-renderer-worker --test ldraw_preview ldraw_preview_uses_pinned_corpus_and_scene_stats -- --nocapture 2>&1 | tee "${output%.json}.log"; \ - python3 -c 'import json,pathlib,re,sys; p=pathlib.Path(sys.argv[1]); report=json.loads(p.read_text()); log=pathlib.Path(sys.argv[2]); lines=[line for line in log.read_text().splitlines() if "[ldraw-metrics]" in line]; threshold=report["thresholds"]["parse_ms"]; expected={"scanned-root","split-root","parsed-root-models","parsed-materializations","traversed"}; observed=[]; failures=[]; pattern=re.compile(r"phase=(?P\S+).*?elapsed_ms=(?P\d+)"); [ (observed.append({"phase":m.group("phase"),"elapsed_ms":int(m.group("elapsed"))}), failures.append(line) if m.group("phase") in expected and int(m.group("elapsed")) > threshold else None ) for line in lines for m in [pattern.search(line)] if m ]; missing=expected-{metric["phase"] for metric in observed}; report["observed"]={"timing_lines":lines,"metrics":observed,"log":sys.argv[2]}; p.write_text(json.dumps(report,indent=2)+chr(10)); [print(f"benchmark threshold exceeded: {line}",file=sys.stderr) for line in failures]; print(f"benchmark missing expected phases: {sorted(missing)}",file=sys.stderr) if missing else None; sys.exit(bool(failures or missing))' "$output" "${output%.json}.log" + # Keep acceptance criteria immutable and require every declared metric. + @set -eu -o pipefail; output="${LDRAW_BENCH_OUTPUT:-$PWD/target/ldraw-evidence/bench.json}"; mkdir -p "$(dirname "$output")"; printf '%s\n' '{"schema":"polymodel-ldraw-bench-v2","model":"omr-10030-1.mpd","thresholds":{"parse_ms":5000,"adapt_ms":2000,"frame_allocations":0,"uv_cache_hit_rate":0.5},"required_metrics":["adapt-ldraw","scene-stats","uv-cache","frame"]}' >"$output"; POLYMODEL_LDRAW_TIMING=1 cargo test -p polymodel-renderer-worker --test ldraw_preview ldraw_preview_uses_pinned_corpus_and_scene_stats -- --nocapture 2>&1 | tee "${output%.json}.log"; log="${output%.json}.preview.log"; prefix="${output%.json}.preview"; Xvfb :99 -screen 0 2560x1440x24 -ac +extension GLX +render -noreset >/tmp/polymodel-xvfb-bench.log 2>&1 & xvfb_pid=$!; trap 'kill "$xvfb_pid" 2>/dev/null || true' EXIT; attempt=0; while test "$attempt" -lt 100; do xset -display :99 q >/dev/null 2>&1 && break; sleep 0.1; attempt=$((attempt + 1)); done; DISPLAY=:99 WINIT_UNIX_BACKEND=x11 WAYLAND_DISPLAY= XDG_RUNTIME_DIR= LIBGL_ALWAYS_SOFTWARE=1 LDRAWDIR="$LDRAWDIR" POLYMODEL_LDRAW_TIMING=1 cargo run -p polymodel-renderer-worker --bin ldraw_preview -- --width 2560 --height 1440 --camera-case identity_front --scene-variant identity --proof-ms 500 --readback-prefix "$prefix" 2>&1 | tee "$log"; cat "$log" >> "${output%.json}.log"; python3 -c 'import json,pathlib,re,sys; p=pathlib.Path(sys.argv[1]); report=json.loads(p.read_text()); log=pathlib.Path(sys.argv[2]); lines=[x for x in log.read_text().splitlines() if "[ldraw-metrics]" in x]; t=report["thresholds"]; fail=[]; parse={"scanned-root","split-root","parsed-root-models","parsed-materializations","traversed"}; metrics=[]; [metrics.append((m.group(1),int(m.group(2)) if m.group(2) else None,x)) for x in lines for m in [re.search(r"phase=(\S+).*?(?:elapsed_ms=(\d+))?",x)] if m]; [fail.append("parse threshold exceeded: "+x) for phase,elapsed,x in metrics if phase in parse and elapsed is not None and elapsed>t["parse_ms"]]; [fail.append("adapt threshold exceeded: "+x) for phase,elapsed,x in metrics if phase=="adapt-ldraw" and elapsed is not None and elapsed>t["adapt_ms"]]; adapt=[x for phase,elapsed,x in metrics if phase=="adapt-ldraw"]; scene=[x for phase,elapsed,x in metrics if phase=="scene-stats"]; uv=[x for x in lines if "phase=uv-cache" in x]; frames=[x for x in lines if "phase=frame" in x]; [fail.append("missing "+name+" metric") for name,items in (("adapt-ldraw",adapt),("scene-stats",scene),("uv-cache",uv),("frame",frames)) if not items]; rate=float(re.search(r"hit_rate=([0-9.]+)",uv[-1]).group(1)) if uv and re.search(r"hit_rate=([0-9.]+)",uv[-1]) else None; fail.append("UV cache hit rate below threshold") if rate is None or ratet["frame_allocations"] else None; pct=lambda a,q: sorted(a)[min(len(a)-1,int((len(a)-1)*q+0.999999))]; report["observed"]={"timing_lines":lines,"metrics":[{"phase":a,"elapsed_ms":b,"line":c} for a,b,c in metrics],"log":str(log),"frame_latency_us":{"count":len(lat),"p95":pct(lat,.95) if lat else None,"p99":pct(lat,.99) if lat else None},"frame_allocations":{"max":max(alloc) if alloc else None,"samples":alloc},"uv_cache_hit_rate":rate,"scene_bytes":int(re.search(r"scene_bytes=(\d+)",scene[-1]).group(1)) if scene and re.search(r"scene_bytes=(\d+)",scene[-1]) else None,"upload_bytes":int(re.search(r"upload_bytes=(\d+)",scene[-1]).group(1)) if scene and re.search(r"upload_bytes=(\d+)",scene[-1]) else None}; p.write_text(json.dumps(report,indent=2)+"\n"); [print("benchmark gate: "+x,file=sys.stderr) for x in fail]; sys.exit(bool(fail))' "$output" "${output%.json}.log" _ldraw-visual: - @set -eu; \ - test -n "${LDRAWDIR:-}" || { echo "[visual] unavailable: LDRAWDIR is not configured" >&2; exit 2; }; \ - test -d "$LDRAWDIR" || { echo "[visual] unavailable: invalid LDRAWDIR=$LDRAWDIR" >&2; exit 2; }; \ - output_dir="${LDRAW_VISUAL_OUTPUT:-$PWD/target/ldraw-evidence/visual}"; mkdir -p "$output_dir"; \ - log="$output_dir/native-preview.log"; prefix="$output_dir/readback"; \ - printf '%s\n' '{"schema":"polymodel-ldraw-visual-v1","model":"omr-10030-1.mpd","proof_ms":1500,"readback_prefix":"readback","thresholds":{"stage_markers":["preparation-stage=closure","native-build-stage=plan","native-build-stage=static","native-build-stage=picker","readback label=frame-1"]}}' >"$output_dir/baseline.json"; \ - Xvfb :99 -screen 0 2560x1440x24 -ac +extension GLX +render -noreset >/tmp/polymodel-xvfb-visual.log 2>&1 & xvfb_pid=$!; \ - trap 'kill "$xvfb_pid" 2>/dev/null || true' EXIT; \ - attempt=0; while test "$attempt" -lt 100; do xset -display :99 q >/dev/null 2>&1 && break; sleep 0.1; attempt=$((attempt + 1)); done; \ - DISPLAY=:99 WINIT_UNIX_BACKEND=x11 WAYLAND_DISPLAY= XDG_RUNTIME_DIR= LIBGL_ALWAYS_SOFTWARE=1 LDRAWDIR="$LDRAWDIR" cargo run -p polymodel-renderer-worker --bin ldraw_preview -- --width 2560 --height 1440 --camera-case identity_front --scene-variant identity --proof-ms 1500 --readback-prefix "$prefix" >"$log" 2>&1; \ - python3 -c 'import json,pathlib,sys; p=pathlib.Path(sys.argv[1]); report=json.loads(p.read_text()); report["log"]=sys.argv[2]; report["readbacks"]=[str(x) for x in pathlib.Path(sys.argv[2]).parent.glob("readback-*.png")]; p.write_text(json.dumps(report,indent=2)+chr(10))' "$output_dir/baseline.json" "$log" + # Capture independent operator-review evidence for every frozen camera case. + @set -eu; test -n "${LDRAWDIR:-}" || { echo "[visual] unavailable: LDRAWDIR is not configured" >&2; exit 2; }; test -d "$LDRAWDIR" || { echo "[visual] unavailable: invalid LDRAWDIR=$LDRAWDIR" >&2; exit 2; }; output_dir="${LDRAW_VISUAL_OUTPUT:-$PWD/target/ldraw-evidence/visual}"; mkdir -p "$output_dir"; cases="identity_front orbit_left close_range top_down"; printf '%s\n' '{"schema":"polymodel-ldraw-visual-v2","model":"omr-10030-1.mpd","proof_ms":1500,"cases":["identity_front","orbit_left","close_range","top_down"],"frozen_pixel_comparison":false,"thresholds":{"stage_markers":["preparation-stage=closure","native-build-stage=plan","native-build-stage=static","native-build-stage=picker","readback label=frame-1","stage=Rendered"]}}' >"$output_dir/baseline.json"; Xvfb :99 -screen 0 2560x1440x24 -ac +extension GLX +render -noreset >/tmp/polymodel-xvfb-visual.log 2>&1 & xvfb_pid=$!; trap 'kill "$xvfb_pid" 2>/dev/null || true' EXIT; attempt=0; while test "$attempt" -lt 100; do xset -display :99 q >/dev/null 2>&1 && break; sleep 0.1; attempt=$((attempt + 1)); done; for case in $cases; do case_dir="$output_dir/$case"; mkdir -p "$case_dir"; log="$case_dir/native-preview.log"; prefix="$case_dir/readback"; DISPLAY=:99 WINIT_UNIX_BACKEND=x11 WAYLAND_DISPLAY= XDG_RUNTIME_DIR= LIBGL_ALWAYS_SOFTWARE=1 LDRAWDIR="$LDRAWDIR" cargo run -p polymodel-renderer-worker --bin ldraw_preview -- --width 2560 --height 1440 --camera-case "$case" --scene-variant identity --proof-ms 1500 --readback-prefix "$prefix" >"$log" 2>&1; done; python3 -c 'import json,pathlib,sys; p=pathlib.Path(sys.argv[1]); r=json.loads(p.read_text()); root=p.parent; marks=r["thresholds"]["stage_markers"]; out=[]; [ (lambda d,t,paths,camera,viewport: (_ for _ in ()).throw(SystemExit(f"visual gate failed for {case}: missing={[m for m in marks if m not in t]} readbacks={paths}")) if any(m not in t for m in marks) or not paths or camera is None or viewport is None else out.append({"case":case,"log":str((d/"native-preview.log").relative_to(root)),"readbacks":paths[:1],"stage_markers":marks,"viewport":viewport,"camera":camera))(root/case,(root/case/"native-preview.log").read_text(),sorted(str(x.relative_to(root)) for x in (root/case).glob("readback-*.png")),next((x for x in (root/case/"native-preview.log").read_text().splitlines() if "camera-fit actual=" in x),None),next((x for x in (root/case/"native-preview.log").read_text().splitlines() if "viewport requested=" in x),None)) for case in r["cases"]]; r["captures"]=out; p.write_text(json.dumps(r,indent=2)+"\n")' "$output_dir/baseline.json" _ldraw-corpus: _ldraw-oracle-integrity _ldraw-oracle-check cargo test -p polymodel-ldraw-testkit