blob: a945fdb46af7b11a8b1b333a995a64c928556bbb [file] [log] [blame]
export const description = `
Testing render pipeline using overridable constants in vertex stage and fragment stage.
`;
import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
import { PerTexelComponent } from '../../../util/texture/texel_data.js';
class F extends AllFeaturesMaxLimitsGPUTest {
async expectShaderOutputWithConstants(
isAsync: boolean,
format: GPUTextureFormat,
expected: PerTexelComponent<number>,
vertex: GPUVertexState,
fragment: GPUFragmentState
) {
const renderTarget = this.createTextureTracked({
format,
size: { width: 1, height: 1, depthOrArrayLayers: 1 },
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const descriptor: GPURenderPipelineDescriptor = {
layout: 'auto',
vertex,
fragment,
primitive: {
topology: 'triangle-list',
frontFace: 'ccw',
cullMode: 'back',
},
};
const promise = isAsync
? this.device.createRenderPipelineAsync(descriptor)
: Promise.resolve(this.device.createRenderPipeline(descriptor));
const pipeline = await promise;
const encoder = this.device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: renderTarget.createView(),
clearValue: {
r: kClearValueResult.R,
g: kClearValueResult.G,
b: kClearValueResult.B,
a: kClearValueResult.A,
},
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass.setPipeline(pipeline);
pass.draw(3);
pass.end();
this.device.queue.submit([encoder.finish()]);
this.expectSingleColor(renderTarget, format, {
size: [1, 1, 1],
exp: expected,
});
}
}
export const g = makeTestGroup(F);
const kClearValueResult = { R: 0.2, G: 0.4, B: 0.6, A: 0.8 };
const kDefaultValueResult = { R: 1.0, G: 1.0, B: 1.0, A: 1.0 };
const kFullScreenTriangleVertexShader = `
override xright: f32 = 3.0;
override ytop: f32 = 3.0;
@vertex fn main(
@builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4<f32> {
var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
vec2<f32>(-1.0, ytop),
vec2<f32>(-1.0, -ytop),
vec2<f32>(xright, 0.0));
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}
`;
const kFullScreenTriangleFragmentShader = `
override R: f32 = 1.0;
override G: f32 = 1.0;
override B: f32 = 1.0;
override A: f32 = 1.0;
@fragment fn main()
-> @location(0) vec4<f32> {
return vec4<f32>(R, G, B, A);
}
`;
g.test('basic')
.desc(
`Test that either correct constants override values or default values when no constants override value are provided at pipeline creation time are used correctly in vertex and fragment shader.`
)
.params(u =>
u
.combine('isAsync', [true, false])
.beginSubcases()
.combineWithParams([
{
expected: kDefaultValueResult,
vertexConstants: {},
fragmentConstants: {},
},
{
expected: kClearValueResult,
vertexConstants: {
xright: -3.0,
} as Record<string, GPUPipelineConstantValue>,
fragmentConstants: {},
},
{
expected: kClearValueResult,
vertexConstants: {
ytop: -3.0,
} as Record<string, GPUPipelineConstantValue>,
fragmentConstants: {},
},
{
expected: kDefaultValueResult,
vertexConstants: {
xright: 4.0,
ytop: 4.0,
} as Record<string, GPUPipelineConstantValue>,
fragmentConstants: {},
},
{
expected: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 },
vertexConstants: {},
fragmentConstants: { R: 0.0, B: 0.0 } as Record<string, GPUPipelineConstantValue>,
},
{
expected: { R: 0.0, G: 0.0, B: 0.0, A: 0.0 },
vertexConstants: {},
fragmentConstants: { R: 0.0, G: 0.0, B: 0.0, A: 0.0 } as Record<
string,
GPUPipelineConstantValue
>,
},
])
)
.fn(async t => {
const format = 'bgra8unorm';
await t.expectShaderOutputWithConstants(
t.params.isAsync,
format,
t.params.expected,
{
module: t.device.createShaderModule({
code: kFullScreenTriangleVertexShader,
}),
entryPoint: 'main',
constants: t.params.vertexConstants,
},
{
module: t.device.createShaderModule({
code: kFullScreenTriangleFragmentShader,
}),
entryPoint: 'main',
constants: t.params.fragmentConstants,
targets: [{ format }],
}
);
});
const kPrecisionTestFormat = 'rgba32float';
g.test('precision')
.desc(`Test that the float number precision is preserved for constants`)
.params(u =>
u
.combine('isAsync', [true, false])
.beginSubcases()
.combineWithParams([
{
expected: { R: 3.14159, G: 1.0, B: 1.0, A: 1.0 },
vertexConstants: {},
fragmentConstants: { R: 3.14159 } as Record<string, GPUPipelineConstantValue>,
},
{
expected: { R: 3.141592653589793, G: 1.0, B: 1.0, A: 1.0 },
vertexConstants: {},
fragmentConstants: { R: 3.141592653589793 } as Record<string, GPUPipelineConstantValue>,
},
])
)
.fn(async t => {
const format = kPrecisionTestFormat;
await t.expectShaderOutputWithConstants(
t.params.isAsync,
format,
t.params.expected,
{
module: t.device.createShaderModule({
code: kFullScreenTriangleVertexShader,
}),
entryPoint: 'main',
constants: t.params.vertexConstants,
},
{
module: t.device.createShaderModule({
code: kFullScreenTriangleFragmentShader,
}),
entryPoint: 'main',
constants: t.params.fragmentConstants,
targets: [{ format }],
}
);
});
g.test('shared_shader_module')
.desc(
`Test that when the same module is shared by different pipelines, the constant values are still being used correctly.`
)
.params(u =>
u
.combine('isAsync', [true, false])
.beginSubcases()
.combineWithParams([
{
expected0: kClearValueResult,
vertexConstants0: {
xright: -3.0,
} as Record<string, GPUPipelineConstantValue>,
fragmentConstants0: {},
expected1: kDefaultValueResult,
vertexConstants1: {},
fragmentConstants1: {},
},
{
expected0: { R: 0.0, G: 0.0, B: 0.0, A: 0.0 },
vertexConstants0: {},
fragmentConstants0: { R: 0.0, G: 0.0, B: 0.0, A: 0.0 } as Record<
string,
GPUPipelineConstantValue
>,
expected1: kDefaultValueResult,
vertexConstants1: {},
fragmentConstants1: {},
},
{
expected0: { R: 1.0, G: 0.0, B: 1.0, A: 0.0 },
vertexConstants0: {},
fragmentConstants0: { R: 1.0, G: 0.0, B: 1.0, A: 0.0 } as Record<
string,
GPUPipelineConstantValue
>,
expected1: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 },
vertexConstants1: {},
fragmentConstants1: { R: 0.0, G: 1.0, B: 0.0, A: 1.0 } as Record<
string,
GPUPipelineConstantValue
>,
},
])
)
.fn(async t => {
const format = 'bgra8unorm';
const vertexModule = t.device.createShaderModule({
code: kFullScreenTriangleVertexShader,
});
const fragmentModule = t.device.createShaderModule({
code: kFullScreenTriangleFragmentShader,
});
const createPipelineFn = async (
vertexConstants: Record<string, GPUPipelineConstantValue>,
fragmentConstants: Record<string, GPUPipelineConstantValue>
) => {
const descriptor: GPURenderPipelineDescriptor = {
layout: 'auto',
vertex: {
module: vertexModule,
entryPoint: 'main',
constants: vertexConstants,
},
fragment: {
module: fragmentModule,
entryPoint: 'main',
targets: [{ format }],
constants: fragmentConstants,
},
primitive: {
topology: 'triangle-list',
frontFace: 'ccw',
cullMode: 'back',
},
};
return t.params.isAsync
? t.device.createRenderPipelineAsync(descriptor)
: t.device.createRenderPipeline(descriptor);
};
const pipeline0 = await createPipelineFn(
t.params.vertexConstants0,
t.params.fragmentConstants0
);
const pipeline1 = await createPipelineFn(
t.params.vertexConstants1,
t.params.fragmentConstants1
);
const renderTarget0 = t.createTextureTracked({
format,
size: { width: 1, height: 1, depthOrArrayLayers: 1 },
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const renderTarget1 = t.createTextureTracked({
format,
size: { width: 1, height: 1, depthOrArrayLayers: 1 },
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
});
const encoder = t.device.createCommandEncoder();
const pass0 = encoder.beginRenderPass({
colorAttachments: [
{
view: renderTarget0.createView(),
clearValue: {
r: kClearValueResult.R,
g: kClearValueResult.G,
b: kClearValueResult.B,
a: kClearValueResult.A,
},
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass0.setPipeline(pipeline0);
pass0.draw(3);
pass0.end();
const pass1 = encoder.beginRenderPass({
colorAttachments: [
{
view: renderTarget1.createView(),
clearValue: {
r: kClearValueResult.R,
g: kClearValueResult.G,
b: kClearValueResult.B,
a: kClearValueResult.A,
},
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass1.setPipeline(pipeline1);
pass1.draw(3);
pass1.end();
t.device.queue.submit([encoder.finish()]);
t.expectSingleColor(renderTarget0, format, {
size: [1, 1, 1],
exp: t.params.expected0,
});
t.expectSingleColor(renderTarget1, format, {
size: [1, 1, 1],
exp: t.params.expected1,
});
});
g.test('multi_entry_points')
.desc(
`Test that when the same module is shared by vertex and fragment shader, the constant values are still being used correctly.`
)
.params(u =>
u
.combine('isAsync', [true, false])
.beginSubcases()
.combineWithParams([
{
expected: { R: 0.8, G: 0.4, B: 0.2, A: 1.0 },
vertexConstants: { A: 4.0, B: 4.0 } as Record<string, GPUPipelineConstantValue>,
fragmentConstants: { A: 0.8, B: 0.4, C: 0.2, D: 1.0 } as Record<
string,
GPUPipelineConstantValue
>,
},
{
expected: { R: 0.8, G: 0.4, B: 0.2, A: 1.0 },
vertexConstants: {},
fragmentConstants: { A: 0.8, B: 0.4, C: 0.2, D: 1.0 } as Record<
string,
GPUPipelineConstantValue
>,
},
{
expected: kClearValueResult,
vertexConstants: { A: -3.0 },
fragmentConstants: { A: 0.8, B: 0.4, C: 0.2, D: 1.0 } as Record<
string,
GPUPipelineConstantValue
>,
},
])
)
.fn(async t => {
const format = 'bgra8unorm';
const module = t.device.createShaderModule({
code: `
override A: f32 = 3.0;
override B: f32 = 3.0;
override C: f32;
override D: f32;
@vertex fn vertexMain(
@builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4<f32> {
var pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
vec2<f32>(-1.0, A),
vec2<f32>(-1.0, -A),
vec2<f32>(B, 0.0));
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}
@fragment fn fragmentMain()
-> @location(0) vec4<f32> {
return vec4<f32>(A, B, C, D);
}
`,
});
await t.expectShaderOutputWithConstants(
t.params.isAsync,
format,
t.params.expected,
{
module,
entryPoint: 'vertexMain',
constants: t.params.vertexConstants,
},
{
module,
entryPoint: 'fragmentMain',
constants: t.params.fragmentConstants,
targets: [{ format }],
}
);
});