Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified src/resources/cache/webgpu/shader/execution/bitcast.bin
Binary file not shown.
93 changes: 93 additions & 0 deletions src/webgpu/api/validation/compute_pipeline.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Note: entry point matching tests are in shader_module/entry_point.spec.ts
import { AllFeaturesMaxLimitsGPUTest } from '../.././gpu_test.js';
import { makeTestGroup } from '../../../common/framework/test_group.js';
import { keysOf } from '../../../common/util/data_tables.js';
import { getGPU } from '../../../common/util/navigator_gpu.js';
import { assert, supportsImmediateData } from '../../../common/util/util.js';
import {
isTextureFormatUsableWithStorageAccessMode,
kPossibleStorageTextureFormats,
Expand Down Expand Up @@ -811,3 +813,94 @@ generates a validation error at createComputePipeline(Async)
};
vtu.doCreateComputePipelineTest(t, isAsync, success, descriptor);
});

g.test('pipeline_creation_immediate_size_mismatch')
.desc(
`
Validate that creating a pipeline fails if the shader uses immediate data
larger than the immediateSize specified in the pipeline layout, or larger than
maxImmediateSize if layout is 'auto'.
Also validates that using less or equal size is allowed.
`
)
.params(u => {
const kNumericCases = [
{ shaderSize: 16, layoutSize: 16 }, // Equal
{ shaderSize: 12, layoutSize: 16 }, // Shader smaller
{ shaderSize: 20, layoutSize: 16 }, // Shader larger (small diff)
{ shaderSize: 32, layoutSize: 16 }, // Shader larger
] as const;
const kMaxLimitsCases = [
{ shaderSize: 'max', layoutSize: 'auto' }, // Shader equal to limit (auto layout)
{ shaderSize: 'exceedLimits', layoutSize: 'auto' }, // Shader larger than limit (auto layout)
] as const;
return u
.combine('isAsync', [true, false])
.combineWithParams([...kNumericCases, ...kMaxLimitsCases] as const);
})
.fn(t => {
t.skipIf(!supportsImmediateData(getGPU(t.rec)), 'Immediate data not supported');

const { isAsync, shaderSize, layoutSize } = t.params;

assert(t.device.limits.maxImmediateSize !== undefined);
const maxImmediateSize = t.device.limits.maxImmediateSize;

let actualLayout: GPUPipelineLayout | 'auto';
let validSize: number;

if (layoutSize === 'auto') {
actualLayout = 'auto';
validSize = maxImmediateSize;
} else {
actualLayout = t.device.createPipelineLayout({
bindGroupLayouts: [],
immediateSize: layoutSize as number,
});
validSize = layoutSize as number;
}

let actualShaderSize: number;
if (shaderSize === 'max') {
actualShaderSize = validSize;
} else if (shaderSize === 'exceedLimits') {
actualShaderSize = validSize + 4;
} else {
actualShaderSize = shaderSize as number;
}

// Ensure the test's fixed sizes fit within the device limit.
if (shaderSize !== 'exceedLimits') {
assert(
actualShaderSize <= maxImmediateSize,
`shaderSize (${actualShaderSize}) must be <= maxImmediateSize (${maxImmediateSize})`
);
}

const numFields = actualShaderSize / 4;
const fields = Array.from({ length: numFields }, (_, i) => `m${i}: u32`).join(', ');
const code = `
struct Immediates { ${fields} }
var<immediate> data: Immediates;
fn use_data() { _ = data.m0; }
@compute @workgroup_size(1) fn main_compute() { use_data(); }
`;

const shouldError = actualShaderSize > validSize;

// When the shader exceeds the device's maxImmediateSize, the error occurs
// at shader module creation time, not pipeline creation time.
if (layoutSize === 'auto' && shouldError) {
t.expectValidationError(() => {
t.device.createShaderModule({ code });
});
return;
}

vtu.doCreateComputePipelineTest(t, isAsync, !shouldError, {
layout: actualLayout,
compute: {
module: t.device.createShaderModule({ code }),
},
});
});
141 changes: 139 additions & 2 deletions src/webgpu/api/validation/render_pipeline/misc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ misc createRenderPipeline and createRenderPipelineAsync validation tests.
`;

import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { getGPU } from '../../../../common/util/navigator_gpu.js';
import { assert, supportsImmediateData } from '../../../../common/util/util.js';
import {
isTextureFormatUsableWithStorageAccessMode,
kPossibleStorageTextureFormats,
Expand Down Expand Up @@ -119,8 +121,10 @@ g.test('pipeline_layout,device_mismatch')
});

g.test('external_texture')
.desc('Tests createRenderPipeline() with an external_texture')
.desc('Tests createRenderPipeline(Async) with an external_texture')
.params(u => u.combine('isAsync', [false, true]))
.fn(t => {
const { isAsync } = t.params;
const shader = t.device.createShaderModule({
code: `
@vertex
Expand Down Expand Up @@ -149,7 +153,7 @@ g.test('external_texture')
},
};

vtu.doCreateRenderPipelineTest(t, false, true, descriptor);
vtu.doCreateRenderPipelineTest(t, isAsync, true, descriptor);
});

g.test('storage_texture,format')
Expand Down Expand Up @@ -192,3 +196,136 @@ generates a validation error at createComputePipeline(Async)
};
vtu.doCreateRenderPipelineTest(t, isAsync, success, descriptor);
});

