From 9b14b25ebdd778da9e6d3405de0682f8a975c42f Mon Sep 17 00:00:00 2001 From: Zachary Anderson Date: Wed, 31 Aug 2022 13:46:16 -0700 Subject: [PATCH] Reland: Adds a reusable FragmentShader (#35846) --- lib/ui/dart_ui.cc | 4 + lib/ui/painting.dart | 146 ++++++--- lib/ui/painting/fragment_program.cc | 7 + lib/ui/painting/fragment_program.h | 7 + lib/ui/painting/fragment_shader.cc | 72 +++++ lib/ui/painting/fragment_shader.h | 34 ++ lib/web_ui/lib/painting.dart | 32 +- .../lib/src/engine/canvaskit/shader.dart | 37 ++- .../src/engine/html/shaders/image_shader.dart | 2 +- .../lib/src/engine/html/shaders/shader.dart | 6 + testing/dart/fragment_shader_test.dart | 305 +++++++++++++++++- 11 files changed, 590 insertions(+), 62 deletions(-) diff --git a/lib/ui/dart_ui.cc b/lib/ui/dart_ui.cc index 20f7345fcd707..b82c69fd978d6 100644 --- a/lib/ui/dart_ui.cc +++ b/lib/ui/dart_ui.cc @@ -18,6 +18,7 @@ #include "flutter/lib/ui/painting/color_filter.h" #include "flutter/lib/ui/painting/engine_layer.h" #include "flutter/lib/ui/painting/fragment_program.h" +#include "flutter/lib/ui/painting/fragment_shader.h" #include "flutter/lib/ui/painting/gradient.h" #include "flutter/lib/ui/painting/image.h" #include "flutter/lib/ui/painting/image_descriptor.h" @@ -67,6 +68,7 @@ typedef CanvasPath Path; V(Canvas::Create, 6) \ V(ColorFilter::Create, 1) \ V(FragmentProgram::Create, 1) \ + V(ReusableFragmentShader::Create, 4) \ V(Gradient::Create, 1) \ V(ImageFilter::Create, 1) \ V(ImageShader::Create, 1) \ @@ -167,6 +169,8 @@ typedef CanvasPath Path; V(EngineLayer, dispose, 1) \ V(FragmentProgram, initFromAsset, 2) \ V(FragmentProgram, shader, 4) \ + V(ReusableFragmentShader, Dispose, 1) \ + V(ReusableFragmentShader, SetSampler, 3) \ V(Gradient, initLinear, 6) \ V(Gradient, initRadial, 8) \ V(Gradient, initSweep, 9) \ diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index cb7c2df5e0fca..1be62147f900c 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -1399,9 +1399,10 @@ class Paint { } set shader(Shader? value) { assert(() { - if (value is ImageShader) { - assert(!value.debugDisposed, 'Attempted to set a disposed shader to $this'); - } + assert( + value == null || !value.debugDisposed, + 'Attempted to set a disposed shader to $this', + ); return true; }()); _ensureObjectsInitialized()[_kShaderIndex] = value; @@ -3682,6 +3683,37 @@ class Shader extends NativeFieldWrapperClass1 { /// or extended directly. @pragma('vm:entry-point') Shader._(); + + bool _debugDisposed = false; + + /// Whether [dispose] has been called. + /// + /// This must only be used when asserts are enabled. Otherwise, it will throw. + bool get debugDisposed { + late bool disposed; + assert(() { + disposed = _debugDisposed; + return true; + }()); + return disposed; + } + + /// Release the resources used by this object. The object is no longer usable + /// after this method is called. + /// + /// The underlying memory allocated by this object will be retained beyond + /// this call if it is still needed by another object that has not been + /// disposed. For example, a [Picture] that has not been disposed that + /// refers to an [ImageShader] may keep its underlying resources alive. + /// + /// Classes that override this method must call `super.dispose()`. + void dispose() { + assert(() { + assert(!_debugDisposed); + _debugDisposed = true; + return true; + }()); + } } /// Defines what happens at the edge of a gradient or the sampling of a source image @@ -4056,55 +4088,29 @@ class ImageShader extends Shader { } } + @override + void dispose() { + super.dispose(); + _dispose(); + } + @FfiNative('ImageShader::Create') external void _constructor(); @FfiNative, Pointer, Int32, Int32, Int32, Handle)>('ImageShader::initWithImage') external String? _initWithImage(_Image image, int tmx, int tmy, int filterQualityIndex, Float64List matrix4); - bool _debugDisposed = false; - - /// Whether [dispose] has been called. - /// - /// This must only be used when asserts are enabled. Otherwise, it will throw. - bool get debugDisposed { - late bool disposed; - assert(() { - disposed = _debugDisposed; - return true; - }()); - return disposed; - } - - /// Release the resources used by this object. The object is no longer usable - /// after this method is called. - /// - /// The underlying memory allocated by this object will be retained beyond - /// this call if it is still needed by another object that has not been - /// disposed. For example, an [Picture] that has not been disposed that - /// refers to this [ImageShader] may keep its underlying resources alive. - void dispose() { - assert(() { - assert(!_debugDisposed); - _debugDisposed = true; - return true; - }()); - _dispose(); - } - /// This can't be a leaf call because the native function calls Dart API /// (Dart_SetNativeInstanceField). @FfiNative)>('ImageShader::dispose') external void _dispose(); } -/// An instance of [FragmentProgram] creates [Shader] objects (as used by [Paint.shader]) that run SPIR-V code. +/// An instance of [FragmentProgram] creates [Shader] objects (as used by +/// [Paint.shader]). /// /// This API is in beta and does not yet work on web. /// See https://github.com/flutter/flutter/projects/207 for roadmap. -/// -/// [A current specification of valid SPIR-V is here.](https://github.com/flutter/engine/blob/main/lib/spirv/README.md) -/// class FragmentProgram extends NativeFieldWrapperClass1 { @pragma('vm:entry-point') FragmentProgram._fromAsset(String assetKey) { @@ -4181,6 +4187,9 @@ class FragmentProgram extends NativeFieldWrapperClass1 { @FfiNative, Handle)>('FragmentProgram::initFromAsset') external String _initFromAsset(String assetKey); + /// Returns a fresh instance of [FragmentShader]. + FragmentShader fragmentShader() => FragmentShader._(this); + /// Constructs a [Shader] object suitable for use by [Paint.shader] with /// the given uniforms. /// @@ -4263,6 +4272,69 @@ class FragmentProgram extends NativeFieldWrapperClass1 { external Handle _shader(_FragmentShader shader, Float32List floatUniforms, List samplerUniforms); } +/// A [Shader] generated from a [FragmentProgram]. +/// +/// Instances of this class can be obtained from the +/// [FragmentProgram.fragmentShader] method. The float uniforms list is +/// initialized to the size expected by the shader and is zero-filled. Uniforms +/// of float type can then be set by calling [setFloat]. Sampler uniforms are +/// set by calling [setSampler]. +/// +/// A [FragmentShader] can be re-used, and this is an efficient way to avoid +/// allocating and re-initializing the uniform buffer and samplers. However, +/// if two [FragmentShader] objects with different float uniforms or samplers +/// are required to exist simultaneously, they must be obtained from two +/// different calls to [FragmentProgram.fragmentShader]. +class FragmentShader extends Shader { + FragmentShader._(FragmentProgram program) : super._() { + _floats = _constructor( + program, + program._uniformFloatCount, + program._samplerCount, + ); + } + + static final Float32List _kEmptyFloat32List = Float32List(0); + + late Float32List _floats; + + /// Sets the float uniform at [index] to [value]. + void setFloat(int index, double value) { + assert(!debugDisposed, 'Tried to accesss uniforms on a disposed Shader: $this'); + _floats[index] = value; + } + + /// Sets the sampler uniform at [index] to [sampler]. + /// + /// All the sampler uniforms that a shader expects must be provided or the + /// results will be undefined. + void setSampler(int index, ImageShader sampler) { + assert(!debugDisposed, 'Tried to access uniforms on a disposed Shader: $this'); + _setSampler(index, sampler); + } + + /// Releases the native resources held by the [FragmentShader]. + /// + /// After this method is called, calling methods on the shader, or attaching + /// it to a [Paint] object will fail with an exception. Calling [dispose] + /// twice will also result in an exception being thrown. + @override + void dispose() { + super.dispose(); + _floats = _kEmptyFloat32List; + _dispose(); + } + + @FfiNative('ReusableFragmentShader::Create') + external Float32List _constructor(FragmentProgram program, int floatUniforms, int samplerUniforms); + + @FfiNative, Handle, Handle)>('ReusableFragmentShader::SetSampler') + external void _setSampler(int index, ImageShader sampler); + + @FfiNative)>('ReusableFragmentShader::Dispose') + external void _dispose(); +} + @pragma('vm:entry-point') class _FragmentShader extends Shader { /// This class is created by the engine and should not be instantiated diff --git a/lib/ui/painting/fragment_program.cc b/lib/ui/painting/fragment_program.cc index e55c09f3eac94..26a53994a3f90 100644 --- a/lib/ui/painting/fragment_program.cc +++ b/lib/ui/painting/fragment_program.cc @@ -133,6 +133,13 @@ fml::RefPtr FragmentProgram::shader(Dart_Handle shader, std::move(uniform_data))); } +std::shared_ptr FragmentProgram::MakeDlColorSource( + sk_sp float_uniforms, + const std::vector>& children) { + return DlColorSource::MakeRuntimeEffect(runtime_effect_, std::move(children), + std::move(float_uniforms)); +} + void FragmentProgram::Create(Dart_Handle wrapper) { auto res = fml::MakeRefCounted(); res->AssociateWithDartWrapper(wrapper); diff --git a/lib/ui/painting/fragment_program.h b/lib/ui/painting/fragment_program.h index 4bc548acc5a70..0f40ddffd9d41 100644 --- a/lib/ui/painting/fragment_program.h +++ b/lib/ui/painting/fragment_program.h @@ -7,6 +7,7 @@ #include "flutter/lib/ui/dart_wrapper.h" #include "flutter/lib/ui/painting/fragment_shader.h" +#include "flutter/lib/ui/painting/shader.h" #include "third_party/skia/include/effects/SkRuntimeEffect.h" #include "third_party/tonic/dart_library_natives.h" #include "third_party/tonic/typed_data/typed_list.h" @@ -16,6 +17,8 @@ namespace flutter { +class FragmentShader; + class FragmentProgram : public RefCountedDartWrappable { DEFINE_WRAPPERTYPEINFO(); FML_FRIEND_MAKE_REF_COUNTED(FragmentProgram); @@ -30,6 +33,10 @@ class FragmentProgram : public RefCountedDartWrappable { Dart_Handle uniforms_handle, Dart_Handle samplers); + std::shared_ptr MakeDlColorSource( + sk_sp float_uniforms, + const std::vector>& children); + private: FragmentProgram(); sk_sp runtime_effect_; diff --git a/lib/ui/painting/fragment_shader.cc b/lib/ui/painting/fragment_shader.cc index 39cee53482bed..7d57b3ac1402d 100644 --- a/lib/ui/painting/fragment_shader.cc +++ b/lib/ui/painting/fragment_shader.cc @@ -7,6 +7,7 @@ #include "flutter/lib/ui/painting/fragment_shader.h" #include "flutter/lib/ui/dart_wrapper.h" +#include "flutter/lib/ui/painting/fragment_program.h" #include "flutter/lib/ui/ui_dart_state.h" #include "third_party/skia/include/core/SkString.h" #include "third_party/tonic/converter/dart_converter.h" @@ -48,4 +49,75 @@ FragmentShader::FragmentShader( FragmentShader::~FragmentShader() = default; +IMPLEMENT_WRAPPERTYPEINFO(ui, ReusableFragmentShader); + +ReusableFragmentShader::ReusableFragmentShader( + fml::RefPtr program, + uint64_t float_count, + uint64_t sampler_count) + : program_(program), + uniform_data_(SkData::MakeUninitialized( + (float_count + 2 * sampler_count) * sizeof(float))), + samplers_(sampler_count), + float_count_(float_count) {} + +Dart_Handle ReusableFragmentShader::Create(Dart_Handle wrapper, + Dart_Handle program, + Dart_Handle float_count_handle, + Dart_Handle sampler_count_handle) { + auto* fragment_program = + tonic::DartConverter::FromDart(program); + uint64_t float_count = + tonic::DartConverter::FromDart(float_count_handle); + uint64_t sampler_count = + tonic::DartConverter::FromDart(sampler_count_handle); + + auto res = fml::MakeRefCounted( + fml::Ref(fragment_program), float_count, sampler_count); + res->AssociateWithDartWrapper(wrapper); + + void* raw_uniform_data = + reinterpret_cast(res->uniform_data_->writable_data()); + return Dart_NewExternalTypedData(Dart_TypedData_kFloat32, raw_uniform_data, + float_count); +} + +void ReusableFragmentShader::SetSampler(Dart_Handle index_handle, + Dart_Handle sampler_handle) { + uint64_t index = tonic::DartConverter::FromDart(index_handle); + ImageShader* sampler = + tonic::DartConverter::FromDart(sampler_handle); + if (index >= samplers_.size()) { + Dart_ThrowException(tonic::ToDart("Sampler index out of bounds")); + } + + // ImageShaders can hold a preferred value for sampling options and + // developers are encouraged to use that value or the value will be supplied + // by "the environment where it is used". The environment here does not + // contain a value to be used if the developer did not specify a preference + // when they constructed the ImageShader, so we will use kNearest which is + // the default filterQuality in a Paint object. + DlImageSampling sampling = DlImageSampling::kNearestNeighbor; + auto* uniform_floats = + reinterpret_cast(uniform_data_->writable_data()); + samplers_[index] = sampler->shader(sampling); + uniform_floats[float_count_ + 2 * index] = sampler->width(); + uniform_floats[float_count_ + 2 * index + 1] = sampler->height(); +} + +std::shared_ptr ReusableFragmentShader::shader( + DlImageSampling sampling) { + FML_CHECK(program_); + return program_->MakeDlColorSource(uniform_data_, samplers_); +} + +void ReusableFragmentShader::Dispose() { + uniform_data_.reset(); + program_ = nullptr; + samplers_.clear(); + ClearDartWrapper(); +} + +ReusableFragmentShader::~ReusableFragmentShader() = default; + } // namespace flutter diff --git a/lib/ui/painting/fragment_shader.h b/lib/ui/painting/fragment_shader.h index f3aa48a156577..2c84b2408ff75 100644 --- a/lib/ui/painting/fragment_shader.h +++ b/lib/ui/painting/fragment_shader.h @@ -6,6 +6,7 @@ #define FLUTTER_LIB_UI_PAINTING_FRAGMENT_SHADER_H_ #include "flutter/lib/ui/dart_wrapper.h" +#include "flutter/lib/ui/painting/fragment_program.h" #include "flutter/lib/ui/painting/image.h" #include "flutter/lib/ui/painting/image_shader.h" #include "flutter/lib/ui/painting/shader.h" @@ -19,6 +20,8 @@ namespace flutter { +class FragmentProgram; + class FragmentShader : public Shader { DEFINE_WRAPPERTYPEINFO(); FML_FRIEND_MAKE_REF_COUNTED(FragmentShader); @@ -29,6 +32,7 @@ class FragmentShader : public Shader { Dart_Handle dart_handle, std::shared_ptr shader); + // |Shader| std::shared_ptr shader(DlImageSampling) override; private: @@ -37,6 +41,36 @@ class FragmentShader : public Shader { std::shared_ptr source_; }; +class ReusableFragmentShader : public Shader { + DEFINE_WRAPPERTYPEINFO(); + FML_FRIEND_MAKE_REF_COUNTED(ReusableFragmentShader); + + public: + ~ReusableFragmentShader() override; + + static Dart_Handle Create(Dart_Handle wrapper, + Dart_Handle program, + Dart_Handle float_count, + Dart_Handle sampler_count); + + void SetSampler(Dart_Handle index, Dart_Handle sampler); + + void Dispose(); + + // |Shader| + std::shared_ptr shader(DlImageSampling) override; + + private: + ReusableFragmentShader(fml::RefPtr program, + uint64_t float_count, + uint64_t sampler_count); + + fml::RefPtr program_; + sk_sp uniform_data_; + std::vector> samplers_; + size_t float_count_; +}; + } // namespace flutter #endif // FLUTTER_LIB_UI_PAINTING_FRAGMENT_SHADER_H_ diff --git a/lib/web_ui/lib/painting.dart b/lib/web_ui/lib/painting.dart index 0edd2faeb0dbd..8daf324d9d77c 100644 --- a/lib/web_ui/lib/painting.dart +++ b/lib/web_ui/lib/painting.dart @@ -270,6 +270,10 @@ abstract class Paint { abstract class Shader { Shader._(); + + void dispose(); + + bool get debugDisposed; } abstract class Gradient extends Shader { @@ -700,8 +704,10 @@ abstract class ImageShader extends Shader { FilterQuality? filterQuality, }) => engine.renderer.createImageShader(image, tmx, tmy, matrix4, filterQuality); + @override void dispose(); + @override bool get debugDisposed; } @@ -799,8 +805,8 @@ class FragmentProgram { throw UnsupportedError('FragmentProgram is not supported for the CanvasKit or HTML renderers.'); } - static Future fromAssetAsync(String assetKey) { - return Future.microtask(() => FragmentProgram.fromAsset(assetKey)); + FragmentShader fragmentShader() { + throw UnsupportedError('FragmentProgram is not supported for the CanvasKit or HTML renderers.'); } Shader shader({ @@ -808,3 +814,25 @@ class FragmentProgram { List? samplerUniforms, }) => throw UnsupportedError('FragmentProgram is not supported for the CanvasKit or HTML renderers.'); } + +class FragmentShader extends Shader { + FragmentShader._() : super._(); + + void setFloat(int index, double value) { + throw UnsupportedError('FragmentShader is not supported for the CanvasKit or HTML renderers.'); + } + + void setSampler(int index, ImageShader sampler) { + throw UnsupportedError('FragmentShader is not supported for the CanvasKit or HTML renderers.'); + } + + @override + void dispose() { + throw UnsupportedError('FragmentShader is not supported for the CanvasKit or HTML renderers.'); + } + + @override + bool get debugDisposed { + throw UnsupportedError('FragmentShader is not supported for the CanvasKit or HTML renderers.'); + } +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/shader.dart b/lib/web_ui/lib/src/engine/canvaskit/shader.dart index 531a251da5744..53144103abfec 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/shader.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/shader.dart @@ -21,6 +21,26 @@ abstract class CkShader extends ManagedSkiaObject void delete() { rawSkiaObject?.delete(); } + + bool _disposed = false; + + @override + bool get debugDisposed { + late bool disposed; + assert(() { + disposed = _disposed; + return true; + }()); + return disposed; + } + + @override + void dispose() { + assert(() { + _disposed = true; + return true; + }()); + } } class CkGradientSweep extends CkShader implements ui.Gradient { @@ -219,24 +239,9 @@ class CkImageShader extends CkShader implements ui.ImageShader { rawSkiaObject?.delete(); } - bool _disposed = false; - - @override - bool get debugDisposed { - late bool disposed; - assert(() { - disposed = _disposed; - return true; - }()); - return disposed; - } - @override void dispose() { - assert(() { - _disposed = true; - return true; - }()); + super.dispose(); _image.dispose(); } } diff --git a/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart b/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart index 60cca92e3db03..0caede789b3e7 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart @@ -284,6 +284,6 @@ class EngineImageShader implements ui.ImageShader { _disposed = true; return true; }()); - image.dispose(); + image.dispose(); } } diff --git a/lib/web_ui/lib/src/engine/html/shaders/shader.dart b/lib/web_ui/lib/src/engine/html/shaders/shader.dart index a12da953df576..be3ebaa92e361 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/shader.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/shader.dart @@ -54,6 +54,12 @@ abstract class EngineGradient implements ui.Gradient { /// Creates a CanvasImageSource to paint gradient. Object createImageBitmap( ui.Rect? shaderBounds, double density, bool createDataUrl); + + @override + bool debugDisposed = false; + + @override + void dispose() {} } class GradientSweep extends EngineGradient { diff --git a/testing/dart/fragment_shader_test.dart b/testing/dart/fragment_shader_test.dart index 5fcc63f3ad328..73404e6e25e49 100644 --- a/testing/dart/fragment_shader_test.dart +++ b/testing/dart/fragment_shader_test.dart @@ -14,6 +14,132 @@ import 'package:path/path.dart' as path; import 'shader_test_file_utils.dart'; void main() async { + bool assertsEnabled = false; + assert(() { + assertsEnabled = true; + return true; + }()); + + test('FragmentShader setSampler throws with out-of-bounds index', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'blue_green_sampler.frag.iplr', + ); + final Image blueGreenImage = await _createBlueGreenImage(); + final ImageShader imageShader = ImageShader( + blueGreenImage, TileMode.clamp, TileMode.clamp, _identityMatrix); + final FragmentShader fragmentShader = program.fragmentShader(); + + try { + fragmentShader.setSampler(1, imageShader); + fail('Unreachable'); + } catch (e) { + expect(e, contains('Sampler index out of bounds')); + } finally { + fragmentShader.dispose(); + imageShader.dispose(); + blueGreenImage.dispose(); + } + }); + + test('Disposed FragmentShader on Paint', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'blue_green_sampler.frag.iplr', + ); + final Image blueGreenImage = await _createBlueGreenImage(); + final ImageShader imageShader = ImageShader( + blueGreenImage, TileMode.clamp, TileMode.clamp, _identityMatrix); + + final FragmentShader shader = program.fragmentShader() + ..setSampler(0, imageShader); + shader.dispose(); + try { + final Paint paint = Paint()..shader = shader; // ignore: unused_local_variable + if (assertsEnabled) { + fail('Unreachable'); + } + } catch (e) { + expect(e.toString(), contains('Attempted to set a disposed shader')); + } + imageShader.dispose(); + blueGreenImage.dispose(); + }); + + test('Disposed FragmentShader setFloat', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'uniforms.frag.iplr', + ); + final FragmentShader shader = program.fragmentShader() + ..setFloat(0, 0.0); + shader.dispose(); + try { + shader.setFloat(0, 0.0); + if (assertsEnabled) { + fail('Unreachable'); + } + } catch (e) { + if (assertsEnabled) { + expect( + e.toString(), + contains('Tried to accesss uniforms on a disposed Shader'), + ); + } else { + expect(e is RangeError, true); + } + } + }); + + test('Disposed FragmentShader setSampler', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'blue_green_sampler.frag.iplr', + ); + final Image blueGreenImage = await _createBlueGreenImage(); + final ImageShader imageShader = ImageShader( + blueGreenImage, TileMode.clamp, TileMode.clamp, _identityMatrix); + + final FragmentShader shader = program.fragmentShader() + ..setSampler(0, imageShader); + shader.dispose(); + try { + shader.setSampler(0, imageShader); + if (assertsEnabled) { + fail('Unreachable'); + } + } on AssertionError catch (e) { + expect( + e.toString(), + contains('Tried to access uniforms on a disposed Shader'), + ); + } on StateError catch (e) { + expect( + e.toString(), + contains('the native peer has been collected'), + ); + } + imageShader.dispose(); + blueGreenImage.dispose(); + }); + + test('Disposed FragmentShader dispose', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'uniforms.frag.iplr', + ); + final FragmentShader shader = program.fragmentShader() + ..setFloat(0, 0.0); + shader.dispose(); + try { + shader.dispose(); + if (assertsEnabled) { + fail('Unreachable'); + } + } catch (e) { + if (assertsEnabled) { + expect(e is AssertionError, true); + } else { + expect(e is StateError, true); + } + } + }); + test('simple shader renders correctly', () async { final FragmentProgram program = await FragmentProgram.fromAsset( 'functions.frag.iplr', @@ -21,7 +147,31 @@ void main() async { final Shader shader = program.shader( floatUniforms: Float32List.fromList([1]), ); - _expectShaderRendersGreen(shader); + await _expectShaderRendersGreen(shader); + }); + + test('FragmentShader simple shader renders correctly', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'functions.frag.iplr', + ); + final FragmentShader shader = program.fragmentShader() + ..setFloat(0, 1.0); + await _expectShaderRendersGreen(shader); + shader.dispose(); + }); + + test('Reused FragmentShader simple shader renders correctly', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'functions.frag.iplr', + ); + final FragmentShader shader = program.fragmentShader() + ..setFloat(0, 1.0); + await _expectShaderRendersGreen(shader); + + shader.setFloat(0, 0.0); + await _expectShaderRendersBlack(shader); + + shader.dispose(); }); test('blue-green image renders green', () async { @@ -36,6 +186,22 @@ void main() async { samplerUniforms: [imageShader], ); await _expectShaderRendersGreen(shader); + imageShader.dispose(); + blueGreenImage.dispose(); + }); + + test('FragmentShader blue-green image renders green', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'blue_green_sampler.frag.iplr', + ); + final Image blueGreenImage = await _createBlueGreenImage(); + final ImageShader imageShader = ImageShader( + blueGreenImage, TileMode.clamp, TileMode.clamp, _identityMatrix); + final FragmentShader shader = program.fragmentShader() + ..setSampler(0, imageShader); + await _expectShaderRendersGreen(shader); + shader.dispose(); + imageShader.dispose(); blueGreenImage.dispose(); }); @@ -51,9 +217,24 @@ void main() async { samplerUniforms: [imageShader], ); await _expectShaderRendersGreen(shader); + imageShader.dispose(); blueGreenImage.dispose(); }); + test('FragmentShader blue-green image renders green - GPU image', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'blue_green_sampler.frag.iplr', + ); + final Image blueGreenImage = _createBlueGreenImageSync(); + final ImageShader imageShader = ImageShader( + blueGreenImage, TileMode.clamp, TileMode.clamp, _identityMatrix); + final FragmentShader shader = program.fragmentShader() + ..setSampler(0, imageShader); + await _expectShaderRendersGreen(shader); + shader.dispose(); + imageShader.dispose(); + blueGreenImage.dispose(); + }); test('shader with uniforms renders correctly', () async { final FragmentProgram program = await FragmentProgram.fromAsset( @@ -81,6 +262,32 @@ void main() async { expect(toFloat(renderedBytes.getUint8(3)), closeTo(1.0, epsilon)); }); + test('FragmentShader with uniforms renders correctly', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'uniforms.frag.iplr', + ); + + final FragmentShader shader = program.fragmentShader() + ..setFloat(0, 0.0) + ..setFloat(1, 0.25) + ..setFloat(2, 0.75) + ..setFloat(3, 0.0) + ..setFloat(4, 0.0) + ..setFloat(5, 0.0) + ..setFloat(6, 1.0); + + 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)); + + shader.dispose(); + }); + test('shader with array uniforms renders correctly', () async { final FragmentProgram program = await FragmentProgram.fromAsset( 'uniform_arrays.frag.iplr', @@ -97,6 +304,20 @@ void main() async { await _expectShaderRendersGreen(shader); }); + test('FragmentShader shader with array uniforms renders correctly', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'uniform_arrays.frag.iplr', + ); + + final FragmentShader shader = program.fragmentShader(); + for (int i = 0; i < 24; i++) { + shader.setFloat(i, i.toDouble()); + } + + await _expectShaderRendersGreen(shader); + shader.dispose(); + }); + test('The ink_sparkle shader is accepted', () async { final FragmentProgram program = await FragmentProgram.fromAsset( 'ink_sparkle.frag.iplr', @@ -111,6 +332,19 @@ void main() async { // produces the correct pixels are in the framework. }); + test('FragmentShader The ink_sparkle shader is accepted', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'ink_sparkle.frag.iplr', + ); + final FragmentShader shader = program.fragmentShader(); + + await _imageByteDataFromShader(shader: shader); + + // Testing that no exceptions are thrown. Tests that the ink_sparkle shader + // produces the correct pixels are in the framework. + shader.dispose(); + }); + test('Uniforms are sorted correctly', () async { final FragmentProgram program = await FragmentProgram.fromAsset( 'uniforms_sorted.frag.iplr', @@ -127,6 +361,23 @@ void main() async { await _expectShaderRendersGreen(shader); }); + test('FragmentShader Uniforms are sorted correctly', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'uniforms_sorted.frag.iplr', + ); + + // The shader will not render green if the compiler doesn't keep the + // uniforms in the right order. + final FragmentShader shader = program.fragmentShader(); + for (int i = 0; i < 32; i++) { + shader.setFloat(i, i.toDouble()); + } + + await _expectShaderRendersGreen(shader); + + shader.dispose(); + }); + test('fromAsset throws an exception on invalid assetKey', () async { bool throws = false; try { @@ -161,6 +412,16 @@ void main() async { await _expectShaderRendersGreen(shader); }); + test('FragmentShader user defined functions do not redefine builtins', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'no_builtin_redefinition.frag.iplr', + ); + final FragmentShader shader = program.fragmentShader() + ..setFloat(0, 1.0); + await _expectShaderRendersGreen(shader); + shader.dispose(); + }); + test('fromAsset accepts a shader with no uniforms', () async { final FragmentProgram program = await FragmentProgram.fromAsset( 'no_uniforms.frag.iplr', @@ -169,6 +430,15 @@ void main() async { await _expectShaderRendersGreen(shader); }); + test('FragmentShader fromAsset accepts a shader with no uniforms', () async { + final FragmentProgram program = await FragmentProgram.fromAsset( + 'no_uniforms.frag.iplr', + ); + final FragmentShader shader = program.fragmentShader(); + await _expectShaderRendersGreen(shader); + shader.dispose(); + }); + // Test all supported GLSL ops. See lib/spirv/lib/src/constants.dart final Map iplrSupportedGLSLOpShaders = await _loadShaderAssets( path.join('supported_glsl_op_shaders', 'iplr'), @@ -176,6 +446,7 @@ void main() async { ); expect(iplrSupportedGLSLOpShaders.isNotEmpty, true); _expectIplrShadersRenderGreen(iplrSupportedGLSLOpShaders); + _expectFragmentShadersRenderGreen(iplrSupportedGLSLOpShaders); // Test all supported instructions. See lib/spirv/lib/src/constants.dart final Map iplrSupportedOpShaders = await _loadShaderAssets( @@ -184,6 +455,7 @@ void main() async { ); expect(iplrSupportedOpShaders.isNotEmpty, true); _expectIplrShadersRenderGreen(iplrSupportedOpShaders); + _expectFragmentShadersRenderGreen(iplrSupportedOpShaders); test('Equality depends on floatUniforms', () async { final FragmentProgram program = await FragmentProgram.fromAsset( @@ -232,22 +504,42 @@ void _expectIplrShadersRenderGreen(Map shaders) { final Shader shader = program.shader( floatUniforms: Float32List.fromList([1]), ); - _expectShaderRendersGreen(shader); + await _expectShaderRendersGreen(shader); + }); + } +} + +void _expectFragmentShadersRenderGreen(Map programs) { + for (final String key in programs.keys) { + test('FragmentProgram $key renders green', () async { + final FragmentProgram program = programs[key]!; + final FragmentShader shader = program.fragmentShader() + ..setFloat(0, 1.0); + await _expectShaderRendersGreen(shader); + shader.dispose(); }); } } -// Expects that a spirv shader only outputs the color green. -Future _expectShaderRendersGreen(Shader shader) async { +Future _expectShaderRendersColor(Shader shader, Color color) async { final ByteData renderedBytes = (await _imageByteDataFromShader( shader: shader, imageDimension: _shaderImageDimension, ))!; - for (final int color in renderedBytes.buffer.asUint32List()) { - expect(toHexString(color), toHexString(_greenColor.value)); + for (final int c in renderedBytes.buffer.asUint32List()) { + expect(toHexString(c), toHexString(color.value)); } } +// Expects that a shader only outputs the color green. +Future _expectShaderRendersGreen(Shader shader) { + return _expectShaderRendersColor(shader, _greenColor); +} + +Future _expectShaderRendersBlack(Shader shader) { + return _expectShaderRendersColor(shader, _blackColor); +} + Future _imageByteDataFromShader({ required Shader shader, int imageDimension = 100, @@ -297,6 +589,7 @@ Future> _loadShaderAssets( const int _shaderImageDimension = 4; const Color _greenColor = Color(0xFF00FF00); +const Color _blackColor = Color(0xFF000000); // Precision for checking uniform values. const double epsilon = 0.5 / 255.0;