-
Notifications
You must be signed in to change notification settings - Fork 100
Validation: Add immediate data pipeline validation tests #4547
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
| const maxImmediateSize = t.device.limits.maxImmediateSize ?? 64; | ||
|
|
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test uses a fallback value of 64 for maxImmediateSize when it's undefined, but this might not match the actual spec default. The test should either skip when maxImmediateSize is not defined (similar to how the init method checks for support), or use the actual spec-defined default value if there is one.
| const maxImmediateSize = t.device.limits.maxImmediateSize ?? 64; | |
| const maxImmediateSize = t.device.limits.maxImmediateSize; | |
| if (maxImmediateSize === undefined) { | |
| t.skip('maxImmediateSize limit is not supported on this device.'); | |
| return; | |
| } |
| g.test('immediate_data_size') | ||
| .desc( | ||
| ` | ||
| Test that creating a pipeline layout with immediateSize validates: | ||
| - immediateSize must be a multiple of 4. | ||
| - immediateSize must be <= device.limits.maxImmediateSize. | ||
| ` | ||
| ) | ||
| .fn(t => { | ||
| const maxImmediateSize = t.device.limits.maxImmediateSize ?? 64; | ||
|
|
||
| const kTestSizes = [0, 4, maxImmediateSize, maxImmediateSize + 4, 1, 2, 3, 5]; | ||
|
|
||
| for (const size of kTestSizes) { | ||
| const descriptor = { | ||
| bindGroupLayouts: [], | ||
| immediateSize: size, | ||
| }; | ||
|
|
||
| const isMultipleOf4 = size % 4 === 0; | ||
| const isWithinLimit = size <= maxImmediateSize; | ||
| const shouldSucceed = isMultipleOf4 && isWithinLimit; | ||
|
|
||
| t.expectValidationError(() => { | ||
| t.device.createPipelineLayout(descriptor); | ||
| }, !shouldSucceed); | ||
| } | ||
| }); |
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test validates immediateSize constraints but doesn't test edge cases like negative values, NaN, or Infinity. While these may be caught by TypeScript, runtime validation should still be tested for robustness, especially since JavaScript allows these values.
| case 'struct_padding': | ||
| kImmediateSize = 64; | ||
| code = ` | ||
| struct A { v: vec3<u32>, } | ||
| struct B { v: vec2<u32>, } | ||
| struct Data { a: A, @align(32) b: B, } | ||
| var<immediate> data: Data; | ||
| @compute @workgroup_size(1) fn main_compute() { _ = data.b.v; } | ||
| @vertex fn main_vertex() -> @builtin(position) vec4<f32> { _ = data.b.v; return vec4<f32>(0.0, 0.0, 0.0, 1.0); } | ||
| @fragment fn main_fragment() -> @location(0) vec4<f32> { _ = data.b.v; return vec4<f32>(0.0, 1.0, 0.0, 1.0); } | ||
| `; |
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the struct_padding scenario, the test accesses only 'data.b.v' in the shader but still requires setting bytes for 'data.a' as well (lines 212-213). The test description should clarify that even though only 'data.b.v' is accessed, all non-padding bytes of the struct must be set because the entire struct variable is considered "statically used" once any part of it is accessed. This is an important validation rule that deserves explicit documentation in the test description.
| `; | ||
|
|
||
| import { makeTestGroup } from '../../../../../common/framework/test_group.js'; | ||
| import { hasFeature } from '../../../../../common/util/util.js'; |
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import of hasFeature is unused since the WGSL feature check should be done using getGPU(this.rec).wgslLanguageFeatures.has() instead. Consider removing this import and adding import { getGPU } from '../../../../../common/util/navigator_gpu.js'; to match the pattern in setImmediates.spec.ts.
| case 'functions': | ||
| kImmediateSize = 16; | ||
| code = ` | ||
| var<immediate> data: vec4<u32>; | ||
| fn use_data() { _ = data; } | ||
| @compute @workgroup_size(1) fn main_compute() { use_data(); } | ||
| @vertex fn main_vertex() -> @builtin(position) vec4<f32> { use_data(); return vec4<f32>(0.0, 0.0, 0.0, 1.0); } | ||
| @fragment fn main_fragment() -> @location(0) vec4<f32> { use_data(); return vec4<f32>(0.0, 1.0, 0.0, 1.0); } | ||
| `; | ||
| break; |
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test checks that immediate data used in a function called by the entry point requires slots to be set. However, it doesn't test the negative case where the function is declared but never called. Consider adding test coverage for a scenario where an immediate variable is used in a function that is never called by any entry point, which should not require slots to be set.
| g.test('required_slots_set') | ||
| .desc( | ||
| ` | ||
| Validate that all immediate data slots required by the pipeline are set on the encoder. | ||
| - For each immediate data variable statically used by the pipeline: | ||
| - All accessible slots (4-byte words) must be set via setImmediates. | ||
| - Scenarios: | ||
| - scalar: Simple u32 usage. | ||
| - vector: Simple vec4<u32> usage. | ||
| - struct_padding: Struct with padding. Padding bytes do not need to be set. | ||
| - functions: Immediate data used in a function called by entry point. | ||
| - dynamic_indexing: Array with dynamic indexing. | ||
| - Usage: | ||
| - full: Set all declared bytes. | ||
| - split: Set all declared bytes in multiple calls. | ||
| - partial: Set only a subset of bytes. | ||
| - For struct_padding, this means setting only members (no padding), which is valid. | ||
| - For others, this means missing required data, which is invalid. | ||
| ` | ||
| ) | ||
| .params(u => | ||
| u | ||
| .combine('encoderType', kProgrammableEncoderTypes) | ||
| .combine('scenario', [ | ||
| 'scalar', | ||
| 'vector', | ||
| 'struct_padding', | ||
| 'functions', | ||
| 'dynamic_indexing', | ||
| ] as const) | ||
| .combine('usage', ['full', 'partial', 'split'] as const) | ||
| .filter(t => { | ||
| if (t.scenario === 'scalar' && t.usage === 'split') return false; | ||
| return true; | ||
| }) | ||
| ) | ||
| .fn(t => { | ||
| const { encoderType, scenario, usage } = t.params; | ||
|
|
||
| let code = ''; | ||
| let kImmediateSize = 0; | ||
|
|
||
| switch (scenario) { | ||
| case 'scalar': | ||
| kImmediateSize = 4; | ||
| code = ` | ||
| var<immediate> data: u32; | ||
| @compute @workgroup_size(1) fn main_compute() { _ = data; } | ||
| @vertex fn main_vertex() -> @builtin(position) vec4<f32> { _ = data; return vec4<f32>(0.0, 0.0, 0.0, 1.0); } | ||
| @fragment fn main_fragment() -> @location(0) vec4<f32> { _ = data; return vec4<f32>(0.0, 1.0, 0.0, 1.0); } | ||
| `; | ||
| break; | ||
| case 'vector': | ||
| kImmediateSize = 16; | ||
| code = ` | ||
| var<immediate> data: vec4<u32>; | ||
| @compute @workgroup_size(1) fn main_compute() { _ = data; } | ||
| @vertex fn main_vertex() -> @builtin(position) vec4<f32> { _ = data; return vec4<f32>(0.0, 0.0, 0.0, 1.0); } | ||
| @fragment fn main_fragment() -> @location(0) vec4<f32> { _ = data; return vec4<f32>(0.0, 1.0, 0.0, 1.0); } | ||
| `; | ||
| break; | ||
| case 'struct_padding': | ||
| kImmediateSize = 64; | ||
| code = ` | ||
| struct A { v: vec3<u32>, } | ||
| struct B { v: vec2<u32>, } | ||
| struct Data { a: A, @align(32) b: B, } | ||
| var<immediate> data: Data; | ||
| @compute @workgroup_size(1) fn main_compute() { _ = data.b.v; } | ||
| @vertex fn main_vertex() -> @builtin(position) vec4<f32> { _ = data.b.v; return vec4<f32>(0.0, 0.0, 0.0, 1.0); } | ||
| @fragment fn main_fragment() -> @location(0) vec4<f32> { _ = data.b.v; return vec4<f32>(0.0, 1.0, 0.0, 1.0); } | ||
| `; | ||
| break; | ||
| case 'functions': | ||
| kImmediateSize = 16; | ||
| code = ` | ||
| var<immediate> data: vec4<u32>; | ||
| fn use_data() { _ = data; } | ||
| @compute @workgroup_size(1) fn main_compute() { use_data(); } | ||
| @vertex fn main_vertex() -> @builtin(position) vec4<f32> { use_data(); return vec4<f32>(0.0, 0.0, 0.0, 1.0); } | ||
| @fragment fn main_fragment() -> @location(0) vec4<f32> { use_data(); return vec4<f32>(0.0, 1.0, 0.0, 1.0); } | ||
| `; | ||
| break; | ||
| case 'dynamic_indexing': | ||
| kImmediateSize = 16; | ||
| code = ` | ||
| var<immediate> data: array<u32, 4>; | ||
| @compute @workgroup_size(1) fn main_compute(@builtin(local_invocation_index) i: u32) { _ = data[i]; } | ||
| @vertex fn main_vertex(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> { _ = data[i]; return vec4<f32>(0.0, 0.0, 0.0, 1.0); } | ||
| @fragment fn main_fragment(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> { | ||
| let i = u32(pos.x); | ||
| _ = data[i]; | ||
| return vec4<f32>(0.0, 1.0, 0.0, 1.0); | ||
| } | ||
| `; | ||
| break; | ||
| } | ||
|
|
||
| const pipeline = t.createPipeline(encoderType, code, kImmediateSize); | ||
| const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType); | ||
|
|
||
| const setImmediates = (offset: number, size: number) => { | ||
| const data = new Uint8Array(size); | ||
| encoder.setImmediates(offset, data, 0, size); | ||
| }; | ||
|
|
||
| if (scenario === 'struct_padding') { | ||
| // Size 64. A: 0-12. Padding: 12-32. B: 32-40. Padding: 40-64. | ||
| if (usage === 'full') { | ||
| setImmediates(0, 64); | ||
| } else if (usage === 'split') { | ||
| setImmediates(0, 32); | ||
| setImmediates(32, 32); | ||
| } else if (usage === 'partial') { | ||
| // Set members only | ||
| setImmediates(0, 12); | ||
| setImmediates(32, 8); | ||
| } | ||
| } else if (scenario === 'scalar') { | ||
| // Size 4. | ||
| if (usage === 'full') { | ||
| setImmediates(0, 4); | ||
| } else if (usage === 'partial') { | ||
| // Set nothing | ||
| } | ||
| } else { | ||
| // Vector (size 16), functions (size 16), dynamic_indexing (size 16) | ||
| if (usage === 'full') { | ||
| setImmediates(0, 16); | ||
| } else if (usage === 'split') { | ||
| setImmediates(0, 8); | ||
| setImmediates(8, 8); | ||
| } else if (usage === 'partial') { | ||
| setImmediates(0, 8); // Missing last 8 bytes | ||
| } | ||
| } | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| t.runPass(encoderType, encoder as any, pipeline); | ||
|
|
||
| const shouldSucceed = | ||
| usage === 'full' || | ||
| usage === 'split' || | ||
| (scenario === 'struct_padding' && usage === 'partial'); | ||
| validateFinishAndSubmit(shouldSucceed, true); | ||
| }); |
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test validates that all immediate data slots required by the pipeline must be set via setImmediates. However, it doesn't test the case where more data than required is set (e.g., setting 32 bytes when only 16 are required by the shader). This should be valid and could be worth testing to ensure overprovision doesn't cause issues.
| g.test('render_bundle_execution_state_invalidation') | ||
| .desc( | ||
| ` | ||
| Validate that executeBundles invalidates the current pipeline and immediate data state | ||
| in the RenderPassEncoder. | ||
| - Pipeline must be re-set after executeBundles. | ||
| - Immediate data must be re-set after executeBundles. | ||
| - setImmediates in bundle does not leak to pass. | ||
| ` | ||
| ) | ||
| .params(u => | ||
| u.combine('check', [ | ||
| 'pipeline_invalidated', | ||
| 'immediates_invalidated', | ||
| 'bundle_no_leak', | ||
| ] as const) | ||
| ) | ||
| .fn(t => { | ||
| const { check } = t.params; | ||
| const kImmediateSize = 16; | ||
|
|
||
| // Create a pipeline requiring immediate data | ||
| const code = ` | ||
| var<immediate> data: vec4<u32>; | ||
| @vertex fn main_vertex() -> @builtin(position) vec4<f32> { _ = data; return vec4<f32>(0.0, 0.0, 0.0, 1.0); } | ||
| @fragment fn main_fragment() -> @location(0) vec4<f32> { _ = data; return vec4<f32>(0.0, 1.0, 0.0, 1.0); } | ||
| `; | ||
| const pipeline = t.createPipeline('render pass', code, kImmediateSize) as GPURenderPipeline; | ||
|
|
||
| // Create an empty bundle (or one that sets immediates for the leak test) | ||
| const bundleEncoder = t.device.createRenderBundleEncoder({ | ||
| colorFormats: ['rgba8unorm'], | ||
| }); | ||
| if (check === 'bundle_no_leak') { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (bundleEncoder as any).setPipeline(pipeline); | ||
| const d = new Uint8Array(16); | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (bundleEncoder as any).setImmediates(0, d, 0, 16); | ||
| bundleEncoder.draw(3); | ||
| } | ||
| const bundle = bundleEncoder.finish(); | ||
|
|
||
| const { encoder, validateFinishAndSubmit } = t.createEncoder('render pass'); | ||
| const pass = encoder; | ||
|
|
||
| // Initial setup | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (pass as any).setPipeline(pipeline); | ||
| const d = new Uint8Array(16); | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (pass as any).setImmediates(0, d, 0, 16); | ||
|
|
||
| // Execute bundle - this should invalidate state | ||
| pass.executeBundles([bundle]); | ||
|
|
||
| // Try to draw | ||
| if (check === 'pipeline_invalidated') { | ||
| // Don't re-set pipeline. Should fail. | ||
| pass.draw(3); | ||
| validateFinishAndSubmit(false, true); | ||
| } else if (check === 'immediates_invalidated') { | ||
| // Re-set pipeline, but not immediates. Should fail. | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (pass as any).setPipeline(pipeline); | ||
| pass.draw(3); | ||
| validateFinishAndSubmit(false, true); | ||
| } else if (check === 'bundle_no_leak') { | ||
| // Re-set pipeline. Bundle had setImmediates, but it shouldn't leak. | ||
| // We didn't re-set immediates in the pass. Should fail. | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (pass as any).setPipeline(pipeline); | ||
| pass.draw(3); | ||
| validateFinishAndSubmit(false, true); | ||
| } | ||
| }); |
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test validates that executeBundles invalidates pipeline and immediate data state, but it doesn't test the positive control case where the pipeline and immediates are properly re-set after executeBundles, which should succeed. Consider adding a test case where both pipeline and immediates are re-set after executeBundles to validate that the state can be correctly restored.
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| t.runPass(encoderType, encoder as any, pipeline); | ||
|
|
||
| const shouldSucceed = | ||
| usage === 'full' || | ||
| usage === 'split' || | ||
| (scenario === 'struct_padding' && usage === 'partial'); | ||
| validateFinishAndSubmit(shouldSucceed, true); | ||
| }); | ||
|
|
||
| g.test('unused_variable') | ||
| .desc( | ||
| ` | ||
| Validate that if an immediate data variable is declared but not statically used, | ||
| it does not require slots to be set. | ||
| ` | ||
| ) | ||
| .params(u => | ||
| u | ||
| .combine('encoderType', kProgrammableEncoderTypes) | ||
| .combine('usage', ['none', 'full', 'partial_start'] as const) | ||
| ) | ||
| .fn(t => { | ||
| const { encoderType, usage } = t.params; | ||
| const kImmediateSize = 16; | ||
|
|
||
| const code = ` | ||
| var<immediate> data: vec4<u32>; | ||
| @compute @workgroup_size(1) fn main_compute() { | ||
| // data is not used | ||
| } | ||
| @vertex fn main_vertex() -> @builtin(position) vec4<f32> { | ||
| return vec4<f32>(0.0, 0.0, 0.0, 1.0); | ||
| } | ||
| @fragment fn main_fragment() -> @location(0) vec4<f32> { | ||
| return vec4<f32>(0.0, 1.0, 0.0, 1.0); | ||
| } | ||
| `; | ||
|
|
||
| const pipeline = t.createPipeline(encoderType, code, kImmediateSize); | ||
|
|
||
| const { encoder, validateFinishAndSubmit } = t.createEncoder(encoderType); | ||
|
|
||
| if (usage === 'full') { | ||
| const data = new Uint8Array(16); | ||
| encoder.setImmediates(0, data, 0, 16); | ||
| } else if (usage === 'partial_start') { | ||
| const data = new Uint8Array(8); | ||
| encoder.setImmediates(0, data, 0, 8); | ||
| } | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| t.runPass(encoderType, encoder as any, pipeline); | ||
|
|
||
| validateFinishAndSubmit(true, true); | ||
| }); | ||
|
|
||
| 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. | ||
| ` | ||
| ) | ||
| .params(u => u.combine('encoderType', kProgrammableEncoderTypes)) | ||
| .fn(t => { | ||
| const { encoderType } = t.params; | ||
| const kLayoutSize = 16; | ||
| const kShaderSize = 32; // Larger than layout | ||
|
|
||
| const layout = t.device.createPipelineLayout({ | ||
| bindGroupLayouts: [], | ||
| immediateSize: kLayoutSize, | ||
| }); | ||
|
|
||
| const code = ` | ||
| var<immediate> data: array<u32, ${kShaderSize / 4}>; | ||
| @compute @workgroup_size(1) fn main_compute() { _ = data[0]; } | ||
| @vertex fn main_vertex() -> @builtin(position) vec4<f32> { _ = data[0]; return vec4<f32>(0.0, 0.0, 0.0, 1.0); } | ||
| @fragment fn main_fragment() -> @location(0) vec4<f32> { _ = data[0]; return vec4<f32>(0.0, 1.0, 0.0, 1.0); } | ||
| `; | ||
|
|
||
| t.expectValidationError(() => { | ||
| if (encoderType === 'compute pass') { | ||
| t.device.createComputePipeline({ | ||
| layout, | ||
| compute: { | ||
| module: t.device.createShaderModule({ code }), | ||
| entryPoint: 'main_compute', | ||
| }, | ||
| }); | ||
| } else { | ||
| t.device.createRenderPipeline({ | ||
| layout, | ||
| vertex: { | ||
| module: t.device.createShaderModule({ code }), | ||
| entryPoint: 'main_vertex', | ||
| }, | ||
| fragment: { | ||
| module: t.device.createShaderModule({ code }), | ||
| entryPoint: 'main_fragment', | ||
| targets: [{ format: 'rgba8unorm' }], | ||
| }, | ||
| }); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| g.test('render_bundle_execution_state_invalidation') | ||
| .desc( | ||
| ` | ||
| Validate that executeBundles invalidates the current pipeline and immediate data state | ||
| in the RenderPassEncoder. | ||
| - Pipeline must be re-set after executeBundles. | ||
| - Immediate data must be re-set after executeBundles. | ||
| - setImmediates in bundle does not leak to pass. | ||
| ` | ||
| ) | ||
| .params(u => | ||
| u.combine('check', [ | ||
| 'pipeline_invalidated', | ||
| 'immediates_invalidated', | ||
| 'bundle_no_leak', | ||
| ] as const) | ||
| ) | ||
| .fn(t => { | ||
| const { check } = t.params; | ||
| const kImmediateSize = 16; | ||
|
|
||
| // Create a pipeline requiring immediate data | ||
| const code = ` | ||
| var<immediate> data: vec4<u32>; | ||
| @vertex fn main_vertex() -> @builtin(position) vec4<f32> { _ = data; return vec4<f32>(0.0, 0.0, 0.0, 1.0); } | ||
| @fragment fn main_fragment() -> @location(0) vec4<f32> { _ = data; return vec4<f32>(0.0, 1.0, 0.0, 1.0); } | ||
| `; | ||
| const pipeline = t.createPipeline('render pass', code, kImmediateSize) as GPURenderPipeline; | ||
|
|
||
| // Create an empty bundle (or one that sets immediates for the leak test) | ||
| const bundleEncoder = t.device.createRenderBundleEncoder({ | ||
| colorFormats: ['rgba8unorm'], | ||
| }); | ||
| if (check === 'bundle_no_leak') { | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (bundleEncoder as any).setPipeline(pipeline); | ||
| const d = new Uint8Array(16); | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (bundleEncoder as any).setImmediates(0, d, 0, 16); | ||
| bundleEncoder.draw(3); | ||
| } | ||
| const bundle = bundleEncoder.finish(); | ||
|
|
||
| const { encoder, validateFinishAndSubmit } = t.createEncoder('render pass'); | ||
| const pass = encoder; | ||
|
|
||
| // Initial setup | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (pass as any).setPipeline(pipeline); | ||
| const d = new Uint8Array(16); | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (pass as any).setImmediates(0, d, 0, 16); | ||
|
|
||
| // Execute bundle - this should invalidate state | ||
| pass.executeBundles([bundle]); | ||
|
|
||
| // Try to draw | ||
| if (check === 'pipeline_invalidated') { | ||
| // Don't re-set pipeline. Should fail. | ||
| pass.draw(3); | ||
| validateFinishAndSubmit(false, true); | ||
| } else if (check === 'immediates_invalidated') { | ||
| // Re-set pipeline, but not immediates. Should fail. | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (pass as any).setPipeline(pipeline); | ||
| pass.draw(3); | ||
| validateFinishAndSubmit(false, true); | ||
| } else if (check === 'bundle_no_leak') { | ||
| // Re-set pipeline. Bundle had setImmediates, but it shouldn't leak. | ||
| // We didn't re-set immediates in the pass. Should fail. | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| (pass as any).setPipeline(pipeline); |
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test repeatedly casts encoder to 'any' to call setImmediates and setPipeline methods (lines 235, 289, 380, 383, 393, 396, 409, 416). While this is necessary due to the experimental nature of the API, the pattern could be improved by creating a helper type or using a consistent pattern. Consider defining a type alias at the file level to reduce repetition and improve maintainability.
| 'setImmediates' in (GPURenderBundleEncoder.prototype as any); | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const supportsLimit = (this.device.limits as any).maxImmediateSize !== undefined; | ||
| const supportsWgslFeature = hasFeature(this.device.features, 'immediate-address-space'); |
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The check for WGSL feature support should use getGPU(this.rec).wgslLanguageFeatures.has('immediate_address_space') instead of hasFeature(this.device.features, 'immediate-address-space'). This is the pattern used in setImmediates.spec.ts and correctly checks WGSL language features rather than device features. Note that the feature name is 'immediate_address_space' with an underscore.
Adds `pipeline_immediate.spec.ts` to validate immediate data usage in RenderPassEncoder, ComputePassEncoder, and RenderBundleEncoder. Tests cover: - Required immediate data slots are set. - Unused variables do not require slots. - Pipeline creation fails if shader immediate size exceeds layout limit. - RenderBundle execution invalidates pipeline and immediate data state.
588108a to
a15fb20
Compare
Adds
pipeline_immediate.spec.tsto validate immediate data usage in RenderPassEncoder, ComputePassEncoder, and RenderBundleEncoder.Tests cover:
Issue: #
Requirements for PR author:
.unimplemented()./** documented */and new helper files are found inhelper_index.txt.Requirements for reviewer sign-off:
When landing this PR, be sure to make any necessary issue status updates.