Debug Interface Guide

Overview

Luxar exposes a window.__luxarDebug object that provides programmatic access to the viewer’s internal state. This is intended for:

  • Diagnosing rendering or data-loading issues during development

  • Writing and debugging Playwright E2E tests

  • AI-assisted debugging via the agent driver (pnpm agent:debug)

  • Generating state dumps for bug reports

The interface is defined in two stages: src/core/bootstrap.ts creates the base object with the app reference and version string, then installDebugInterface() (in src/core/app/debug/debug-interface.ts, invoked from app.ts via setupDebugInterface()) extends it with runtime components (scene, camera, cache helpers, etc.) after initialization completes.

Enabling the Debug Interface

URL parameter (recommended):

http://localhost:5173/?src=http://127.0.0.1:8000&debug

localStorage (persists across page loads):

localStorage.setItem('luxar.debug', 'true');

Note: either flag — the ?debug URL parameter or the persisted luxar.debug localStorage entry — enables full debug mode: bootstrap.ts seeds the base debug object, and setupDebugInterface() in app.ts (which delegates to installDebugInterface() in src/core/app/debug/debug-interface.ts) adds the runtime components (scene, camera, controls, etc.) once initialization completes.

When enabled, the viewer logs Debug interface available at window.__luxarDebug to the console and, after initialization, prints a summary of available commands.

When the ?debug URL parameter is absent and localStorage is not set, the object is never created, so there is zero overhead in production.

Available Properties and Methods

Base properties (from bootstrap.ts, available immediately)

Property

Type

Description

window.__luxarBuild

BuildInfo

Unconditional pre-initialization build identity: version, commit, build time, and whether the bundle was stamped.

app

LuxarApp

The application instance.

consoleInterceptor

ConsoleInterceptor

Captures all console output for replay.

version

string

VIEWER_VERSION (src/version.ts): the package.json version injected at build time (a semver-normalized CalVer such as 2026.6.5), or 0.0.0-dev in a consumer that bundles src/ without the define. Commit and build time live on window.__luxarBuild.

Runtime components (from installDebugInterface(), available once runtimeReady is true)

Property

Type

Description

scene

THREE.Scene

The Three.js scene graph root.

camera

LuxarCamera

Active camera (perspective or orthographic).

renderer

THREE.WebGLRenderer

The WebGL renderer instance.

controls

ControlsManager

Orbit/fly controls manager.

postProcessing

PostProcessingManager

Bloom, AO, AA pipeline.

animationController

AnimationController

Manages the render loop.

inputHandler

InputHandler

Keyboard and mouse input system.

renderingControls

RenderingControls

UI panel for rendering settings.

recordingPanel

RecordingPanel

Screenshot and video capture panel.

sceneDimsManager

SceneDimsManager

Dimension navigation state.

runtimeReady

boolean

true once all components are initialized.

Helper functions

Method

Return type

Description

getState()

object

JSON-serializable snapshot of current state (point counts per cloud, camera position/FOV, dimension info, animation status, initialization status, the two memory-ceiling fields tabled below, and isLoading — true while a load pass, i.e. an updateView fetch/decode/upload sweep up to its geometry commit, is in flight on any registered scene loader, or a view-state is queued behind one; see the scope notes below).

renderOnce()

void

Kicks the animation loop to force a single render frame. Useful for stable screenshots.

getSceneLoader()

SceneLoaderManager

Returns the singleton scene loader manager for inspecting loaded data.

Memory-ceiling fields on getState()

