"use strict"; (function (global) { const nowMs = () => (global.performance?.now ? global.performance.now() : Date.now()); const debugEnabled = (() => { try { const params = new URLSearchParams(global.location?.search || ""); return params.get("debug") === "1" || params.get("debug") === "true" || params.get("perf") === "1"; } catch (_) { return false; } })(); const buckets = new Map(); const stack = []; const metrics = new Map(); let frameOpen = null; let frameCount = 0; let lastSmoothAt = 0; const storageKey = "tarinai_visual_settings_v1"; const allowed = { effects: new Set(["off", "high"]), shadows: new Set(["off", "high"]), details: new Set(["low", "high"]), }; const defaults = Object.freeze({ effects: "high", shadows: "high", details: "high" }); let visualSettings = loadVisualSettings(); let resizeRequested = false; function bucketFor(label) { let b = buckets.get(label); if (!b) { b = { label, ms: 0, selfMs: 0, avg: 0, selfAvg: 0, max: 0, calls: 0, last: 0, lastSelf: 0 }; buckets.set(label, b); } return b; } function begin(label) { if (!debugEnabled) return null; const frame = { label: String(label || "unknown"), start: nowMs(), childMs: 0 }; stack.push(frame); return () => { const top = stack.pop(); const active = top === frame ? frame : top || frame; // If calls ended out of order, keep the profiler alive rather than throwing. if (top !== frame) { const idx = stack.lastIndexOf(frame); if (idx >= 0) stack.splice(idx, 1); } const elapsed = Math.max(0, nowMs() - active.start); const selfElapsed = Math.max(0, elapsed - (active.childMs || 0)); if (stack.length) stack[stack.length - 1].childMs += elapsed; const b = bucketFor(active.label); b.ms += elapsed; b.selfMs += selfElapsed; b.calls += 1; b.last = elapsed; b.lastSelf = selfElapsed; if (elapsed > b.max) b.max = elapsed; return elapsed; }; } function beginFrame() { frameOpen = nowMs(); if (debugEnabled) stack.length = 0; return frameOpen; } function smoothBuckets() { if (!debugEnabled) return; for (const b of buckets.values()) { b.avg = b.avg ? b.avg * 0.82 + b.ms * 0.18 : b.ms; b.selfAvg = b.selfAvg ? b.selfAvg * 0.82 + b.selfMs * 0.18 : b.selfMs; b.ms = 0; b.selfMs = 0; b.calls = 0; } } function normalizeVisualSetting(key, value) { const raw = String(value || ""); if (key === "effects" || key === "shadows") { if (raw === "off") return "off"; if (raw === "on" || raw === "low" || raw === "mid" || raw === "high") return "high"; } if (key === "details") { if (raw === "low") return "low"; if (raw === "mid" || raw === "high") return "high"; } return defaults[key]; } function sanitizeVisualSettings(value) { const src = value && typeof value === "object" ? value : {}; const next = { ...defaults }; for (const key of Object.keys(defaults)) { const normalized = normalizeVisualSetting(key, src[key]); if (allowed[key]?.has(normalized)) next[key] = normalized; } return next; } function loadVisualSettings() { try { const raw = global.localStorage?.getItem(storageKey); return sanitizeVisualSettings(raw ? JSON.parse(raw) : null); } catch (_) { return { ...defaults }; } } function saveVisualSettings() { try { global.localStorage?.setItem(storageKey, JSON.stringify(visualSettings)); } catch (_) {} } function dprScaleForDetails(details) { if (details === "low") return 0.72; return 1; } function endFrame() { frameCount += 1; if (frameOpen != null && debugEnabled) { const elapsed = Math.max(0, nowMs() - frameOpen); const b = bucketFor("frame.total"); b.ms += elapsed; b.selfMs += Math.max(0, elapsed - stack.reduce((sum, f) => sum + (f.childMs || 0), 0)); b.calls += 1; b.last = elapsed; b.lastSelf = b.last; if (elapsed > b.max) b.max = elapsed; } frameOpen = null; if (debugEnabled) stack.length = 0; const t = nowMs(); if (t - lastSmoothAt >= 750) { lastSmoothAt = t; smoothBuckets(); } } function visualLevel(key, fallback = "high") { return visualSettings[key] || fallback; } function visualSettingsValue() { return { ...visualSettings }; } function setVisualSetting(key, value) { if (!Object.prototype.hasOwnProperty.call(defaults, key)) return false; const nextValue = normalizeVisualSetting(key, value); if (!allowed[key]?.has(nextValue)) return false; if (visualSettings[key] === nextValue) return true; visualSettings = { ...visualSettings, [key]: nextValue }; saveVisualSettings(); if (key === "details") resizeRequested = true; return true; } function renderQualityTier() { return visualSettings.details; } function shadowQualityTier() { return visualSettings.shadows; } function dprScaleValue() { return dprScaleForDetails(visualSettings.details); } function consumeResizeRequest() { const v = resizeRequested; resizeRequested = false; return v; } function setMetric(name, value) { if (!debugEnabled) return; metrics.set(String(name || "metric"), value); } function snapshot() { const frameAvg = buckets.get("frame.total")?.avg || 0; const entries = [...buckets.values()].map(b => ({ label: b.label, avg: Number((b.avg || 0).toFixed(2)), selfAvg: Number((b.selfAvg || 0).toFixed(2)), max: Number((b.max || 0).toFixed(2)), last: Number((b.last || 0).toFixed(2)), lastSelf: Number((b.lastSelf || 0).toFixed(2)), calls: b.calls || 0, pct: frameAvg > 0 ? Number((((b.avg || 0) / frameAvg) * 100).toFixed(0)) : 0, selfPct: frameAvg > 0 ? Number((((b.selfAvg || 0) / frameAvg) * 100).toFixed(0)) : 0, })); const topInclusive = entries.slice().sort((a, b) => b.avg - a.avg).slice(0, 12); const topSelf = entries.slice().sort((a, b) => b.selfAvg - a.selfAvg).slice(0, 12); return { enabled: debugEnabled, tier: visualSettings.details, visual: visualSettingsValue(), dprScale: dprScaleValue(), frameCount, frameAvg: Number(frameAvg.toFixed(2)), entries: topInclusive, topInclusive, topSelf, metrics: Object.fromEntries(metrics.entries()), }; } global.TarinaiPerf = Object.freeze({ begin, beginFrame, endFrame, renderQualityTier, shadowQualityTier, visualLevel, visualSettings: visualSettingsValue, setVisualSetting, dprScale: dprScaleValue, consumeResizeRequest, setMetric, snapshot, }); })(typeof window !== "undefined" ? window : globalThis);