Skip to content

Commit

Permalink
[dart:ui] Adds FragmentShaderBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
zanderso committed Aug 10, 2022
1 parent 75e17f1 commit 8873298
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 11 deletions.
3 changes: 3 additions & 0 deletions lib/ui/dart_ui.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) \
Expand Down Expand Up @@ -166,6 +168,7 @@ typedef CanvasPath Path;
V(EngineLayer, dispose, 1) \
V(FragmentProgram, initFromAsset, 2) \
V(FragmentProgram, shader, 4) \
V(ReusableFragmentShader, SetSampler, 3) \
V(Gradient, initLinear, 6) \
V(Gradient, initRadial, 8) \
V(Gradient, initSweep, 9) \
Expand Down
110 changes: 109 additions & 1 deletion lib/ui/painting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4052,7 +4052,7 @@ class ImageShader extends Shader {
external String? _initWithImage(_Image image, int tmx, int tmy, int filterQualityIndex, Float64List matrix4);
}

/// 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.
Expand Down Expand Up @@ -4135,6 +4135,9 @@ class FragmentProgram extends NativeFieldWrapperClass1 {
@FfiNative<Handle Function(Pointer<Void>, 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.
///
Expand Down Expand Up @@ -4217,6 +4220,111 @@ class FragmentProgram extends NativeFieldWrapperClass1 {
external Handle _shader(_FragmentShader shader, Float32List floatUniforms, List<ImageShader> samplerUniforms);
}

/// A utility class for building [Shader] objects from a [FragmentProgram].
///
/// Instances of this class can be obtained from the `shaderBuilder()` method
/// of a [FragmentProgram] instance. The [floatUniforms] list is initialized to
/// the size expected by the shader, and the [samplerUniforms] list is empty.
/// Uniforms of float type can then be set by setting elements of
/// [floatUniforms], and the sampler uniforms can be provided by adding elements
/// to [samplerUniforms].
///
/// On a call to the [build] method, the length of [samplerUniforms] must match
/// the number of sampler uniforms expected by the shader, otherwise a
/// [StateError] exception will be thrown. The [floatUniforms] list should not
/// be modified after calling the [build] method until the [Shader] object it
/// returns is no longer needed. Values set in the [floatUniforms] list will
/// persist across calls to [build] and use of the [Shader] object it returns.
/// This allows one to modify only the float uniforms of a shader that change
/// from frame to frame. If two [Shader] objects generated from the same
/// [FragmentProgram] are required to exist at the same time with different
/// uniform values, then they should be constructed using two different
/// [FragmentShaderBuilder] instances obtained from two calls to
/// [FragmentProgram.shaderBuilder].
///
/// Consider a fragment shader with uniforms like the following:
///
/// ```glsl
/// layout (location = 0) uniform float a;
/// layout (location = 1) uniform vec2 b;
/// layout (location = 2) uniform vec3 c;
/// layout (location = 3) uniform mat2x2 d;
/// layout (location = 4) uniform sampler2d s;
/// ```
///
/// If this shader is loaded into a [FragmentProgram] object `program`, a
/// [Shader] object can be constructed as follows:
/// ```dart
/// ImageShader sampler;
/// final FragmentShaderBuilder b = program.shaderBuilder()
/// ..floatUniforms[0] = 0.0
/// ..floatUniforms[1] = 1.0
/// ..floatUniforms[2] = 2.0
/// ..floatUniforms[3] = 3.0
/// ..floatUniforms[4] = 4.0
/// ..floatUniforms[5] = 5.0
/// ..floatUniforms[6] = 6.0
/// ..floatUniforms[7] = 7.0
/// ..floatUniforms[8] = 8.8
/// ..floatUniforms[9] = 9.0
/// ..samplerUniforms.add(sampler);
///
/// final Shader s = b.build();
/// ```
///
/// The uniforms will then be set as follows:
///
/// a: 0.0
/// b: [1.0, 2.0]
/// c: [3.0, 4.0, 5.0]
/// d: [6.0, 7.0, 8.0, 9.0] // 2x2 matrix in column-major order
///
/// On subsequent frames, if only the uniform `a` is required to vary, then
/// only `b.floatUniforms[0]` need be modified before another call to
/// `b.build()` to acquire the new [Shader]. However, even if the [ImageShader]
/// `s` does not change, the call to `b.samplerUniforms.add(s)` is still
/// required.
class FragmentShader extends Shader {
FragmentShader._(this.program) : super._() {
_floats = _constructor(program, program._uniformFloatCount, program._samplerCount);
}

final FragmentProgram program;
late final Float32List _floats;

Float32List get floatUniforms => _floats;

void setSampler(int index, ImageShader sampler) {
_setSampler(index, sampler);
}

@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is FragmentShader
&& other.program == program
&& _listEquals<double>(other._floats, _floats);
}

@override
int get hashCode => Object.hash(
program,
Object.hashAll(_floats),
);

@FfiNative<Handle Function(Handle, Handle, Handle, Handle)>('ReusableFragmentShader::Create')
external Float32List _constructor(FragmentProgram program, int floatUniforms, int samplerUniforms);


@FfiNative<Void Function(Pointer<Void>, Handle, Handle)>('ReusableFragmentShader::SetSampler')
external void _setSampler(int index, ImageShader sampler);
}

@pragma('vm:entry-point')
class _FragmentShader extends Shader {
/// This class is created by the engine and should not be instantiated
Expand Down
4 changes: 4 additions & 0 deletions lib/ui/painting/fragment_program.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

namespace flutter {

class FragmentShader;

class FragmentProgram : public RefCountedDartWrappable<FragmentProgram> {
DEFINE_WRAPPERTYPEINFO();
FML_FRIEND_MAKE_REF_COUNTED(FragmentProgram);
Expand All @@ -30,6 +32,8 @@ class FragmentProgram : public RefCountedDartWrappable<FragmentProgram> {
Dart_Handle uniforms_handle,
Dart_Handle samplers);

sk_sp<SkRuntimeEffect> runtime_effect() const { return runtime_effect_; }

private:
FragmentProgram();
sk_sp<SkRuntimeEffect> runtime_effect_;
Expand Down
68 changes: 68 additions & 0 deletions lib/ui/painting/fragment_shader.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -46,4 +47,71 @@ FragmentShader::FragmentShader(sk_sp<SkShader> shader)

FragmentShader::~FragmentShader() = default;

IMPLEMENT_WRAPPERTYPEINFO(ui, ReusableFragmentShader);

ReusableFragmentShader::ReusableFragmentShader(
fml::RefPtr<FragmentProgram> 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<FragmentProgram*>::FromDart(program);
uint64_t float_count =
tonic::DartConverter<uint64_t>::FromDart(float_count_handle);
uint64_t sampler_count =
tonic::DartConverter<uint64_t>::FromDart(sampler_count_handle);

auto res = fml::MakeRefCounted<ReusableFragmentShader>(
fml::Ref(fragment_program), float_count, sampler_count);
res->AssociateWithDartWrapper(wrapper);

void* raw_uniform_data =
reinterpret_cast<void*>(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<uint64_t>::FromDart(index_handle);
ImageShader* sampler =
tonic::DartConverter<ImageShader*>::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<float*>(uniform_data_->writable_data());
samplers_[index] = sampler->shader(sampling)->skia_object();
uniform_floats[float_count_ + 2 * index] = sampler->width();
uniform_floats[float_count_ + 2 * index + 1] = sampler->height();
}

std::shared_ptr<DlColorSource> ReusableFragmentShader::shader(
DlImageSampling sampling) {
// Sampling options are ignored, since sampling options don't make sense for
// generative shaders.
auto sk_shader = program_->runtime_effect()->makeShader(
uniform_data_, samplers_.data(), samplers_.size());
return DlColorSource::From(sk_shader);
}

ReusableFragmentShader::~ReusableFragmentShader() = default;

} // namespace flutter
32 changes: 32 additions & 0 deletions lib/ui/painting/fragment_shader.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -19,6 +20,8 @@

namespace flutter {

class FragmentProgram;

class FragmentShader : public Shader {
DEFINE_WRAPPERTYPEINFO();
FML_FRIEND_MAKE_REF_COUNTED(FragmentShader);
Expand All @@ -36,6 +39,35 @@ class FragmentShader : public Shader {
std::shared_ptr<DlColorSource> 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);

std::shared_ptr<DlColorSource> shader(DlImageSampling) override;

private:
ReusableFragmentShader(fml::RefPtr<FragmentProgram> program,
uint64_t float_count,
uint64_t sampler_count);

fml::RefPtr<FragmentProgram> program_;
sk_sp<SkData> uniform_data_;
// TODO(): This will probably need to be changed to
// std::shared_ptr<DlColorSource> when we add Impeller support.
std::vector<sk_sp<SkShader>> samplers_;
size_t float_count_;
};

} // namespace flutter

#endif // FLUTTER_LIB_UI_PAINTING_FRAGMENT_SHADER_H_
45 changes: 35 additions & 10 deletions testing/dart/fragment_shader_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,46 @@ void main() async {
await _expectShaderRendersGreen(shader);
});

test('blue-green image renders green with reusable shader', () 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);
});

test('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);

try {
program.fragmentShader()..setSampler(1, imageShader);
fail('Unreachable');
} catch (e) {
expect(e, contains('Sampler index out of bounds'));
}
});

test('shader with uniforms renders correctly', () async {
final FragmentProgram program = await FragmentProgram.fromAsset(
'uniforms.frag.iplr',
);

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 FragmentShader shader = program.fragmentShader()
..floatUniforms[0] = 0.0
..floatUniforms[1] = 0.25
..floatUniforms[2] = 0.75
..floatUniforms.fillRange(3, 6, 0.0)
..floatUniforms[6] = 1.0;

final ByteData renderedBytes = (await _imageByteDataFromShader(
shader: shader,
Expand Down

0 comments on commit 8873298

Please sign in to comment.