diff --git a/apps/typegpu-docs/src/content/docs/apis/utils.mdx b/apps/typegpu-docs/src/content/docs/apis/utils.mdx index 819069d345..8b5a73930e 100644 --- a/apps/typegpu-docs/src/content/docs/apis/utils.mdx +++ b/apps/typegpu-docs/src/content/docs/apis/utils.mdx @@ -548,7 +548,7 @@ If what the expression is a direct reference to an existing value (e.g. a unifor storage binding, ...), then choose from `'uniform'`, `'mutable'`, `'readonly'`, `'workgroup'`, `'private'` or `'handle'` depending on the address space of the referred value. -### `possibleSideEffects` +### *possibleSideEffects* The fourth optional parameter `possibleSideEffects` indicates, whether generating this snippet may produce a WGSL expression with observable side-effects (e.g. calling a barrier, discarding a fragment, or writing to memory). @@ -557,3 +557,21 @@ Snippets with `possibleSideEffects: true` cannot appear in ternary branches that get compiled to `select()`, because `select()` evaluates both branches unconditionally - a side-effect meant to be conditional would execute regardless of the condition. + +## *warn* + +TypeGPU warns you about a lot of potential problems or API misuses. +Although it is almost always better addressed properly, the `warn` object lets you silence specific warning types. + +```ts twoslash +import { tgpu, d, warn } from 'typegpu'; + +const fn = tgpu.fn([], d.u32)(() => { + 'use gpu'; + return 1.5; // <- AbstractFloat needs an implicit cast to u32 +}); + +warn.disable('implicit-conversion'); +console.log(tgpu.resolve([fn])); // resolves without warning about the cast +warn.reset(); +``` diff --git a/apps/typegpu-docs/src/examples/react/confetti/index.tsx b/apps/typegpu-docs/src/examples/react/confetti/index.tsx index 2a7fb460bf..476bfdab19 100644 --- a/apps/typegpu-docs/src/examples/react/confetti/index.tsx +++ b/apps/typegpu-docs/src/examples/react/confetti/index.tsx @@ -127,7 +127,7 @@ function App() { const particleDataBuffer = useBuffer(d.arrayOf(ParticleData, PARTICLE_AMOUNT), { initial: writeRandomPositions, - }).$usage('storage', 'uniform', 'vertex'); + }).$usage('storage', 'vertex'); const aspectRatio = useUniform(d.f32, { initial: 1 }); const deltaTime = useUniform(d.f32); diff --git a/apps/typegpu-docs/src/examples/simulation/confetti/index.ts b/apps/typegpu-docs/src/examples/simulation/confetti/index.ts index 97a4d007ea..6655c8690e 100644 --- a/apps/typegpu-docs/src/examples/simulation/confetti/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/confetti/index.ts @@ -48,7 +48,7 @@ const particleGeometryBuffer = root const particleDataBuffer = root .createBuffer(d.arrayOf(ParticleData, PARTICLE_AMOUNT)) - .$usage('storage', 'uniform', 'vertex'); + .$usage('storage', 'vertex'); let elapsedTime = 0; const aspectRatio = root.createUniform(d.f32, canvas.width / canvas.height); diff --git a/packages/typegpu/src/core/buffer/buffer.ts b/packages/typegpu/src/core/buffer/buffer.ts index cab7ab00af..77ba5f1264 100644 --- a/packages/typegpu/src/core/buffer/buffer.ts +++ b/packages/typegpu/src/core/buffer/buffer.ts @@ -32,6 +32,7 @@ import { type TgpuReadonly, type TgpuUniform, } from './bufferBinding.ts'; +import { warnIfNotUniformAligned } from '../pipeline/webgpuLimitations.ts'; // ---------- // Public API @@ -292,6 +293,10 @@ class TgpuBufferImpl implements TgpuBuffer { throw new Error(`Buffer of type ${this.dataType.type} cannot be used as ${usage}`); } + if (usage === 'uniform') { + warnIfNotUniformAligned(this.dataType); + } + this.flags |= usage === 'uniform' ? GPUBufferUsage.UNIFORM : 0; this.flags |= usage === 'storage' ? GPUBufferUsage.STORAGE : 0; this.flags |= usage === 'vertex' ? GPUBufferUsage.VERTEX : 0; diff --git a/packages/typegpu/src/core/pipeline/applyPipelineState.ts b/packages/typegpu/src/core/pipeline/applyPipelineState.ts index df2e708ddc..7f5fb466ba 100644 --- a/packages/typegpu/src/core/pipeline/applyPipelineState.ts +++ b/packages/typegpu/src/core/pipeline/applyPipelineState.ts @@ -10,7 +10,7 @@ import type { BaseData } from '../../data/wgslTypes.ts'; import type { TgpuVertexLayout } from '../vertexLayout/vertexLayout.ts'; import type { ExperimentalTgpuRoot } from '../root/rootTypes.ts'; -import { warnIfOverflow } from './limitsOverflow.ts'; +import { warnIfOverflow } from './webgpuLimitations.ts'; // ----------------------------------------------- // shared helpers for applying pipeline state to render/compute pass encoders diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index 36e80ddca7..fef3a07b8e 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -42,6 +42,7 @@ import { PerformanceTrackerImpl, type PerformanceTracker, } from './performanceTracker.ts'; +import { logger } from '../../tgpuLogger.ts'; interface ComputePipelineInternals { readonly rawPipeline: GPUComputePipeline; @@ -227,7 +228,8 @@ class TgpuComputePipelineImpl implements TgpuComputePipeline { const querySet = this.#core.performanceCallbackQuerySet; if (!querySet) { - console.warn( + logger.warn( + 'webgpu-feature-missing', 'Performance callback cannot be used because the timestamp-query feature is not enabled on the root.', ); return this; diff --git a/packages/typegpu/src/core/pipeline/limitsOverflow.ts b/packages/typegpu/src/core/pipeline/limitsOverflow.ts deleted file mode 100644 index 81d2883644..0000000000 --- a/packages/typegpu/src/core/pipeline/limitsOverflow.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { TgpuBindGroupLayout } from '../../tgpuBindGroupLayout.ts'; - -export function warnIfOverflow(layouts: TgpuBindGroupLayout[], limits: GPUSupportedLimits) { - const entries = Object.values(layouts) - .flatMap((layout) => Object.values(layout.entries)) - .filter((entry) => entry !== null); - - const uniform = entries.filter((entry) => 'uniform' in entry).length; - const storage = entries.filter((entry) => 'storage' in entry).length; - - if (uniform > limits.maxUniformBuffersPerShaderStage) { - console.warn( - `Total number of uniform buffers (${uniform}) exceeds maxUniformBuffersPerShaderStage (${limits.maxUniformBuffersPerShaderStage}). Consider: -1. Grouping some of the uniforms into one using 'd.struct', -2. Increasing the limit when requesting a device or creating a root.`, - ); - } - - if (storage > limits.maxStorageBuffersPerShaderStage) { - console.warn( - `Total number of storage buffers (${storage}) exceeds maxStorageBuffersPerShaderStage (${limits.maxStorageBuffersPerShaderStage}).`, - ); - } -} diff --git a/packages/typegpu/src/core/pipeline/pipelineUtils.ts b/packages/typegpu/src/core/pipeline/pipelineUtils.ts index 80027b7386..0870f31f46 100644 --- a/packages/typegpu/src/core/pipeline/pipelineUtils.ts +++ b/packages/typegpu/src/core/pipeline/pipelineUtils.ts @@ -3,6 +3,7 @@ import { memoryLayoutOf, type PrimitiveOffsetInfo } from '../../data/offsetUtils import { sizeOf } from '../../data/sizeOf.ts'; import type { BaseData } from '../../data/wgslTypes.ts'; import { isGPUBuffer } from '../../types.ts'; +import { logger } from '../../tgpuLogger.ts'; type IndirectOperation = 'dispatchWorkgroupsIndirect' | 'drawIndirect' | 'drawIndexedIndirect'; const IndirectOperationToRequiredData = { @@ -51,7 +52,8 @@ export function resolveIndirectOffset( validateIndirectBufferSize(sizeOf(indirectBuffer.dataType), offset, requiredSize, operation); if (contiguous < requiredSize) { - console.warn( + logger.warn( + 'suspicious', `${operation}: Starting at offset ${offset}, only ${contiguous} contiguous bytes are available before padding. '${operation}' requires ${requiredSize} bytes (${IndirectOperationToRequiredData[operation]}). Reading across padding may result in undefined behavior.`, ); } diff --git a/packages/typegpu/src/core/pipeline/renderPipeline.ts b/packages/typegpu/src/core/pipeline/renderPipeline.ts index 64d53cdc4a..194ff7b6a9 100644 --- a/packages/typegpu/src/core/pipeline/renderPipeline.ts +++ b/packages/typegpu/src/core/pipeline/renderPipeline.ts @@ -88,6 +88,7 @@ import { PerformanceTrackerImpl, type PerformanceTracker, } from './performanceTracker.ts'; +import { logger } from '../../tgpuLogger.ts'; const DRAW_INDIRECT_SIZE = 16; // 4 x 4 const DRAW_INDEXED_INDIRECT_SIZE = 20; // 5 x 4 @@ -601,7 +602,8 @@ class TgpuRenderPipelineImpl implements TgpuRenderPipeline { const querySet = internals.core.performanceCallbackQuerySet; if (!querySet) { - console.warn( + logger.warn( + 'webgpu-feature-missing', 'Performance callback cannot be used because the timestamp-query feature is not enabled on the root.', ); return this; @@ -1288,7 +1290,8 @@ export function matchUpVaryingLocations( if (locations[key] === undefined) { saveLocation(key, customLocation); } else if (locations[key] !== customLocation) { - console.warn( + logger.warn( + 'locations-mismatched', `Mismatched location between vertexFn (${vertexFnName}) output (${ locations[key] }) and fragmentFn (${fragmentFnName}) input (${customLocation}) for the key "${key}", using the location set on vertex output.`, diff --git a/packages/typegpu/src/core/pipeline/webgpuLimitations.ts b/packages/typegpu/src/core/pipeline/webgpuLimitations.ts new file mode 100644 index 0000000000..60132e566a --- /dev/null +++ b/packages/typegpu/src/core/pipeline/webgpuLimitations.ts @@ -0,0 +1,111 @@ +import { alignmentOf } from '../../data/alignmentOf.ts'; +import { memoryLayoutOf } from '../../data/offsetUtils.ts'; +import { sizeOf } from '../../data/sizeOf.ts'; +import { isWgslArray, isWgslStruct, type BaseData } from '../../data/wgslTypes.ts'; +import { invariant } from '../../errors.ts'; +import { getName } from '../../internal.ts'; +import { roundUp } from '../../mathUtils.ts'; +import type { TgpuBindGroupLayout } from '../../tgpuBindGroupLayout.ts'; +import { logger } from '../../tgpuLogger.ts'; + +/** + * Warns if layout exceeds supported buffer count limits. + */ +export function warnIfOverflow(layouts: TgpuBindGroupLayout[], limits: GPUSupportedLimits) { + const entries = Object.values(layouts) + .flatMap((layout) => Object.values(layout.entries)) + .filter((entry) => entry !== null); + + const uniform = entries.filter((entry) => 'uniform' in entry).length; + const storage = entries.filter((entry) => 'storage' in entry).length; + + if (uniform > limits.maxUniformBuffersPerShaderStage) { + logger.warn( + 'webgpu-limits-exceeded', + `Total number of uniform buffers (${uniform}) exceeds maxUniformBuffersPerShaderStage (${limits.maxUniformBuffersPerShaderStage}). Consider: +1. Grouping some of the uniforms into one using 'd.struct', +2. Increasing the limit when requesting a device or creating a root.`, + ); + } + + if (storage > limits.maxStorageBuffersPerShaderStage) { + logger.warn( + 'webgpu-limits-exceeded', + `Total number of storage buffers (${storage}) exceeds maxStorageBuffersPerShaderStage (${limits.maxStorageBuffersPerShaderStage}).`, + ); + } +} + +function requiredAlignOf(schema: BaseData) { + if (isWgslStruct(schema) || isWgslArray(schema)) { + return roundUp(alignmentOf(schema), 16); + } + return alignmentOf(schema); +} + +/** + * See https://www.w3.org/TR/WGSL/#address-space-layout-constraints + */ +export function warnIfNotUniformAligned(schema: BaseData) { + if (isWgslArray(schema)) { + warnIfNotUniformAligned(schema.elementType); + + const stride = roundUp(sizeOf(schema.elementType), alignmentOf(schema.elementType)); + if (stride % 16) { + logger.warn( + 'uniform-schema-misaligned', + `\ +Schema '${getName(schema.elementType) ?? ''}' is used in an array in an uniform buffer, and its stride (${stride}) is not a multiple of 16. +This is not portable (see https://www.w3.org/TR/WGSL/#address-space-layout-constraints), and will break on some devices. +To address this, wrap the element in 'd.align(16, ...)'.`, + ); + } + } + if (isWgslStruct(schema)) { + Object.values(schema.propTypes).forEach(warnIfNotUniformAligned); + + Object.entries(schema.propTypes).forEach(([key, value]) => { + const offset = memoryLayoutOf(schema, (schema) => schema[key]).offset; + const requiredAlignment = requiredAlignOf(value); + + if (offset % requiredAlignment) { + logger.warn( + 'uniform-schema-misaligned', + `\ +Schema '${getName(schema) ?? ''}' is used in an uniform buffer, and its property '${key}' does not meet required alignment (offset is ${offset}, required alignment is ${requiredAlignment}). +This is not portable (see https://www.w3.org/TR/WGSL/#address-space-layout-constraints), and will break on some devices. +To address this, wrap the property '${key}' in 'd.align(${requiredAlignment}, ...)'.`, + ); + } + }); + + const keys = Object.keys(schema.propTypes); + for (let i = 0; i < keys.length - 1; i++) { + const thisKey = keys[i]; + const nextKey = keys[i + 1]; + invariant(thisKey && nextKey); + + const thisValue = schema.propTypes[thisKey]; + invariant(thisValue); + + if (!isWgslStruct(thisValue)) { + continue; + } + + const minimumDifference = roundUp(16, sizeOf(thisValue)); + const thisKeyOffset = memoryLayoutOf(schema, (schema) => schema[thisKey]).offset; + const nextKeyOffset = memoryLayoutOf(schema, (schema) => schema[nextKey]).offset; + const difference = nextKeyOffset - thisKeyOffset; + + if (minimumDifference > difference) { + logger.warn( + 'uniform-schema-misaligned', + `\ +Schema '${getName(schema) ?? ''}' is used in an uniform buffer, and the difference between memory offsets of '${thisKey}' and '${nextKey}' props (${difference}) is less than recommended (${minimumDifference}). +This is not portable (see https://www.w3.org/TR/WGSL/#address-space-layout-constraints), and will break on some devices. +To address this, wrap the '${thisKey}' prop in 'd.size(${minimumDifference}, ...)'.`, + ); + } + } + } +} diff --git a/packages/typegpu/src/core/resolve/externals.ts b/packages/typegpu/src/core/resolve/externals.ts index 856f3cbb8b..4d25e11068 100644 --- a/packages/typegpu/src/core/resolve/externals.ts +++ b/packages/typegpu/src/core/resolve/externals.ts @@ -1,6 +1,7 @@ import { isLooseData } from '../../data/dataTypes.ts'; import { isWgslStruct } from '../../data/wgslTypes.ts'; import { getName, hasTinyestMetadata, isNamable, setName } from '../../shared/meta.ts'; +import { logger } from '../../tgpuLogger.ts'; import { isWgsl, type ResolutionCtx } from '../../types.ts'; import type { FnExternals } from '../function/fnCore.ts'; @@ -124,7 +125,8 @@ export function replaceExternalsInWgsl( } if (typeof currentItem !== 'object' || currentItem === null || i === chain.length - 1) { - console.warn( + logger.warn( + 'external-omitted', `During resolution, the external '${chain.slice(0, i + 1).join('.')}' has been omitted. Only TGPU resources, 'use gpu' functions, primitives, and plain JS objects can be used as externals.`, ); return match; diff --git a/packages/typegpu/src/core/resolve/tgpuResolve.ts b/packages/typegpu/src/core/resolve/tgpuResolve.ts index ecd6b42df4..a0b07a3475 100644 --- a/packages/typegpu/src/core/resolve/tgpuResolve.ts +++ b/packages/typegpu/src/core/resolve/tgpuResolve.ts @@ -3,6 +3,7 @@ import { Void } from '../../data/wgslTypes.ts'; import { type ResolutionResult, resolve as resolveImpl } from '../../resolutionCtx.ts'; import { $internal, $resolve } from '../../shared/symbols.ts'; import { isBindGroupLayout } from '../../tgpuBindGroupLayout.ts'; +import { logger } from '../../tgpuLogger.ts'; import type { ShaderGenerator } from '../../tgsl/shaderGenerator.ts'; import type { ResolvableObject, SelfResolvable, Wgsl } from '../../types.ts'; import type { WgslEnableExtension } from '../../wgslExtensions.ts'; @@ -190,7 +191,8 @@ function resolveFromTemplate(options: TgpuExtendedResolveOptions): ResolutionRes } = options; if (!template) { - console.warn( + logger.warn( + 'deprecated', "Calling resolve with an empty template is deprecated and will soon return an empty string. Consider using the 'tgpu.resolve(resolvableArray, options)' API instead.", ); } diff --git a/packages/typegpu/src/core/root/init.ts b/packages/typegpu/src/core/root/init.ts index 504f0dc2dd..d5439452cb 100644 --- a/packages/typegpu/src/core/root/init.ts +++ b/packages/typegpu/src/core/root/init.ts @@ -81,6 +81,7 @@ import { u32 } from '../../data/numeric.ts'; import { ceil } from '../../std/numeric.ts'; import { allEq } from '../../std/boolean.ts'; import { getName, setName } from '../../shared/meta.ts'; +import { logger } from '../../tgpuLogger.ts'; /** * Changes the given array to a vec of 3 numbers, filling missing values with 1. @@ -659,7 +660,7 @@ class TgpuRootImpl extends WithBindingImpl implements TgpuRoot, ExperimentalTgpu } flush() { - console.warn('flush() has been deprecated, and has no effect.'); + logger.warn('deprecated', 'flush() has been deprecated, and has no effect.'); } } @@ -741,7 +742,10 @@ export async function init(options?: InitOptions): Promise { if (adapter.features.has(feature)) { availableFeatures.push(feature); } else { - console.warn(`Optional feature "${feature}" is not supported by the adapter.`); + logger.warn( + 'webgpu-feature-missing', + `Optional feature "${feature}" is not supported by the adapter.`, + ); } } diff --git a/packages/typegpu/src/core/texture/texture.ts b/packages/typegpu/src/core/texture/texture.ts index 5103ec081c..f496b7d34a 100644 --- a/packages/typegpu/src/core/texture/texture.ts +++ b/packages/typegpu/src/core/texture/texture.ts @@ -33,6 +33,7 @@ import type { SampledFlag, } from './usageExtension.ts'; import { generateTextureMipmaps, getImageSourceDimensions, resampleImage } from './textureUtils.ts'; +import { logger } from '../../tgpuLogger.ts'; export type TextureInternals = { unwrap(): GPUTexture; @@ -394,7 +395,8 @@ class TgpuTextureImpl implements TgpuTexture implements TgpuTexture layerCount) { - console.warn( + logger.warn( + 'suspicious', `Too many image sources provided. Expected ${layerCount} layers, got ${source.length}. Extra sources will be ignored.`, ); } diff --git a/packages/typegpu/src/data/compiledIO.ts b/packages/typegpu/src/data/compiledIO.ts index 9ba97f39c3..4c00c4d2ad 100644 --- a/packages/typegpu/src/data/compiledIO.ts +++ b/packages/typegpu/src/data/compiledIO.ts @@ -1,4 +1,5 @@ import { roundUp } from '../mathUtils.ts'; +import { logger } from '../tgpuLogger.ts'; import { alignmentOf } from './alignmentOf.ts'; import { isDisarray, isUnstruct } from './dataTypes.ts'; import { offsetsForProps } from './offsets.ts'; @@ -289,7 +290,10 @@ export function buildWriter( export function getCompiledWriter(schema: wgsl.BaseData): CompiledWriter | undefined { if (!EVAL_ALLOWED_IN_ENV) { - console.warn('This environment does not allow eval - using default writer as fallback'); + logger.warn( + 'fallback', + 'This environment does not allow eval - using default writer as fallback', + ); return undefined; } @@ -316,7 +320,8 @@ export function getCompiledWriter(schema: wgsl.BaseData): CompiledWriter | undef compiledWriters.set(schema, fn); return fn; } catch (error) { - console.warn( + logger.warn( + 'fallback', `Failed to compile writer for schema: ${schema}\nReason: ${ error instanceof Error ? error.message : String(error) }\nFalling back to default writer`, diff --git a/packages/typegpu/src/data/dataIO.ts b/packages/typegpu/src/data/dataIO.ts index f41ee091ad..c0b8bfdc25 100644 --- a/packages/typegpu/src/data/dataIO.ts +++ b/packages/typegpu/src/data/dataIO.ts @@ -31,6 +31,7 @@ import type { BufferWriteOptions } from '../core/buffer/buffer.ts'; import { getCompiledWriter } from './compiledIO.ts'; import { getName } from '../shared/meta.ts'; import { roundUp } from '../mathUtils.ts'; +import { logger } from '../tgpuLogger.ts'; type DataWriter = ( output: ISerialOutput, @@ -455,7 +456,8 @@ export function writeData( const src = value as ArrayBufferView; const expected = sizeOf(schema); if (src.byteLength !== expected) { - console.warn( + logger.warn( + 'suspicious', `TypedArray size mismatch: schema expects ${expected} bytes, got ${src.byteLength}. ` + (src.byteLength < expected ? 'Data truncated.' : 'Excess ignored.'), ); @@ -841,7 +843,8 @@ export function writeToArrayBuffer( : new Uint8Array(data.buffer, data.byteOffset, data.byteLength); const regionSize = endOffset - startOffset; if (src.byteLength !== regionSize) { - console.warn( + logger.warn( + 'suspicious', `Buffer size mismatch: expected ${regionSize} bytes, got ${src.byteLength}. ` + (src.byteLength < regionSize ? 'Data truncated.' : 'Excess ignored.'), ); @@ -861,7 +864,8 @@ export function writeToArrayBuffer( compiledWriter(dataView, startOffset, data, isLittleEndian, endOffset); return; } catch (error) { - console.error( + logger.warn( + 'fallback', `Error when using compiled writer for data type '${ schema.type }' (${getName(schema) ?? 'unnamed'}) - this is likely a bug, please submit an issue at https://github.com/software-mansion/TypeGPU/issues\nUsing fallback writer instead.`, diff --git a/packages/typegpu/src/data/ref.ts b/packages/typegpu/src/data/ref.ts index 77dac4395a..7598ca5f94 100644 --- a/packages/typegpu/src/data/ref.ts +++ b/packages/typegpu/src/data/ref.ts @@ -148,7 +148,6 @@ export function INTERNAL_createRef(value: T): ref { return false; } if (prop === '$') { - console.log('Setting ref value:', propValue); return Reflect.set(target, prop, propValue); } return Reflect.set(value as object, prop, propValue); diff --git a/packages/typegpu/src/indexNamedExports.ts b/packages/typegpu/src/indexNamedExports.ts index 12dc70eee1..809af0e2bd 100644 --- a/packages/typegpu/src/indexNamedExports.ts +++ b/packages/typegpu/src/indexNamedExports.ts @@ -42,6 +42,7 @@ export { } from './tgsl/wgslGenerator.ts'; export { readFromArrayBuffer, writeToArrayBuffer } from './data/dataIO.ts'; export { patchArrayBuffer } from './data/partialIO.ts'; +export { warn } from './tgpuLogger.ts'; // types diff --git a/packages/typegpu/src/shared/stringify.ts b/packages/typegpu/src/shared/stringify.ts index 7af99b8e0e..57c32a7269 100644 --- a/packages/typegpu/src/shared/stringify.ts +++ b/packages/typegpu/src/shared/stringify.ts @@ -1,4 +1,5 @@ import { isMatInstance, isVecInstance } from '../data/wgslTypes.ts'; +import { logger } from '../tgpuLogger.ts'; export function safeStringify(item: unknown): string { if (Array.isArray(item)) { @@ -13,7 +14,7 @@ export function safeStringify(item: unknown): string { try { return JSON.stringify(item); } catch (error) { - console.error('Error parsing JSON:', error); + logger.warn('suspicious', 'Error parsing JSON:', error); return ''; } } diff --git a/packages/typegpu/src/tgpuLogger.ts b/packages/typegpu/src/tgpuLogger.ts new file mode 100644 index 0000000000..822cc90e3d --- /dev/null +++ b/packages/typegpu/src/tgpuLogger.ts @@ -0,0 +1,79 @@ +import { DEV, TEST } from './shared/env.ts'; + +const warningTypes = [ + 'deprecated', + 'suspicious', + 'fallback', + + 'precision-loss', + 'implicit-conversion', + + 'webgpu-feature-missing', + 'webgpu-limits-exceeded', + 'locations-mismatched', + 'log-limit-exceeded', + 'external-omitted', + 'uniform-schema-misaligned', +] as const; +type WarningType = (typeof warningTypes)[number]; + +// internal API +interface Logger { + warn(type: WarningType, ...args: unknown[]): void; +} + +/** + * Use this object to globally disable TypeGPU warnings. + * All warnings are better addressed than silenced, only use this when absolutely necessary. + * + * By default, lesser warnings are already silenced in production environment. + */ +interface Warn { + /** + * Globally disables one kind of warnings. + * Do not use unless absolutely necessary. + */ + disable(type: WarningType): void; + /** + * Restores the initial state. + */ + reset(): void; +} + +export class TgpuLogger implements Logger, Warn { + #initialEnabledWarnings: readonly WarningType[]; + #enabledWarnings: Set; + + constructor(prod: boolean) { + if (prod) { + this.#initialEnabledWarnings = [ + 'webgpu-feature-missing', + 'webgpu-limits-exceeded', + 'locations-mismatched', + 'log-limit-exceeded', + 'external-omitted', + ]; + } else { + this.#initialEnabledWarnings = warningTypes; + } + this.#enabledWarnings = new Set(this.#initialEnabledWarnings); + } + + disable(type: WarningType) { + this.#enabledWarnings.delete(type); + } + + reset() { + this.#enabledWarnings = new Set(this.#initialEnabledWarnings); + } + + warn(type: WarningType, ...args: unknown[]) { + if (this.#enabledWarnings.has(type)) { + console.warn(`⚠️ [${type}] `, ...args); + } + } +} + +const tgpuLogger = new TgpuLogger(!(DEV || TEST)); +export const logger: Logger = tgpuLogger; +export const warn: Warn = tgpuLogger; diff --git a/packages/typegpu/src/tgsl/consoleLog/deserializers.ts b/packages/typegpu/src/tgsl/consoleLog/deserializers.ts index bc89a0eec5..42d06a18d7 100644 --- a/packages/typegpu/src/tgsl/consoleLog/deserializers.ts +++ b/packages/typegpu/src/tgsl/consoleLog/deserializers.ts @@ -29,6 +29,7 @@ import type { Infer } from '../../shared/repr.ts'; import { niceStringify } from '../../shared/stringify.ts'; import { bitcast } from '../../std/bitcast.ts'; import { unpack2x16float } from '../../std/packing.ts'; +import { logger } from '../../tgpuLogger.ts'; import type { LogMeta, LogResources } from './types.ts'; const toF = (n: number | undefined) => bitcast(u32, f32)(n ?? 0); @@ -186,7 +187,8 @@ export function logDataFromGPU(resources: LogResources) { void indexBuffer.read().then((totalCalls) => { if (totalCalls > options.logCountLimit) { - console.warn( + logger.warn( + 'log-limit-exceeded', `Log count limit per dispatch (${options.logCountLimit}) exceeded by ${ totalCalls - options.logCountLimit } calls. Consider increasing the limit by passing appropriate options to tgpu.init().`, diff --git a/packages/typegpu/src/tgsl/consoleLog/logGenerator.ts b/packages/typegpu/src/tgsl/consoleLog/logGenerator.ts index ebcf984717..41182d4330 100644 --- a/packages/typegpu/src/tgsl/consoleLog/logGenerator.ts +++ b/packages/typegpu/src/tgsl/consoleLog/logGenerator.ts @@ -17,6 +17,7 @@ import { } from '../../data/wgslTypes.ts'; import { invariant } from '../../errors.ts'; import { $internal } from '../../shared/symbols.ts'; +import { logger } from '../../tgpuLogger.ts'; import { convertToCommonType } from '../conversion.ts'; import { concretizeSnippet, type GenerationCtx } from '../generationHelpers.ts'; import { createLoggingFunction } from './serializers.ts'; @@ -42,7 +43,7 @@ export class LogGeneratorNullImpl implements LogGenerator { return undefined; } generateLog(): Snippet { - console.warn("'console.log' is only supported when resolving pipelines."); + logger.warn('fallback', "'console.log' is only supported when resolving pipelines."); return fallbackSnippet; } } @@ -79,7 +80,7 @@ export class LogGeneratorImpl implements LogGenerator { */ generateLog(ctx: GenerationCtx, op: SupportedLogOp, args: Snippet[]): Snippet { if (shaderStageSlot.$ === 'vertex') { - console.warn(`'console' operations are not supported in vertex shaders.`); + logger.warn('suspicious', `'console' operations are not supported in vertex shaders.`); return fallbackSnippet; } diff --git a/packages/typegpu/src/tgsl/conversion.ts b/packages/typegpu/src/tgsl/conversion.ts index f64492ccb9..8d83f8c7aa 100644 --- a/packages/typegpu/src/tgsl/conversion.ts +++ b/packages/typegpu/src/tgsl/conversion.ts @@ -19,10 +19,10 @@ import { type WgslStruct, } from '../data/wgslTypes.ts'; import { invariant, WgslTypeError } from '../errors.ts'; -import { DEV, TEST } from '../shared/env.ts'; import { getName } from '../shared/meta.ts'; import { safeStringify } from '../shared/stringify.ts'; import { assertExhaustive } from '../shared/utilityTypes.ts'; +import { logger } from '../tgpuLogger.ts'; import type { ResolutionCtx } from '../types.ts'; import { accessStructProp } from './accessStructProp.ts'; @@ -363,19 +363,19 @@ export function convertToCommonType( return undefined; } - if (DEV && Array.isArray(restrictTo) && restrictTo.length === 0) { - console.warn( - 'convertToCommonType was called with an empty restrictTo array, which prevents any conversions from being made. If you intend to allow all conversions, pass undefined instead. If this was intended call the function conditionally since the result will always be undefined.', - ); - } + invariant( + !(Array.isArray(restrictTo) && restrictTo.length === 0), + "Internal error, expected 'restrictTo' to not be an empty array.", + ); const conversion = getBestConversion(types as BaseData[], restrictTo); if (!conversion) { return undefined; } - if ((TEST || DEV) && verbose && conversion.hasImplicitConversions) { - console.warn( + if (verbose && conversion.hasImplicitConversions) { + logger.warn( + 'implicit-conversion', `Implicit conversions from [\n${values .map((v) => ` ${ctx.resolveSnippet(v).value}: ${safeStringify(v.dataType)}`) .join(',\n')}\n] to ${conversion.targetType.type} are supported, but not recommended. diff --git a/packages/typegpu/src/tgsl/generationHelpers.ts b/packages/typegpu/src/tgsl/generationHelpers.ts index 3eb4d37b56..f35794db33 100644 --- a/packages/typegpu/src/tgsl/generationHelpers.ts +++ b/packages/typegpu/src/tgsl/generationHelpers.ts @@ -24,6 +24,7 @@ import type { ShelllessRepository } from './shellless.ts'; import { WgslTypeError } from '../errors.ts'; import { $internal, $resolve } from '../shared/symbols.ts'; import type { SupportedLogOp } from './consoleLog/types.ts'; +import { logger } from '../tgpuLogger.ts'; export function numericLiteralToSnippet(value: number): Snippet { if (value >= 2 ** 63 || value < -(2 ** 63)) { @@ -33,7 +34,8 @@ export function numericLiteralToSnippet(value: number): Snippet { // Warn when values exceed this range to prevent precision loss. if (Number.isInteger(value)) { if (!Number.isSafeInteger(value)) { - console.warn( + logger.warn( + 'precision-loss', `The integer ${value} exceeds the safe integer range and may have lost precision.`, ); } diff --git a/packages/typegpu/tests/buffer.test.ts b/packages/typegpu/tests/buffer.test.ts index 3473bb79ae..b8f8dd3c17 100644 --- a/packages/typegpu/tests/buffer.test.ts +++ b/packages/typegpu/tests/buffer.test.ts @@ -1732,3 +1732,147 @@ describe('ValidateBufferSchema', () => { ); }); }); + +describe('Uniform alignment', () => { + it('does not report legit schemas', ({ root }) => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + root.createUniform(d.u32); + root.createUniform(d.struct({ p: d.u32 })); + root.createUniform(d.struct({ p: d.struct({ p: d.u32 }), q: d.vec4f })); + root.createUniform(d.struct({ p: d.struct({ p: d.u32 }), q: d.align(16, d.u32) })); + root.createUniform(d.struct({ p: d.struct({ p: d.u32 }), q: d.align(32, d.u32) })); + root.createUniform(d.struct({ p: d.struct({ p: d.u32 }), q: d.vec3f })); + root.createUniform(d.struct({ p: d.size(16, d.struct({ p: d.u32 })), q: d.u32 })); + root.createUniform(d.struct({ p: d.size(32, d.struct({ p: d.u32 })), q: d.u32 })); + root.createUniform(d.arrayOf(d.vec4f, 3)); + root.createUniform(d.arrayOf(d.vec3f, 3)); + root.createUniform(d.arrayOf(d.align(16, d.u32), 3)); + root.createUniform(d.arrayOf(d.struct({ p: d.vec3f }), 3)); + + expect(consoleWarnSpy).not.toHaveBeenCalled(); + }); + + it('reports props not meeting requiredAlignOf in structs', ({ root }) => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + root.createUniform(d.struct({ q: d.u32, p: d.struct({ p: d.u32 }) })); + + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [uniform-schema-misaligned] ", + "Schema '' is used in an uniform buffer, and its property 'p' does not meet required alignment (offset is 4, required alignment is 16). + This is not portable (see https://www.w3.org/TR/WGSL/#address-space-layout-constraints), and will break on some devices. + To address this, wrap the property 'p' in 'd.align(16, ...)'.", + ] + `); + }); + + it('reports unaligned props in structs', ({ root }) => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + root.createUniform(d.struct({ p: d.struct({ p: d.u32 }), q: d.u32 })); + + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [uniform-schema-misaligned] ", + "Schema '' is used in an uniform buffer, and the difference between memory offsets of 'p' and 'q' props (4) is less than recommended (16). + This is not portable (see https://www.w3.org/TR/WGSL/#address-space-layout-constraints), and will break on some devices. + To address this, wrap the 'p' prop in 'd.size(16, ...)'.", + ] + `); + }); + + it('reports further unaligned props in structs', ({ root }) => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + root.createUniform(d.struct({ p: d.vec4f, q: d.struct({ p: d.u32 }), r: d.u32 })); + + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [uniform-schema-misaligned] ", + "Schema '' is used in an uniform buffer, and the difference between memory offsets of 'q' and 'r' props (4) is less than recommended (16). + This is not portable (see https://www.w3.org/TR/WGSL/#address-space-layout-constraints), and will break on some devices. + To address this, wrap the 'q' prop in 'd.size(16, ...)'.", + ] + `); + }); + + it('reports nested unaligned props in structs', ({ root }) => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + root.createUniform( + d.struct({ p: d.vec4f, q: d.struct({ p: d.struct({ p: d.u32 }), q: d.u32 }) }), + ); + + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [uniform-schema-misaligned] ", + "Schema 'q' is used in an uniform buffer, and the difference between memory offsets of 'p' and 'q' props (4) is less than recommended (16). + This is not portable (see https://www.w3.org/TR/WGSL/#address-space-layout-constraints), and will break on some devices. + To address this, wrap the 'p' prop in 'd.size(16, ...)'.", + ] + `); + }); + + it('reports unaligned arrays', ({ root }) => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + root.createUniform(d.arrayOf(d.u32, 3)); + + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [uniform-schema-misaligned] ", + "Schema 'u32' is used in an array in an uniform buffer, and its stride (4) is not a multiple of 16. + This is not portable (see https://www.w3.org/TR/WGSL/#address-space-layout-constraints), and will break on some devices. + To address this, wrap the element in 'd.align(16, ...)'.", + ] + `); + }); + + it('reports nested unaligned arrays', ({ root }) => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + root.createUniform(d.arrayOf(d.arrayOf(d.u32, 4), 4)); + + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [uniform-schema-misaligned] ", + "Schema 'u32' is used in an array in an uniform buffer, and its stride (4) is not a multiple of 16. + This is not portable (see https://www.w3.org/TR/WGSL/#address-space-layout-constraints), and will break on some devices. + To address this, wrap the element in 'd.align(16, ...)'.", + ] + `); + }); + + it('reports when giving usage', ({ root }) => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + root.createBuffer(d.arrayOf(d.u32, 2)).$usage('uniform'); + + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [uniform-schema-misaligned] ", + "Schema 'u32' is used in an array in an uniform buffer, and its stride (4) is not a multiple of 16. + This is not portable (see https://www.w3.org/TR/WGSL/#address-space-layout-constraints), and will break on some devices. + To address this, wrap the element in 'd.align(16, ...)'.", + ] + `); + }); + + it('does not report twice', ({ root }) => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + root.createBuffer(d.arrayOf(d.u32, 2)).$usage('uniform').as('uniform'); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [uniform-schema-misaligned] ", + "Schema 'u32' is used in an array in an uniform buffer, and its stride (4) is not a multiple of 16. + This is not portable (see https://www.w3.org/TR/WGSL/#address-space-layout-constraints), and will break on some devices. + To address this, wrap the element in 'd.align(16, ...)'.", + ] + `); + }); +}); diff --git a/packages/typegpu/tests/computePipeline.test.ts b/packages/typegpu/tests/computePipeline.test.ts index e39927a5a7..92b10789a8 100644 --- a/packages/typegpu/tests/computePipeline.test.ts +++ b/packages/typegpu/tests/computePipeline.test.ts @@ -98,9 +98,12 @@ describe('TgpuComputePipeline', () => { // no-op expect(after).toBe(before); }).not.toThrow(); - expect(consoleWarnSpy).toHaveBeenCalledWith( - 'Performance callback cannot be used because the timestamp-query feature is not enabled on the root.', - ); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [webgpu-feature-missing] ", + "Performance callback cannot be used because the timestamp-query feature is not enabled on the root.", + ] + `); }); it('should setup timestamp writes in compute pass descriptor', ({ root, commandEncoder }) => { @@ -329,14 +332,20 @@ describe('TgpuComputePipeline', () => { pipeline.dispatchThreads(); - expect(consoleWarnSpy).toHaveBeenCalledWith( - `Total number of uniform buffers (14) exceeds maxUniformBuffersPerShaderStage (12). Consider: -1. Grouping some of the uniforms into one using 'd.struct', -2. Increasing the limit when requesting a device or creating a root.`, - ); - expect(consoleWarnSpy).toHaveBeenCalledWith( - `Total number of storage buffers (9) exceeds maxStorageBuffersPerShaderStage (8).`, - ); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [webgpu-limits-exceeded] ", + "Total number of uniform buffers (14) exceeds maxUniformBuffersPerShaderStage (12). Consider: + 1. Grouping some of the uniforms into one using 'd.struct', + 2. Increasing the limit when requesting a device or creating a root.", + ] + `); + expect(consoleWarnSpy.mock.calls[1]).toMatchInlineSnapshot(` + [ + "⚠️ [webgpu-limits-exceeded] ", + "Total number of storage buffers (9) exceeds maxStorageBuffersPerShaderStage (8).", + ] + `); }); describe('dispatchWorkgroupsIndirect', () => { @@ -413,9 +422,12 @@ describe('TgpuComputePipeline', () => { d.memoryLayoutOf(PaddedStruct, (s) => s.a), ); - expect(warnSpy.mock.calls[0]![0]).toMatchInlineSnapshot( - `"dispatchWorkgroupsIndirect: Starting at offset 0, only 4 contiguous bytes are available before padding. 'dispatchWorkgroupsIndirect' requires 12 bytes (3 x u32). Reading across padding may result in undefined behavior."`, - ); + expect(warnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [suspicious] ", + "dispatchWorkgroupsIndirect: Starting at offset 0, only 4 contiguous bytes are available before padding. 'dispatchWorkgroupsIndirect' requires 12 bytes (3 x u32). Reading across padding may result in undefined behavior.", + ] + `); const deepBuffer = root.createBuffer(DeepStruct).$usage('indirect'); pipeline.dispatchWorkgroupsIndirect( @@ -423,18 +435,24 @@ describe('TgpuComputePipeline', () => { d.memoryLayoutOf(DeepStruct, (s) => s.someData[11]), ); - expect(warnSpy.mock.calls[1]![0]).toMatchInlineSnapshot( - `"dispatchWorkgroupsIndirect: Starting at offset 44, only 8 contiguous bytes are available before padding. 'dispatchWorkgroupsIndirect' requires 12 bytes (3 x u32). Reading across padding may result in undefined behavior."`, - ); + expect(warnSpy.mock.calls[1]).toMatchInlineSnapshot(` + [ + "⚠️ [suspicious] ", + "dispatchWorkgroupsIndirect: Starting at offset 44, only 8 contiguous bytes are available before padding. 'dispatchWorkgroupsIndirect' requires 12 bytes (3 x u32). Reading across padding may result in undefined behavior.", + ] + `); pipeline.dispatchWorkgroupsIndirect( deepBuffer, d.memoryLayoutOf(DeepStruct, (s) => s.nested.innerNested[0]?.yy), ); - expect(warnSpy.mock.calls[2]![0]).toMatchInlineSnapshot( - `"dispatchWorkgroupsIndirect: Starting at offset 84, only 8 contiguous bytes are available before padding. 'dispatchWorkgroupsIndirect' requires 12 bytes (3 x u32). Reading across padding may result in undefined behavior."`, - ); + expect(warnSpy.mock.calls[2]).toMatchInlineSnapshot(` + [ + "⚠️ [suspicious] ", + "dispatchWorkgroupsIndirect: Starting at offset 84, only 8 contiguous bytes are available before padding. 'dispatchWorkgroupsIndirect' requires 12 bytes (3 x u32). Reading across padding may result in undefined behavior.", + ] + `); }); it('does not warn when dispatch has sufficient contiguous data', ({ root }) => { diff --git a/packages/typegpu/tests/internal/limitsOverflow.test.ts b/packages/typegpu/tests/internal/limitsOverflow.test.ts index 83da7a4b30..2bc7b53006 100644 --- a/packages/typegpu/tests/internal/limitsOverflow.test.ts +++ b/packages/typegpu/tests/internal/limitsOverflow.test.ts @@ -1,7 +1,7 @@ import { describe, expect, vi } from 'vitest'; import { it } from 'typegpu-testing-utility'; import { tgpu, d } from 'typegpu'; -import { warnIfOverflow } from '../../src/core/pipeline/limitsOverflow.ts'; +import { warnIfOverflow } from '../../src/core/pipeline/webgpuLimitations.ts'; describe('warnIfOverflow', () => { const limits = { @@ -35,11 +35,14 @@ describe('warnIfOverflow', () => { warnIfOverflow([layout], limits); - expect(consoleWarnSpy).toHaveBeenCalledWith( - `Total number of uniform buffers (3) exceeds maxUniformBuffersPerShaderStage (2). Consider: -1. Grouping some of the uniforms into one using 'd.struct', -2. Increasing the limit when requesting a device or creating a root.`, - ); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [webgpu-limits-exceeded] ", + "Total number of uniform buffers (3) exceeds maxUniformBuffersPerShaderStage (2). Consider: + 1. Grouping some of the uniforms into one using 'd.struct', + 2. Increasing the limit when requesting a device or creating a root.", + ] + `); }); it('warns for storages', () => { @@ -52,9 +55,12 @@ describe('warnIfOverflow', () => { warnIfOverflow([layout], limits); - expect(consoleWarnSpy).toHaveBeenCalledWith( - `Total number of storage buffers (2) exceeds maxStorageBuffersPerShaderStage (1).`, - ); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [webgpu-limits-exceeded] ", + "Total number of storage buffers (2) exceeds maxStorageBuffersPerShaderStage (1).", + ] + `); }); it('warns when resources are split among layouts', () => { @@ -74,10 +80,13 @@ describe('warnIfOverflow', () => { warnIfOverflow([layout1, layout2, layout3], limits); - expect(consoleWarnSpy).toHaveBeenCalledWith( - `Total number of uniform buffers (3) exceeds maxUniformBuffersPerShaderStage (2). Consider: -1. Grouping some of the uniforms into one using 'd.struct', -2. Increasing the limit when requesting a device or creating a root.`, - ); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [webgpu-limits-exceeded] ", + "Total number of uniform buffers (3) exceeds maxUniformBuffersPerShaderStage (2). Consider: + 1. Grouping some of the uniforms into one using 'd.struct', + 2. Increasing the limit when requesting a device or creating a root.", + ] + `); }); }); diff --git a/packages/typegpu/tests/internal/tgpuLogger.test.ts b/packages/typegpu/tests/internal/tgpuLogger.test.ts new file mode 100644 index 0000000000..0bcdd64083 --- /dev/null +++ b/packages/typegpu/tests/internal/tgpuLogger.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it, vi } from 'vitest'; +import { TgpuLogger } from '../../src/tgpuLogger.ts'; + +describe('tgpuLogger', () => { + it('warns through console.warn', () => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const logger = new TgpuLogger(false); + + logger.warn('deprecated', 'this is deprecated'); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [deprecated] ", + "this is deprecated", + ] + `); + }); + + it('does not warn after disabling', () => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const logger = new TgpuLogger(false); + + logger.disable('deprecated'); + logger.warn('deprecated', 'this is deprecated'); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(0); + }); + + it('starts warning after reset', () => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const logger = new TgpuLogger(false); + + logger.disable('deprecated'); + logger.reset(); + logger.warn('deprecated', 'this is deprecated'); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [deprecated] ", + "this is deprecated", + ] + `); + }); + + it('works with multiple arguments', () => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const logger = new TgpuLogger(false); + + logger.warn('suspicious', 'there is an impostor among us', 42, { prop: 1 }); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [suspicious] ", + "there is an impostor among us", + 42, + { + "prop": 1, + }, + ] + `); + }); + + it('only silences the disabled type', () => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const logger = new TgpuLogger(false); + + logger.disable('deprecated'); + logger.warn('suspicious', 'still warns'); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [suspicious] ", + "still warns", + ] + `); + }); + + it('has stricter rules in prod mode', () => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const logger = new TgpuLogger(true); + + logger.warn('suspicious', '...'); + logger.warn('deprecated', '...'); + logger.warn('fallback', '...'); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(0); + }); + + it('correctly resets to initial state', () => { + using consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const logger = new TgpuLogger(true); + + logger.reset(); + logger.warn('suspicious', '...'); + logger.warn('deprecated', '...'); + logger.warn('fallback', '...'); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages/typegpu/tests/renderPipeline.test.ts b/packages/typegpu/tests/renderPipeline.test.ts index 2ad3410849..58a1569af6 100644 --- a/packages/typegpu/tests/renderPipeline.test.ts +++ b/packages/typegpu/tests/renderPipeline.test.ts @@ -291,9 +291,12 @@ describe('render pipeline behavior', () => { }); tgpu.resolve([pipeline]); - expect(consoleWarnSpy).toHaveBeenCalledWith( - 'Mismatched location between vertexFn (vertexMain) output (0) and fragmentFn (fragmentMain) input (1) for the key "bar", using the location set on vertex output.', - ); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [locations-mismatched] ", + "Mismatched location between vertexFn (vertexMain) output (0) and fragmentFn (fragmentMain) input (1) for the key "bar", using the location set on vertex output.", + ] + `); }); it('does not log warning when resolving pipeline having vertex and fragment functions with non-conflicting user-defined locations', ({ @@ -355,9 +358,12 @@ describe('render pipeline behavior', () => { // no-op expect(after).toBe(before); }).not.toThrow(); - expect(consoleWarnSpy).toHaveBeenCalledWith( - 'Performance callback cannot be used because the timestamp-query feature is not enabled on the root.', - ); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [webgpu-feature-missing] ", + "Performance callback cannot be used because the timestamp-query feature is not enabled on the root.", + ] + `); }); it("should not throw 'A color target was not provided to the shader'", ({ root }) => { @@ -597,14 +603,20 @@ describe('render pipeline behavior', () => { }) .draw(3); - expect(consoleWarnSpy).toHaveBeenCalledWith( - `Total number of uniform buffers (13) exceeds maxUniformBuffersPerShaderStage (12). Consider: -1. Grouping some of the uniforms into one using 'd.struct', -2. Increasing the limit when requesting a device or creating a root.`, - ); - expect(consoleWarnSpy).toHaveBeenCalledWith( - `Total number of storage buffers (9) exceeds maxStorageBuffersPerShaderStage (8).`, - ); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [webgpu-limits-exceeded] ", + "Total number of uniform buffers (13) exceeds maxUniformBuffersPerShaderStage (12). Consider: + 1. Grouping some of the uniforms into one using 'd.struct', + 2. Increasing the limit when requesting a device or creating a root.", + ] + `); + expect(consoleWarnSpy.mock.calls[1]).toMatchInlineSnapshot(` + [ + "⚠️ [webgpu-limits-exceeded] ", + "Total number of storage buffers (9) exceeds maxStorageBuffersPerShaderStage (8).", + ] + `); }); }); @@ -1577,9 +1589,12 @@ describe('drawIndirect / drawIndexedIndirect buffer and offset validation', () = d.memoryLayoutOf(DeepStruct, (s) => s.someData[10]), ); - expect(warnSpy.mock.calls[0]![0]).toMatchInlineSnapshot( - `"drawIndirect: Starting at offset 40, only 12 contiguous bytes are available before padding. 'drawIndirect' requires 16 bytes (4 x u32). Reading across padding may result in undefined behavior."`, - ); + expect(warnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [suspicious] ", + "drawIndirect: Starting at offset 40, only 12 contiguous bytes are available before padding. 'drawIndirect' requires 16 bytes (4 x u32). Reading across padding may result in undefined behavior.", + ] + `); }); it('does not warn when draw has sufficient contiguous data', ({ root }) => { @@ -1648,9 +1663,12 @@ describe('drawIndirect / drawIndexedIndirect buffer and offset validation', () = d.memoryLayoutOf(DeepStruct, (s) => s.someData[9]), ); - expect(warnSpy.mock.calls[0]![0]).toMatchInlineSnapshot( - `"drawIndexedIndirect: Starting at offset 36, only 16 contiguous bytes are available before padding. 'drawIndexedIndirect' requires 20 bytes (3 x u32, i32, u32). Reading across padding may result in undefined behavior."`, - ); + expect(warnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [suspicious] ", + "drawIndexedIndirect: Starting at offset 36, only 16 contiguous bytes are available before padding. 'drawIndexedIndirect' requires 20 bytes (3 x u32, i32, u32). Reading across padding may result in undefined behavior.", + ] + `); }); it('does not warn when drawIndexed has sufficient contiguous data', ({ root }) => { diff --git a/packages/typegpu/tests/resolve.test.ts b/packages/typegpu/tests/resolve.test.ts index e388823b3c..532b5766d6 100644 --- a/packages/typegpu/tests/resolve.test.ts +++ b/packages/typegpu/tests/resolve.test.ts @@ -456,9 +456,12 @@ describe('tgpu resolveWithContext', () => { externals: { identity: (a: number) => a }, }); - expect(consoleWarnSpy).toHaveBeenCalledWith( - "During resolution, the external 'identity' has been omitted. Only TGPU resources, 'use gpu' functions, primitives, and plain JS objects can be used as externals.", - ); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [external-omitted] ", + "During resolution, the external 'identity' has been omitted. Only TGPU resources, 'use gpu' functions, primitives, and plain JS objects can be used as externals.", + ] + `); }); it('should warn when the end of external chain was reached without a resolvable', () => { @@ -474,9 +477,12 @@ describe('tgpu resolveWithContext', () => { }" `); - expect(consoleWarnSpy).toHaveBeenCalledWith( - "During resolution, the external 'EXT.p.q' has been omitted. Only TGPU resources, 'use gpu' functions, primitives, and plain JS objects can be used as externals.", - ); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [external-omitted] ", + "During resolution, the external 'EXT.p.q' has been omitted. Only TGPU resources, 'use gpu' functions, primitives, and plain JS objects can be used as externals.", + ] + `); }); it('should not warn when In/Out are unused', () => { @@ -511,9 +517,12 @@ describe('resolve without template', () => { tgpu.resolve({ externals: { Boid }, template: '' }); - expect(consoleWarnSpy).toHaveBeenCalledWith( - "Calling resolve with an empty template is deprecated and will soon return an empty string. Consider using the 'tgpu.resolve(resolvableArray, options)' API instead.", - ); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [deprecated] ", + "Calling resolve with an empty template is deprecated and will soon return an empty string. Consider using the 'tgpu.resolve(resolvableArray, options)' API instead.", + ] + `); }); it('resolves one item', () => { diff --git a/packages/typegpu/tests/texture.test.ts b/packages/typegpu/tests/texture.test.ts index 6573f38c15..f5440c32ca 100644 --- a/packages/typegpu/tests/texture.test.ts +++ b/packages/typegpu/tests/texture.test.ts @@ -743,9 +743,12 @@ Overload 3 of 4, '(schema: "(Error) Texture not usable as storage, call $usage(' // Base mip level 3 would result in 0 mip levels to generate, so it should return early expect(() => texture.generateMipmaps(3)).not.toThrow(); - expect(consoleSpy).toHaveBeenCalledWith( - 'generateMipmaps is a no-op: would generate 0 mip levels (base: 3, total: 3)', - ); + expect(consoleSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [suspicious] ", + "generateMipmaps is a no-op: would generate 0 mip levels (base: 3, total: 3)", + ] + `); consoleSpy.mockRestore(); }); @@ -764,9 +767,12 @@ Overload 3 of 4, '(schema: "(Error) Texture not usable as storage, call $usage(' // Base mip level 2 would result in 1 mip level to generate (3-2=1), so it should warn and return early expect(() => texture.generateMipmaps(2)).not.toThrow(); - expect(consoleSpy).toHaveBeenCalledWith( - 'generateMipmaps is a no-op: would generate 1 mip levels (base: 2, total: 3)', - ); + expect(consoleSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [suspicious] ", + "generateMipmaps is a no-op: would generate 1 mip levels (base: 2, total: 3)", + ] + `); consoleSpy.mockRestore(); }); diff --git a/packages/typegpu/tests/tgsl/assignment.test.ts b/packages/typegpu/tests/tgsl/assignment.test.ts index 48b8206528..f45d7bd6e5 100644 --- a/packages/typegpu/tests/tgsl/assignment.test.ts +++ b/packages/typegpu/tests/tgsl/assignment.test.ts @@ -26,10 +26,16 @@ it('implicitly casts right-hand side, with a warning', () => { }" `); - expect(warnSpy).toHaveBeenCalledExactlyOnceWith(`\ -Implicit conversions from [ - a: i32, - arg: f32 -] to i32 are supported, but not recommended. -Consider using explicit conversions instead.`); + expect(warnSpy.mock.calls).toMatchInlineSnapshot(` + [ + [ + "⚠️ [implicit-conversion] ", + "Implicit conversions from [ + a: i32, + arg: f32 + ] to i32 are supported, but not recommended. + Consider using explicit conversions instead.", + ], + ] + `); }); diff --git a/packages/typegpu/tests/tgsl/consoleLog.test.ts b/packages/typegpu/tests/tgsl/consoleLog.test.ts index 1093d7a92c..df5233582c 100644 --- a/packages/typegpu/tests/tgsl/consoleLog.test.ts +++ b/packages/typegpu/tests/tgsl/consoleLog.test.ts @@ -16,9 +16,12 @@ describe('wgslGenerator with console.log', () => { }" `); - expect(consoleWarnSpy).toHaveBeenCalledWith( - "'console.log' is only supported when resolving pipelines.", - ); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [fallback] ", + "'console.log' is only supported when resolving pipelines.", + ] + `); expect(consoleWarnSpy).toHaveBeenCalledTimes(1); }); @@ -218,9 +221,12 @@ describe('wgslGenerator with console.log', () => { }" `); - expect(consoleWarnSpy).toHaveBeenCalledWith( - "'console' operations are not supported in vertex shaders.", - ); + expect(consoleWarnSpy.mock.calls[0]).toMatchInlineSnapshot(` + [ + "⚠️ [suspicious] ", + "'console' operations are not supported in vertex shaders.", + ] + `); expect(consoleWarnSpy).toHaveBeenCalledTimes(1); }); diff --git a/packages/typegpu/tests/tgsl/conversion.test.ts b/packages/typegpu/tests/tgsl/conversion.test.ts index 37f095c8ec..9b7243a565 100644 --- a/packages/typegpu/tests/tgsl/conversion.test.ts +++ b/packages/typegpu/tests/tgsl/conversion.test.ts @@ -38,6 +38,7 @@ describe('convertToCommonType', () => { expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` [ [ + "⚠️ [implicit-conversion] ", "Implicit conversions from [ 1i: i32, 2f: f32 @@ -170,12 +171,14 @@ describe('convertToCommonType', () => { expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` [ [ + "⚠️ [implicit-conversion] ", "Implicit conversions from [ 1i: i32 ] to f32 are supported, but not recommended. Consider using explicit conversions instead.", ], [ + "⚠️ [implicit-conversion] ", "Implicit conversions from [ 2u: u32 ] to i32 are supported, but not recommended. diff --git a/packages/typegpu/tests/tgsl/multiplication.test.ts b/packages/typegpu/tests/tgsl/multiplication.test.ts index 5d416adeb4..94459f32df 100644 --- a/packages/typegpu/tests/tgsl/multiplication.test.ts +++ b/packages/typegpu/tests/tgsl/multiplication.test.ts @@ -30,18 +30,21 @@ test('multiplying i32 with a float literal should implicitly convert to an f32', expect(consoleWarnSpy.mock.calls).toMatchInlineSnapshot(` [ [ + "⚠️ [implicit-conversion] ", "Implicit conversions from [ 1i: i32 ] to f32 are supported, but not recommended. Consider using explicit conversions instead.", ], [ + "⚠️ [implicit-conversion] ", "Implicit conversions from [ int: i32 ] to f32 are supported, but not recommended. Consider using explicit conversions instead.", ], [ + "⚠️ [implicit-conversion] ", "Implicit conversions from [ 1i: i32 ] to f32 are supported, but not recommended. @@ -77,18 +80,21 @@ test('multiplying u32 with a float literal should implicitly convert to an f32', expect(consoleWarnSpy.mock.calls).toMatchInlineSnapshot(` [ [ + "⚠️ [implicit-conversion] ", "Implicit conversions from [ 10u: u32 ] to f32 are supported, but not recommended. Consider using explicit conversions instead.", ], [ + "⚠️ [implicit-conversion] ", "Implicit conversions from [ int: u32 ] to f32 are supported, but not recommended. Consider using explicit conversions instead.", ], [ + "⚠️ [implicit-conversion] ", "Implicit conversions from [ 1u: u32 ] to f32 are supported, but not recommended. @@ -119,6 +125,7 @@ test('multiplying u32 with an i32 should implicitly convert to an i32', () => { expect(consoleWarnSpy.mock.calls).toMatchInlineSnapshot(` [ [ + "⚠️ [implicit-conversion] ", "Implicit conversions from [ uint: u32 ] to i32 are supported, but not recommended. diff --git a/packages/typegpu/tests/tgsl/typeInference.test.ts b/packages/typegpu/tests/tgsl/typeInference.test.ts index 63508d2bd8..5cba8fc88f 100644 --- a/packages/typegpu/tests/tgsl/typeInference.test.ts +++ b/packages/typegpu/tests/tgsl/typeInference.test.ts @@ -234,9 +234,17 @@ describe('wgsl generator type inference', () => { }" `); - expect(warnSpy).toHaveBeenCalledExactlyOnceWith( - 'Implicit conversions from [\n 1.1: abstractFloat\n] to u32 are supported, but not recommended.\nConsider using explicit conversions instead.', - ); + expect(warnSpy.mock.calls).toMatchInlineSnapshot(` + [ + [ + "⚠️ [implicit-conversion] ", + "Implicit conversions from [ + 1.1: abstractFloat + ] to u32 are supported, but not recommended. + Consider using explicit conversions instead.", + ], + ] + `); }); it('throws when no info about what to coerce to', () => {