import { RegistryData } from "../ecs/batchUtils.js"; import { ProviderIdentity, ProviderKind } from "../debug-protocol.js"; import { DebugRegistry } from "./DebugRegistry.js"; import { SubscriberRegistry } from "./SubscriberRegistry.js"; import { WebSocketLike } from "./bus-websocket.js"; import { WebGPURenderer } from "three/webgpu"; //#region src/debug/DevtoolsProvider.d.ts interface DevtoolsProviderOptions { /** Human-readable name shown in the consumer UI. */ name?: string; /** Explicit provider UUID. Default: auto-generated. */ id?: string; /** * Override the discovery (bonjour) channel name. Rarely needed — the * default `DISCOVERY_CHANNEL` is what every consumer uses. Providing a * custom value only makes sense if you're running multiple isolated * devtools sessions in the same origin (e.g. tests). */ discoveryChannelName?: string; /** Provider kind. Default: `'user'`. */ kind?: ProviderKind; /** * Remote-debugging endpoint (#114): a WebSocket URL (usually a * `flatland-devtools-relay`) or an already-open WebSocket. When set, * `start()` bridges this provider's bus traffic over the socket so a * desktop dashboard can attach from another device. The in-process * BroadcastChannel bus is untouched — the bridge only taps it. */ remote?: string | WebSocketLike; } /** * Devtools producer — owns the BroadcastChannel, subscriber registry, * stats + env collectors, and the per-tick packet-building logic. * * Intended to be used two ways: * * 1. **Composed inside `Flatland`** — Flatland constructs one when the * devtools build gate + `isDevtoolsActive()` runtime-gate * are both true, and calls `beginFrame()` / `endFrame()` around * its `render()` body. Host consumers don't have to know devtools * exist. * * 2. **Standalone** — a bare three.js app (no Flatland, or a different * engine) can instantiate `DevtoolsProvider` and call * `beginFrame(now)` / `endFrame(renderer)` around its rAF tick (or * around each `renderer.render()` call if the app has only one). * Standard bus protocol; any consumer that knows the protocol * works the same. * * ## Timing * * Explicit begin/end boundaries, not scene hooks. Engines that do * multiple internal `renderer.render()` calls per logical frame (SDF * pass, occlusion pass, main render, post-processing) would count * each internal pass as a separate "frame" if we hooked * `scene.onAfterRender` — FPS misreports as a multiple of the real * rate, and per-render stats don't aggregate. The begin/end approach * lets the caller define the true frame boundary. * * - `cpuMs` = `endFrame` - `beginFrame` (full frame CPU cost) * - FPS = interval between consecutive `endFrame` calls * - `drawCalls` / `triangles` = `renderer.info.render` delta between * begin and end, aggregating across all internal render passes * during the frame * * For multi-scene apps, call begin/end around the whole rAF tick and * `renderer.info.render` accumulates across every scene's render. * * ## Hot path * * Zero-allocation past construction — scratch message objects are * mutated in place; `structuredClone` inside `postMessage` gives * consumers their own copy, so the producer can keep reusing its * scratch on the next tick. * * ## Liveness * * Emits a `ping` broadcast if no `data` packet has been sent within * `IDLE_PING_MS`. Pure liveness signal — consumers treat any server * message (`data` / `ping` / `subscribe:ack`) as proof-of-life. */ declare class DevtoolsProvider { readonly identity: ProviderIdentity; /** * Cached options — needed at `start()` time. The constructor only * stashes them; channels / transport / listeners aren't created until * `start()` runs. This is what makes the class safe to construct * speculatively (e.g. inside `Flatland`'s constructor, which R3F may * call for renders that get discarded). */ private readonly _opts; /** Remote WS bridge (null unless `options.remote` was provided). */ private _remoteBridge; /** Bonjour channel — discovery traffic only (query/announce/gone). Lazy. */ private _discoveryBus; /** * Per-provider data channel — named after `identity.id`. Used in * receive-only mode (subscribe/ack/unsubscribe in). Outbound data / * ping / subscribe:ack go through `_dataTransport` so the heavy * `data` packets can be offloaded to a worker. Lazy. */ private _dataBus; /** Producer-side transport for outbound data-channel messages. Lazy. */ private _dataTransport; /** * Listener references kept so we can `removeEventListener` on dispose * — important because `BroadcastChannel.close()` doesn't always * detach pending callbacks in every runtime. */ private _onDiscovery; private _onData; private _subs; private _stats; private _env; private _registry; private _textures; private _batches; /** Scratch `data` message. Reused across flushes; features reassigned each tick. */ private _dataScratch; /** Scratch envelope for idle `ping` broadcasts. */ private _pingScratch; /** Scratch env payload reused across flushes. */ private _envScratch; /** Scratch stats payload reused across flushes; fields reassigned each drain. */ private _statsScratch; /** Scratch registry payload reused across flushes. */ private _registryScratch; /** Scratch buffers payload reused across flushes. */ private _buffersScratch; /** Scratch batches payload reused across flushes. */ private _batchesScratch; /** Wall-clock time of the last outbound broadcast. */ private _lastBroadcastAt; /** Latest renderer seen during `endFrame` — cached so `_sendSubscribeAck` has something to read. */ private _latestRenderer; /** Flush timer handle. Ticks every `STATS_BATCH_MS`. Null when not active. */ private _flushTimer; /** * `true` between `start()` and `dispose()`. Pure-constructor instances * stay `false` until something explicitly activates them; per-frame * methods short-circuit while inactive so they're safe to call * speculatively. */ private _active; private _forceNextKeyFrame; constructor(options?: DevtoolsProviderOptions); /** * Activate the provider on the bus. Opens BroadcastChannels, registers * listeners + module-level debug sinks, announces our existence on * discovery, and starts the batched flush timer. * * Idempotent: a second call while already active is a no-op. Safe to * call after `dispose()` to re-activate (e.g. when a Flatland Object3D * is re-added to the scene graph). */ start(): void; /** * Mark the start of a logical frame. Call before any * `renderer.render()` for this frame. `now` should be * `performance.now()`. */ beginFrame(now: number, renderer: WebGPURenderer): void; /** * Toggle the renderer's GPU timestamp tracking to match demand. Devtools * owns this — hosts no longer pass `trackTimestamp: true` themselves, so * when devtools is off no queries are issued and there's no pool to drain. * * Driven live by the `stats` subscription: on while a consumer wants stats * (panel expanded), off when none do (collapsed). Turning it off stops * three from issuing timestamp queries, so the pool never accumulates * undrained entries; turning it back on resumes sampling. three.js * negotiates every adapter feature at device creation, so `timestamp-query` * is already available and the per-frame flag is the only lever needed. * * Decision logic (incl. the pre-init/Safari edge cases) lives in * `resolveTrackTimestamp`; this just applies the result when it changes. * On the on→off transition we drain three's query pool first (while * tracking is still enabled) so we don't strand unresolved queries. */ private _syncGpuTiming; /** * Mark the end of a logical frame. Records the sample and drains the * GPU-timestamp query pool. Does NOT broadcast — batches are shipped * on the `_flush` interval, not per-frame. */ /** * Snapshot the current `BatchRegistry` into the batches collector. * Called by the host engine (Flatland) once per frame after all * internal passes complete and the batch set is stable. No-op when * no consumer is subscribed to the `'batches'` feature. */ captureBatches(registry: RegistryData): void; endFrame(renderer: WebGPURenderer): void; /** * Assemble and broadcast a batched `data` packet. Invoked by the * `STATS_BATCH_MS` interval. No-op when there are no subscribers or * no samples / env delta accumulated this window; idle pings keep * consumers aware that the server is alive. */ private _flush; /** * `true` when the provider is NOT currently active on the bus — * either because `start()` was never called, or because `dispose()` * has been called since the last `start()`. After `dispose()` the * instance can be re-activated by calling `start()` again. */ get disposed(): boolean; /** * Release bus resources: closes both channels, stops the flush * timer, clears module-level sinks, broadcasts `provider:gone`. * Idempotent. After this returns the instance is dormant but * reusable — call `start()` to bring it back online. */ dispose(): void; /** * The per-provider data channel. Exposed for test harnesses. `null` * before `start()` or after `dispose()`. */ get bus(): BroadcastChannel | null; /** The current subscriber registry. Read-only view. */ get subscribers(): SubscriberRegistry; /** Current engine frame counter. */ get frame(): number; /** * Debug registry — register CPU arrays here to expose them to the * pane. Entries only cost wire bytes when subscribers include * `registry` in their feature set, so it's safe to leave permanently * registered from engine code. */ get registry(): DebugRegistry; /** * Handler for the per-provider data channel — all traffic here is * already implicitly addressed to us, so no per-message `providerId` * filtering is needed. */ private _handleDataMessage; private _announce; private _sendSubscribeAck; /** * If no `data` packet has been sent in `IDLE_PING_MS`, broadcast a * `ping` on our data channel so consumers know we're alive during * quiet periods. */ private _maybeIdlePing; } //#endregion export { DevtoolsProvider, DevtoolsProviderOptions }; //# sourceMappingURL=DevtoolsProvider.d.ts.map