import type { GpuContext } from "./types.js"; export type DebugLogLevel = "resources" | "commands"; export interface DebugOptions { /** * Measure GPU pass times and frame submit-to-completion latency, reported as * {@link DebugStats.timings}. Requires the "timestamp-query" feature on the * device — `optionalFeatures: ["timestamp-query"]` at createContext — and * stays off with a warning without it. */ timing?: boolean; /** * "resources" logs GPU object creation, cache misses and allocator growth — * quiet in steady state, so per-frame messages indicate churn. "commands" * additionally logs every pass, draw and dispatch. */ log?: DebugLogLevel | false; /** * Wrap frames and labeled commands in pushDebugGroup/popDebugGroup so GPU * profilers show the frame structure. */ groups?: boolean; } declare const RESOURCE_COUNTS_KEYS: readonly ["buffers", "textures", "querySets", "samplers", "renderBundles"]; declare const CACHE_COUNTS_KEYS: readonly ["shaderReflections", "shaderModules", "renderPipelines", "computePipelines", "bindGroupLayouts", "pipelineLayouts", "bindGroups"]; /** Creation/disposal counts and estimated GPU memory for one resource type. */ export interface ResourceCounts { alive: number; created: number; disposed: number; /** Estimated GPU memory in bytes (texture sizes are format estimates). */ bytes: number; } /** * Get-or-create cache hit/miss counts for one cache. * * An access is recorded by whichever layer served it: a draw reusing a pipeline * object's reflection or pipeline variant books the hit in the commands layer, * and never reaches the source- and descriptor-keyed caches under it — which * would otherwise only ever see the misses that got past the front line, and * report 0% on a scene where every draw is in fact cached. */ export interface CacheCounts { created: number; hits: number; /** * Wall time spent creating the missed entries, in milliseconds — the cost a * hit avoids. Only the miss path is measured, so a cache at steady state * leaves it untouched. */ ms: number; } export type ResourceCountsKey = (typeof RESOURCE_COUNTS_KEYS)[number]; export type CacheCountsKey = (typeof CACHE_COUNTS_KEYS)[number]; /** * One live GPU resource. Frozen at creation and shared with every snapshot * rather than copied into each: WebGPU has no resize or reformat, so none of * these can go stale, and `bytes` is what untrackResource decrements the * counters by. */ export interface ResourceEntry { /** Same id the resource object carries, or an assigned one for raw handles. */ readonly id: number; readonly label?: string; /** Estimated GPU memory in bytes; 0 for resources owning none. */ readonly bytes: number; } export interface BufferEntry extends ResourceEntry { readonly usage: GPUBufferUsageFlags; } export interface TextureEntry extends ResourceEntry { readonly usage: GPUTextureUsageFlags; readonly format: GPUTextureFormat; readonly width: number; readonly height: number; readonly depthOrArrayLayers: number; readonly mipLevelCount: number; readonly sampleCount: number; readonly dimension: GPUTextureDimension; } export interface QuerySetEntry extends ResourceEntry { readonly type: GPUQueryType; readonly count: number; } /** * Every live resource by type, in creation order. Group it to break usage, * format or size down however the question demands: * * ```js * import { usageNames } from "pex-gpu/internals"; * * Object.groupBy(resources.textures, (texture) => texture.format); * Object.groupBy(resources.buffers, (buffer) => * usageNames(buffer.usage, "buffer").join("|"), * ); * ``` */ export interface DebugResources { buffers: BufferEntry[]; textures: TextureEntry[]; querySets: QuerySetEntry[]; samplers: ResourceEntry[]; renderBundles: ResourceEntry[]; } /** * Split raw usage flags into WebGPU's flag names, in bit order — for grouping * or printing {@link DebugResources} entries. * * ```js * usageNames(positions.usage, "buffer").join("|"); // "copy-dst|vertex" * ``` */ export declare const usageNames: (usage: number, type: "buffer" | "texture") => string[]; /** Work submitted through declarative commands since the frame started. */ export interface FrameCounts extends Record { renderPasses: number; computePasses: number; draws: number; dispatches: number; /** Bundles replayed with executeBundles. */ bundles: number; /** Bytes packed into the uniform ring allocator. */ uniformBytes: number; } /** * Resource/cache pressure counters since context creation — always on, never * reset. */ export interface LifetimeCounts extends Record, Record { } /** The always-on counters, without the resource listing. */ export interface DebugCounters { lifetime: LifetimeCounts; frame: FrameCounts; } /** * Resource/rendering pressure for a context. `lifetime` is always on and starts * at context creation; `frame` resets at the start of each frame() tick; * `resources` lists what is alive right now; `timings` reports the GPU. */ export interface DebugStats extends DebugCounters { resources: DebugResources; /** * GPU timings for the most recently completed frame, or null while timing is * off or no readback has landed yet. A sibling of `frame` rather than a field * in it: those counters reset synchronously at beginFrame, and these describe * a frame two or three back — {@link DebugTimings.frameIndex} says which. */ timings: DebugTimings | null; } /** * Live mutable counters for a device — incremented by the instrumented * internals. */ export declare const debugCounters: (device: GPUDevice) => DebugCounters; /** * List a created resource and bump its type's lifetime counters. Takes * ownership of `entry` and freezes it. Entries hold plain data, never the GPU * handle: that would pin an undisposed resource that is otherwise unreachable, * turning a recoverable leak into a permanent one. */ export declare function trackResource(device: GPUDevice, type: K, entry: DebugResources[K][number]): void; /** Drop a disposed resource. A second call for the same id is a no-op. */ export declare function untrackResource(device: GPUDevice, type: ResourceCountsKey, id: number): void; /** * Like {@link trackResource}, for resources WebGPU reclaims with the JS object * instead of a destroy() — samplers and render bundles, which own no dispose() * to untrack them. Entries drop once `object` is collected. */ export declare function trackCollectable(device: GPUDevice, type: K, object: object, entry: DebugResources[K][number]): void; /** * Record a get-or-create cache lookup against both the lifetime and per-frame * counters. `ms` is how long the miss took to produce its entry; null records a * hit, which by definition produced nothing. */ export declare function recordCacheAccess(device: GPUDevice, key: CacheCountsKey, ms: number | null): void; /** Reset the per-frame counters; called by frame() at the start of each tick. */ export declare function resetFrameCounters(device: GPUDevice): void; /** Whether GPU debug groups should be emitted, per {@link debug}. */ export declare const debugGroupsEnabled: (device: GPUDevice) => boolean; /** Whether pass timing should be collected, per {@link debug}. */ export declare const timingEnabled: (device: GPUDevice) => boolean; /** * Log a debug message when the level is enabled. The message is built lazily so * disabled logging costs a WeakMap lookup. */ export declare function debugLog(device: GPUDevice, level: DebugLogLevel, message: () => string): void; /** * Toggle debug logging and GPU debug groups. Counters ({@link debugStats}) are * always on — this only controls reporting. * * Timing is absent from the boolean shorthand on purpose: it needs a device * feature and changes what the GPU records, so it is only ever switched on by * asking for it. * * ```js * gpu.debug(ctx); // log: "resources", groups: true * gpu.debug(ctx, { log: "commands" }); // full submit stream, no groups * gpu.debug(ctx, { timing: true }); // pass timings, no logging * gpu.debug(ctx, false); // all reporting off * ``` */ export declare function debug(ctx: GpuContext, options?: boolean | DebugOptions): void; /** * Snapshot of the context's resource/rendering pressure counters and of every * live resource. * * ```js * const { lifetime, frame, resources } = gpu.debugStats(ctx); * console.log(`${lifetime.buffers.alive} buffers, ${frame.draws} draws`); * console.table(resources.textures); * ``` */ export declare function debugStats(ctx: GpuContext): DebugStats; /** One timed pass, in the order its encoder was opened. */ export interface PassTiming { index: number; label: string; kind: "render" | "compute"; /** * GPU time in milliseconds, or null when the pass went untimed — it carried * caller-supplied `timestampWrites`, or the query set was full. */ ms: number | null; } /** * Timings for one completed frame, reached through {@link DebugStats.timings}. * They lag the current frame by two or three, which is why they sit beside the * per-frame counters rather than in them. */ export interface DebugTimings { /** The frame these came from, counted from the first timed frame. */ frameIndex: number; /** * Submit to queue completion, in milliseconds. Frame latency, not GPU busy * time: with frames in flight it includes waiting behind earlier ones. */ latency: number | null; /** * Sum of the pass times. An attribution rather than an exclusive cost — * passes can overlap, and a pass's span includes stalls waiting on its own * dependencies, so this can exceed {@link DebugTimings.latency}. */ gpuTime: number | null; passes: PassTiming[]; /** * Every timing is a multiple of 100µs, so the browser is bucketing them. * Chrome does by default: run with * chrome://flags/#enable-webgpu-developer-features (or * --disable-dawn-features=timestamp_quantization) for real numbers. */ quantized: boolean; /** Frames skipped because every readback buffer was still in flight. */ dropped: number; } /** * Start a frame's timings: called by beginFrame. Grows the query set when the * previous frame ran out of it — a set can't be resized, so the passes that * overflowed went untimed and the next frame gets the bigger one. */ export declare function resetFrameTiming(ctx: GpuContext): void; /** * Claim a begin/end query pair for a pass being opened, and return the writes * to put in its descriptor. Undefined when timing is off, when `userWrites` * already claims the descriptor's single `timestampWrites` slot, or when the * query set is full — the pass is still listed, with a null time. * * Render and compute timestamp writes are the same shape, so one return type * serves both descriptors. */ export declare function trackPassTiming(ctx: GpuContext, label: string, kind: "render" | "compute", userWrites?: GPURenderPassTimestampWrites | GPUComputePassTimestampWrites): GPURenderPassTimestampWrites | undefined; /** * Encode this frame's query resolution: called by endFrame, after every pass * has ended and before the encoder is finished. */ export declare function resolveFrameTiming(ctx: GpuContext, encoder: GPUCommandEncoder): void; /** * Close a frame's timings: called by endFrame right after the queue submit, * which is both when the resolved buffer may be mapped and when frame latency * starts counting. */ export declare function completeFrameTiming(ctx: GpuContext): void; /** Release the query set and readback buffers. Called by context dispose. */ export declare function disposeTiming(ctx: GpuContext): void; export {}; //# sourceMappingURL=debug.d.ts.map