blob: 602440f6074070b9fa7945ab38bbe9acc8eb4606 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui';
import 'package:litetest/litetest.dart';
import 'package:path/path.dart' as path;
import 'shader_test_file_utils.dart';
void main() {
test('throws exception for invalid shader', () async {
final ByteBuffer invalidBytes = Uint8List.fromList(
<int>[1, 2, 3, 4, 5],
).buffer;
try {
await FragmentProgram.compile(spirv: invalidBytes);
fail('expected compile to throw an exception');
} catch (_) {
}
});
test('simple shader renders correctly', () async {
final Uint8List shaderBytes = await shaderFile(
path.join('general_shaders', 'spirv'),
'functions.frag.spirv',
).readAsBytes();
final FragmentProgram program = await FragmentProgram.compile(
spirv: shaderBytes.buffer,
);
final Shader shader = program.shader(
floatUniforms: Float32List.fromList(<double>[1]),
);
_expectShaderRendersGreen(shader);
});
test('simple sksl shader renders correctly', () async {
final Uint8List shaderBytes = await shaderFile(
path.join('general_shaders', 'sksl'),
'functions.frag.sksl',
).readAsBytes();
final FragmentProgram program = await FragmentProgram.compile(
raw: shaderBytes.buffer,
uniformFloatCount: 1,
);
final Shader shader = program.shader(
floatUniforms: Float32List.fromList(<double>[1]),
);
_expectShaderRendersGreen(shader);
});
test('shader with functions renders green', () async {
final ByteBuffer spirv = shaderFile(
path.join('general_shaders', 'spirv'),
'functions.frag.spirv',
).readAsBytesSync().buffer;
final FragmentProgram program = await FragmentProgram.compile(
spirv: spirv,
);
final Shader shader = program.shader(
floatUniforms: Float32List.fromList(<double>[1]),
);
_expectShaderRendersGreen(shader);
});
test('sksl shader with functions renders green', () async {
final ByteBuffer sksl = shaderFile(
path.join('general_shaders', 'sksl'),
'functions.frag.sksl',
).readAsBytesSync().buffer;
final FragmentProgram program = await FragmentProgram.compile(
raw: sksl,
uniformFloatCount: 1,
);
final Shader shader = program.shader(
floatUniforms: Float32List.fromList(<double>[1]),
);
_expectShaderRendersGreen(shader);
});
test('blue-green image renders green', () async {
final ByteBuffer spirv = shaderFile(
path.join('general_shaders', 'spirv'),
'blue_green_sampler.frag.spirv',
).readAsBytesSync().buffer;
final FragmentProgram program = await FragmentProgram.compile(
debugPrint: true,
spirv: spirv,
);
final Image blueGreenImage = await _createBlueGreenImage();
final ImageShader imageShader = ImageShader(
blueGreenImage, TileMode.clamp, TileMode.clamp, _identityMatrix);
final Shader shader = program.shader(
floatUniforms: Float32List.fromList(<double>[]),
samplerUniforms: <ImageShader>[imageShader],
);
await _expectShaderRendersGreen(shader);
});
test('sksl blue-green image renders green', () async {
final ByteBuffer sksl = shaderFile(
path.join('general_shaders', 'sksl'),
'blue_green_sampler.frag.sksl',
).readAsBytesSync().buffer;
final FragmentProgram program = await FragmentProgram.compile(
debugPrint: true,
raw: sksl,
uniformFloatCount: 0,
samplerCount: 1,
);
final Image blueGreenImage = await _createBlueGreenImage();
final ImageShader imageShader = ImageShader(
blueGreenImage, TileMode.clamp, TileMode.clamp, _identityMatrix);
final Shader shader = program.shader(
floatUniforms: Float32List.fromList(<double>[]),
samplerUniforms: <ImageShader>[imageShader],
);
await _expectShaderRendersGreen(shader);
});
test('shader with uniforms renders correctly', () async {
final Uint8List shaderBytes = await shaderFile(
path.join('general_shaders', 'spirv'),
'uniforms.frag.spirv',
).readAsBytes();
final FragmentProgram program = await FragmentProgram.compile(
spirv: shaderBytes.buffer,
);
final Shader shader = program.shader(
floatUniforms: Float32List.fromList(<double>[
0.0, // iFloatUniform
0.25, // iVec2Uniform.x
0.75, // iVec2Uniform.y
0.0, // iMat2Uniform[0][0]
0.0, // iMat2Uniform[0][1]
0.0, // iMat2Uniform[1][0]
1.0, // iMat2Uniform[1][1]
]));
final ByteData renderedBytes = (await _imageByteDataFromShader(
shader: shader,
))!;
expect(toFloat(renderedBytes.getUint8(0)), closeTo(0.0, epsilon));
expect(toFloat(renderedBytes.getUint8(1)), closeTo(0.25, epsilon));
expect(toFloat(renderedBytes.getUint8(2)), closeTo(0.75, epsilon));
expect(toFloat(renderedBytes.getUint8(3)), closeTo(1.0, epsilon));
});
test('sksl shader with uniforms renders correctly', () async {
final Uint8List shaderBytes = await shaderFile(
path.join('general_shaders', 'sksl'),
'uniforms.frag.sksl',
).readAsBytes();
final FragmentProgram program = await FragmentProgram.compile(
raw: shaderBytes.buffer,
uniformFloatCount: 7,
);
final Shader shader = program.shader(
floatUniforms: Float32List.fromList(<double>[
0.0, // iFloatUniform
0.25, // iVec2Uniform.x
0.75, // iVec2Uniform.y
0.0, // iMat2Uniform[0][0]
0.0, // iMat2Uniform[0][1]
0.0, // iMat2Uniform[1][0]
1.0, // iMat2Uniform[1][1]
]));
final ByteData renderedBytes = (await _imageByteDataFromShader(
shader: shader,
))!;
expect(toFloat(renderedBytes.getUint8(0)), closeTo(0.0, epsilon));
expect(toFloat(renderedBytes.getUint8(1)), closeTo(0.25, epsilon));
expect(toFloat(renderedBytes.getUint8(2)), closeTo(0.75, epsilon));
expect(toFloat(renderedBytes.getUint8(3)), closeTo(1.0, epsilon));
});
test('spirv for the ink_sparkle shader is accepted', () async {
final Uint8List spirv = await shaderFile(
path.join('spirv'),
'ink_sparkle.frag.spirv',
).readAsBytes();
final FragmentProgram program = await FragmentProgram.compile(
spirv: spirv.buffer,
);
final Shader shader = program.shader(
floatUniforms: Float32List(32),
);
await _imageByteDataFromShader(shader: shader);
// Testing that no exceptions are thrown. Tests that the ink_sparkle shader
// produces the correct pixels are in the framework.
});
test('sksl for the ink_sparkle shader is accepted', () async {
final Uint8List sksl = await shaderFile(
path.join('sksl'),
'ink_sparkle.frag.sksl',
).readAsBytes();
final FragmentProgram program = await FragmentProgram.compile(
raw: sksl.buffer,
uniformFloatCount: 32,
);
final Shader shader = program.shader(
floatUniforms: Float32List(32),
);
await _imageByteDataFromShader(shader: shader);
// Testing that no exceptions are thrown. Tests that the ink_sparkle shader
// produces the correct pixels are in the framework.
});
test('sksl uniforms are sorted correctly', () async {
final Uint8List sksl = await shaderFile(
path.join('general_shaders', 'sksl'),
'uniforms_sorted.frag.sksl',
).readAsBytes();
final FragmentProgram program = await FragmentProgram.compile(
raw: sksl.buffer,
uniformFloatCount: 32,
);
// The shader will not render green if the compiler doesn't keep the
// uniforms in the right order.
final Shader shader = program.shader(
floatUniforms: Float32List.fromList(
List<double>.generate(32, (int i) => i.toDouble()),
),
);
await _expectShaderRendersGreen(shader);
});
// Test all supported GLSL ops. See lib/spirv/lib/src/constants.dart
final Map<String, ByteBuffer> supportedGLSLOpShaders = _loadShaders(
path.join('supported_glsl_op_shaders', 'spirv'),
'.spirv',
);
expect(supportedGLSLOpShaders.isNotEmpty, true);
_expectShadersRenderGreen(supportedGLSLOpShaders);
_expectShadersHaveOp(supportedGLSLOpShaders, true /* glsl ops */);
final Map<String, ByteBuffer> skslSupportedGLSLOpShaders = _loadShaders(
path.join('supported_glsl_op_shaders', 'sksl'),
'.sksl',
);
expect(skslSupportedGLSLOpShaders.isNotEmpty, true);
_expectSkSLShadersRenderGreen(skslSupportedGLSLOpShaders);
// Test all supported instructions. See lib/spirv/lib/src/constants.dart
final Map<String, ByteBuffer> supportedOpShaders = _loadShaders(
path.join('supported_op_shaders', 'spirv'),
'.spirv',
);
expect(supportedOpShaders.isNotEmpty, true);
_expectShadersRenderGreen(supportedOpShaders);
_expectShadersHaveOp(supportedOpShaders, false /* glsl ops */);
final Map<String, ByteBuffer> skslSupportedOpShaders = _loadShaders(
path.join('supported_op_shaders', 'sksl'),
'.sksl',
);
expect(skslSupportedOpShaders.isNotEmpty, true);
_expectSkSLShadersRenderGreen(skslSupportedOpShaders);
test('equality depends on floatUniforms', () async {
final ByteBuffer spirv = shaderFile(
path.join('general_shaders', 'spirv'),
'simple.frag.spirv',
).readAsBytesSync().buffer;
final FragmentProgram program = await FragmentProgram.compile(spirv: spirv);
final Float32List ones = Float32List.fromList(<double>[1]);
final Float32List zeroes = Float32List.fromList(<double>[0]);
{
final Shader a = program.shader(floatUniforms: ones);
final Shader b = program.shader(floatUniforms: ones);
expect(a, b);
expect(a.hashCode, b.hashCode);
}
{
final Shader a = program.shader(floatUniforms: ones);
final Shader b = program.shader(floatUniforms: zeroes);
expect(a, notEquals(b));
expect(a.hashCode, notEquals(b.hashCode));
}
});
test('sksl equality depends on floatUniforms', () async {
final ByteBuffer sksl = shaderFile(
path.join('general_shaders', 'sksl'),
'simple.frag.sksl',
).readAsBytesSync().buffer;
final FragmentProgram program = await FragmentProgram.compile(
raw: sksl,
uniformFloatCount: 1,
);
final Float32List ones = Float32List.fromList(<double>[1]);
final Float32List zeroes = Float32List.fromList(<double>[0]);
{
final Shader a = program.shader(floatUniforms: ones);
final Shader b = program.shader(floatUniforms: ones);
expect(a, b);
expect(a.hashCode, b.hashCode);
}
{
final Shader a = program.shader(floatUniforms: ones);
final Shader b = program.shader(floatUniforms: zeroes);
expect(a, notEquals(b));
expect(a.hashCode, notEquals(b.hashCode));
}
});
test('equality depends on spirv', () async {
final ByteBuffer spirvA = shaderFile(
path.join('general_shaders', 'spirv'),
'simple.frag.spirv',
).readAsBytesSync().buffer;
final ByteBuffer spirvB = shaderFile(
path.join('general_shaders', 'spirv'),
'uniforms.frag.spirv',
).readAsBytesSync().buffer;
final FragmentProgram programA = await FragmentProgram.compile(spirv: spirvA);
final FragmentProgram programB = await FragmentProgram.compile(spirv: spirvB);
final Shader a = programA.shader();
final Shader b = programB.shader();
expect(a, notEquals(b));
expect(a.hashCode, notEquals(b.hashCode));
});
test('equality depends on data', () async {
final ByteBuffer skslA = shaderFile(
path.join('general_shaders', 'sksl'),
'simple.frag.sksl',
).readAsBytesSync().buffer;
final ByteBuffer skslB = shaderFile(
path.join('general_shaders', 'sksl'),
'uniforms.frag.sksl',
).readAsBytesSync().buffer;
final FragmentProgram programA = await FragmentProgram.compile(
raw: skslA,
uniformFloatCount: 1,
);
final FragmentProgram programB = await FragmentProgram.compile(
raw: skslB,
uniformFloatCount: 7,
);
final Shader a = programA.shader();
final Shader b = programB.shader();
expect(a, notEquals(b));
expect(a.hashCode, notEquals(b.hashCode));
});
test('Compilation does not create a Timer object', () async {
final ByteBuffer spirvA = shaderFile(
path.join('general_shaders', 'spirv'),
'simple.frag.spirv',
).readAsBytesSync().buffer;
bool createdTimer = false;
final ZoneSpecification specification = ZoneSpecification(
createTimer: (Zone self, ZoneDelegate parent, Zone zone, Duration duration, void Function() f) {
createdTimer = true;
return parent.createTimer(zone, duration, f);
},
);
await runZoned(() async {
await FragmentProgram.compile(spirv: spirvA);
}, zoneSpecification: specification);
expect(createdTimer, false);
});
test('sksl "compile" does not create a Timer object', () async {
final ByteBuffer skslA = shaderFile(
path.join('general_shaders', 'sksl'),
'simple.frag.sksl',
).readAsBytesSync().buffer;
bool createdTimer = false;
final ZoneSpecification specification = ZoneSpecification(
createTimer: (Zone self, ZoneDelegate parent, Zone zone, Duration duration, void Function() f) {
createdTimer = true;
return parent.createTimer(zone, duration, f);
},
);
await runZoned(() async {
await FragmentProgram.compile(
raw: skslA,
uniformFloatCount: 1,
);
}, zoneSpecification: specification);
expect(createdTimer, false);
});
}
// Expect that all of the spirv shaders in this folder render green.
// Keeping the outer loop of the test synchronous allows for easy printing
// of the file name within the test case.
void _expectShadersRenderGreen(Map<String, ByteBuffer> shaders) {
for (final String key in shaders.keys) {
test('$key renders green', () async {
final FragmentProgram program = await FragmentProgram.compile(
spirv: shaders[key]!,
);
final Shader shader = program.shader(
floatUniforms: Float32List.fromList(<double>[1]),
);
_expectShaderRendersGreen(shader);
});
}
}
// Expect that all of the shaders in this folder render green.
// Keeping the outer loop of the test synchronous allows for easy printing
// of the file name within the test case.
void _expectSkSLShadersRenderGreen(Map<String, ByteBuffer> shaders) {
for (final String key in shaders.keys) {
test('SkSL $key renders green', () async {
final FragmentProgram program = await FragmentProgram.compile(
raw: shaders[key]!,
uniformFloatCount: 1,
);
final Shader shader = program.shader(
floatUniforms: Float32List.fromList(<double>[1]),
);
_expectShaderRendersGreen(shader);
});
}
}
void _expectShadersHaveOp(Map<String, ByteBuffer> shaders, bool glsl) {
for (final String key in shaders.keys) {
test('$key contains opcode', () {
_expectShaderHasOp(shaders[key]!, key, glsl);
});
}
}
const int _opExtInst = 12;
// Expects that a spirv shader has the op code identified by its file name.
void _expectShaderHasOp(ByteBuffer spirv, String filename, bool glsl) {
final Uint32List words = spirv.asUint32List();
final List<String> sections = filename.split('_');
expect(sections.length, greaterThan(1));
final int op = int.parse(sections.first);
// skip the header
int position = 5;
bool found = false;
while (position < words.length) {
final int word = words[position];
final int currentOpCode = word & 0xFFFF;
if (glsl) {
if (currentOpCode == _opExtInst && words[position + 4] == op) {
found = true;
break;
}
} else {
if (currentOpCode == op) {
found = true;
break;
}
}
final int advance = word >> 16;
if (advance <= 0) {
break;
}
position += advance;
}
expect(found, true);
}
// Expects that a spirv shader only outputs the color green.
Future<void> _expectShaderRendersGreen(Shader shader) async {
final ByteData renderedBytes = (await _imageByteDataFromShader(
shader: shader,
imageDimension: _shaderImageDimension,
))!;
for (final int color in renderedBytes.buffer.asUint32List()) {
expect(toHexString(color), toHexString(_greenColor.value));
}
}
Future<ByteData?> _imageByteDataFromShader({
required Shader shader,
int imageDimension = 100,
}) async {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
final Paint paint = Paint()..shader = shader;
canvas.drawPaint(paint);
final Picture picture = recorder.endRecording();
final Image image = await picture.toImage(
imageDimension,
imageDimension,
);
return image.toByteData();
}
// Loads the path and spirv content of the files at
// $FLUTTER_BUILD_DIRECTORY/gen/flutter/lib/spirv/test/$leafFolderName
// This is synchronous so that tests can be inside of a loop with
// the proper test name.
Map<String, ByteBuffer> _loadShaders(String leafFolderName, String ext) {
final Map<String, ByteBuffer> out = SplayTreeMap<String, ByteBuffer>();
final Directory directory = shaderDirectory(leafFolderName);
if (!directory.existsSync()) {
return out;
}
directory
.listSync()
.where((FileSystemEntity entry) => path.extension(entry.path) == ext)
.forEach((FileSystemEntity entry) {
final String key = path.basenameWithoutExtension(entry.path);
out[key] = (entry as File).readAsBytesSync().buffer;
});
return out;
}
// Arbitrary, but needs to be greater than 1 for frag coord tests.
const int _shaderImageDimension = 4;
const Color _greenColor = Color(0xFF00FF00);
// Precision for checking uniform values.
const double epsilon = 0.5 / 255.0;
// Maps an int value from 0-255 to a double value of 0.0 to 1.0.
double toFloat(int v) => v.toDouble() / 255.0;
String toHexString(int color) => '#${color.toRadixString(16)}';
// 10x10 image where the left half is blue and the right half is
// green.
Future<Image> _createBlueGreenImage() async {
const int length = 10;
const int bytesPerPixel = 4;
final Uint8List pixels = Uint8List(length * length * bytesPerPixel);
int i = 0;
for (int y = 0; y < length; y++) {
for (int x = 0; x < length; x++) {
if (x < length/2) {
pixels[i+2] = 0xFF; // blue channel
} else {
pixels[i+1] = 0xFF; // green channel
}
pixels[i+3] = 0xFF; // alpha channel
i += bytesPerPixel;
}
}
final ImageDescriptor descriptor = ImageDescriptor.raw(
await ImmutableBuffer.fromUint8List(pixels),
width: length,
height: length,
pixelFormat: PixelFormat.rgba8888,
);
final Codec codec = await descriptor.instantiateCodec();
final FrameInfo frame = await codec.getNextFrame();
return frame.image;
}
final Float64List _identityMatrix = Float64List.fromList(<double>[
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]);