Two fields answer “were these element counts decided by the store, or by this machine?”. core/app/debug/capture-readiness.ts refuses a capture on either (#2508). refinementResidency and gpuPool.byteBudgetEvictions are cumulative for the life of the scene loader and never reset within it: they report that something happened at some point while this scene was loaded, not that it is true right now. The scope is the LOADER, not the page — an in-page dataset switch builds a fresh SceneLoader (fresh reporter, fresh GPU pool), so the next scene starts clean with no reload. The rest of gpuPool is not cumulative: activeBytes, pooledBytes, totalBytes, largestPooledBytes, activeBuffers and pooledBuffers are instantaneous readings, and evictions is a lifetime counter dominated by routine recycling.

Field

Type

Description

gpuPool

GPUPoolDebugStats (absent when unavailable)

GPU buffer-pool usage — the instantaneous activeBuffers / pooledBuffers / activeBytes / pooledBytes / totalBytes / largestPooledBytes, plus the cumulative evictions and byteBudgetEvictions. The last is the subset charged by the VRAM byte-budget pass: evictions alone is dominated by routine LRU recycling and means nothing on its own. Not new to the snapshot TYPE, but only populated in production as of #2508 (a gpuPoolStats provider had been declared and never passed), so an older build reports undefined here. Absent when no loader is registered or pooling is disabled.

refinementResidency

RefinementResidencyStop (absent when nothing was declined)

Present once progressive refinement declined at least one rung at the residency byte ceiling; carries the first refusal’s reason / residentBytes / budgetBytes / firstPath, the distinct declinedPathCount, and a bounded declinedPaths sample. Presence is the signal — absence means “never stopped” (or an older build), never “in trouble”.

Cache helpers (__luxarDebug.cache)

Method

Return type

Description

getStats()

{ l0, l1, l2 }

Returns hit/miss/size statistics for all three cache tiers.

listDatasets()

object

Lists datasets stored in the caching store.

clearL0()

void

Clears the L0 decompressed chunk cache (in-memory).

clearL1()

void

Clears the L1 memory cache.

clearL2()

Promise<void>

Clears the L2 OPFS (Origin Private File System) persistent cache.

clearAll()

Promise<void>

Clears all three cache tiers.

Common Debug Workflows

Inspecting scene state

Open the browser console and run:

const state = __luxarDebug.getState();
console.table(state.pointClouds);  // Per-cloud point counts, visibility, attributes
console.log('Total points:', state.totalPoints);
console.log('Camera:', state.camera);
console.log('Dimensions:', state.dimensions);

To walk the Three.js scene graph directly:

__luxarDebug.scene.traverse(obj => {
  if (obj.type === 'Points') console.log(obj.name, obj.geometry.attributes);
});

Checking cache performance

const stats = __luxarDebug.cache.getStats();
console.log('L0 (decompressed):', stats.l0);
console.log('L1 (memory):', stats.l1);
console.log('L2 (OPFS):', stats.l2);

To isolate a cache tier during testing:

__luxarDebug.cache.clearL0();  // Force re-decompression on next access
await __luxarDebug.cache.clearL2();  // Force re-fetch from network

Forcing re-renders

__luxarDebug.renderOnce();

This restarts the animation loop briefly, producing at least one fresh frame. Useful after programmatically changing material uniforms or camera position.

Dumping state for bug reports

Copy-paste the following into the console to produce a JSON blob suitable for attaching to an issue:

JSON.stringify(__luxarDebug.getState(), null, 2);

For cache state:

JSON.stringify(__luxarDebug.cache.getStats(), null, 2);

Using with Playwright (E2E Tests)

E2E tests load the viewer with ?debug in the URL and wait for the debug interface before making assertions. The helpers in src/tests/e2e/helpers.ts encapsulate the common patterns.

Eight helpers poll getState().isLoading: waitForDataLoaded, waitForDimensionNavigation, waitForSpatialQuery, waitForSpatialQueryOrThrow, waitForNavigationComplete, waitForNavigationCompleteOrThrow, and the state-based fallbacks inside waitForRenderStable and waitForNextRender. (waitForPointsLoaded does not — it gates on state.totalPoints >= minPoints.) So that flag has to be a real boolean in every snapshot, not merely absent when nothing is loading: !undefined is true, which gates on nothing.

isLoading is scoped to a load pass — an updateView sweep (fetch / decode / upload) up to its geometry commit, a failed-loader retry (which takes the same lock), or a view-state that is queued behind either and has not begun loading yet. That last clause is what keeps the refinement exclusion below from opening a hole: a nav arriving during a refinement hold parks in the queue without touching the lock, so an isLoading: true with no fetch in flight is the expected reading on any laddered dataset. It deliberately does not cover:

  • the initial loadScene (that path only touches the loader’s lock at its very end, to hand it to the post-load refinement kick). Wait on initialized for the first load. An in-page dataset switch is covered by neither flag: initialized stays true and the replacement loader is registered before its loadScene runs, so isLoading reads false throughout the switch’s load.

  • lazy substitutive-LOD / deferred-partition ensureLoaded promotions, which run outside any updateView cycle and surface as content-change notifications instead.

  • the progressive-LOD refinement drain, which inherits the same lock after the current view has already committed. Excluding it keeps the flag reporting first-commit latency rather than full-ladder latency, so a wait doesn’t sit through every additive ladder. SceneLoader.isUpdateInProgress() retains the broader “the lock is held at all” meaning for the adaptive-DPR manager and the init pipeline.

Waiting for initialization

import { waitForLuxarReady } from './helpers';

await waitForLuxarReady(page);  // Waits for getState().initialized === true

Under the hood this polls window.__luxarDebug.getState().initialized:

await page.waitForFunction(() => {
  const debug = (window as any).__luxarDebug;
  return debug && debug.getState && debug.getState().initialized;
}, null, { timeout: 45000 });

Reading state from a test

import { getLuxarState } from './helpers';

const state = await getLuxarState(page);
expect(state.totalPoints).toBeGreaterThan(0);

Forcing a render for screenshots

import { renderOnce } from './helpers';

await renderOnce(page);  // Triggers render + 100ms settle
await page.screenshot({ path: 'screenshot.png' });

Waiting for data to load

import { waitForPointsLoaded } from './helpers';

await waitForPointsLoaded(page, 100);  // Wait until >= 100 points are loaded

Direct page.evaluate access

For one-off checks not covered by helpers:

const cacheStats = await page.evaluate(() => {
  return (window as any).__luxarDebug.cache.getStats();
});

Using with the Agent Debugger

The agent debugger (pnpm agent:debug) launches a headless Playwright browser with ?debug in the URL and dumps the debug interface state automatically.

cd packages/luxar-viewer
pnpm agent:debug           # Headless, captures state + screenshot
pnpm agent:debug:visible   # Headed browser for visual inspection

The output includes:

  • [BROWSER-CONSOLE-*] – all console messages captured by consoleInterceptor

  • A JSON state dump from getState() (point counts, camera, dimensions)

  • A screenshot saved to test-results/debug/debug-view.png

Typical workflow:

  1. Run pnpm agent:debug to capture current viewer state.

  2. Inspect the JSON dump and screenshot for anomalies.

  3. If needed, add console.log() calls in source code and re-run.

  4. Fix the issue and verify with another pnpm agent:debug run.

  5. Remove any temporary logging before committing.

Source Files

File

Role

packages/luxar-viewer/src/core/bootstrap.ts

Creates the base __luxarDebug object and checks activation flags (?debug URL param / luxar.debug localStorage).

packages/luxar-viewer/src/core/app.ts

Calls setupDebugInterface() after init, delegating to installDebugInterface().

packages/luxar-viewer/src/core/app/debug/debug-interface.ts

installDebugInterface() — assigns the runtime components (scene, camera, cache helpers, getState, etc.) onto __luxarDebug.

packages/luxar-viewer/src/tests/e2e/helpers.ts

Playwright helper functions that consume the debug interface.