g.test('pipeline_creation_immediate_size_mismatch')
.desc(
`
Validate that creating a pipeline fails if the shader uses immediate data
larger than the immediateSize specified in the pipeline layout, or larger than
maxImmediateSize if layout is 'auto'.
Also validates that using less or equal size is allowed.
`
)
.params(u => {
const kNumericCases = [
{ vertexSize: 16, fragmentSize: 16, layoutSize: 16 }, // Equal
{ vertexSize: 12, fragmentSize: 12, layoutSize: 16 }, // Shader smaller
{ vertexSize: 20, fragmentSize: 20, layoutSize: 16 }, // Shader larger (small diff)
{ vertexSize: 32, fragmentSize: 32, layoutSize: 16 }, // Shader larger
] as const;
const kMaxLimitsCases = [
{ vertexSize: 'max', fragmentSize: 0, layoutSize: 'auto' }, // Vertex = Limit (Control)
{ vertexSize: 0, fragmentSize: 'max', layoutSize: 'auto' }, // Fragment = Limit (Control)
{ vertexSize: 'max', fragmentSize: 'max', layoutSize: 'auto' }, // Both at Limit (Control)
{ vertexSize: 'exceedLimits', fragmentSize: 0, layoutSize: 'auto' }, // Vertex > Limit
{ vertexSize: 0, fragmentSize: 'exceedLimits', layoutSize: 'auto' }, // Fragment > Limit
] as const;
return u
.combine('isAsync', [true, false])
.combineWithParams([...kNumericCases, ...kMaxLimitsCases] as const);
})
.fn(t => {
t.skipIf(!supportsImmediateData(getGPU(t.rec)), 'Immediate data not supported');

const { isAsync, vertexSize, fragmentSize, layoutSize } = t.params;

assert(t.device.limits.maxImmediateSize !== undefined);
const maxImmediateSize = t.device.limits.maxImmediateSize;

const resolveSize = (sizeDescriptor: number | string) => {
if (typeof sizeDescriptor === 'number') return sizeDescriptor;
if (sizeDescriptor === 'max') return maxImmediateSize;
if (sizeDescriptor === 'exceedLimits') return maxImmediateSize + 4;
return 0;
};

const resolvedVertexImmediateSize = resolveSize(vertexSize);
const resolvedFragmentImmediateSize = resolveSize(fragmentSize);

// Ensure the test's fixed sizes fit within the device limit.
if (vertexSize !== 'exceedLimits') {
assert(
resolvedVertexImmediateSize <= maxImmediateSize,
`vertexSize (${resolvedVertexImmediateSize}) must be <= maxImmediateSize (${maxImmediateSize})`
);
}
if (fragmentSize !== 'exceedLimits') {
assert(
resolvedFragmentImmediateSize <= maxImmediateSize,
`fragmentSize (${resolvedFragmentImmediateSize}) must be <= maxImmediateSize (${maxImmediateSize})`
);
}

// Helper to generate a stage-specific shader module with the given immediate data size.
const makeShaderCode = (size: number, stage: 'vertex' | 'fragment') => {
if (size === 0) {
if (stage === 'vertex') {
return `@vertex fn main_vertex() -> @builtin(position) vec4<f32> { return vec4<f32>(0.0, 0.0, 0.0, 1.0); }`;
}
return `@fragment fn main_fragment() -> @location(0) vec4<f32> { return vec4<f32>(0.0, 1.0, 0.0, 1.0); }`;
}
const numFields = size / 4;
const fields = Array.from({ length: numFields }, (_, i) => `m${i}: u32`).join(', ');
if (stage === 'vertex') {
return `
struct Immediates { ${fields} }
var<immediate> data: Immediates;
@vertex fn main_vertex() -> @builtin(position) vec4<f32> { _ = data.m0; return vec4<f32>(0.0, 0.0, 0.0, 1.0); }
`;
}
return `
struct Immediates { ${fields} }
var<immediate> data: Immediates;
@fragment fn main_fragment() -> @location(0) vec4<f32> { _ = data.m0; return vec4<f32>(0.0, 1.0, 0.0, 1.0); }
`;
};

let layout: GPUPipelineLayout | 'auto';
let validSize: number;

if (layoutSize === 'auto') {
layout = 'auto';
validSize = maxImmediateSize;
} else {
layout = t.device.createPipelineLayout({
bindGroupLayouts: [],
immediateSize: layoutSize as number,
});
validSize = layoutSize as number;
}

const vertexExceedsLimit = resolvedVertexImmediateSize > validSize;
const fragmentExceedsLimit = resolvedFragmentImmediateSize > validSize;
const shouldError = vertexExceedsLimit || fragmentExceedsLimit;

// When the shader exceeds the device's maxImmediateSize, the error occurs
// at shader module creation time, not pipeline creation time.
// Create each shader module separately so the correct one gets the error.
const vertexCode = makeShaderCode(resolvedVertexImmediateSize, 'vertex');
const fragmentCode = makeShaderCode(resolvedFragmentImmediateSize, 'fragment');

if (layoutSize === 'auto' && vertexExceedsLimit) {
t.expectValidationError(() => {
t.device.createShaderModule({ code: vertexCode });
});
}
if (layoutSize === 'auto' && fragmentExceedsLimit) {
t.expectValidationError(() => {
t.device.createShaderModule({ code: fragmentCode });
});
}
if (layoutSize === 'auto' && shouldError) {
return;
}

vtu.doCreateRenderPipelineTest(t, isAsync, !shouldError, {
layout,
vertex: {
module: t.device.createShaderModule({ code: vertexCode }),
},
fragment: {
module: t.device.createShaderModule({ code: fragmentCode }),
targets: [{ format: 'rgba8unorm' }],
},
});
});