Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Conversation

@chriscraws
Copy link
Contributor

@chriscraws chriscraws commented Aug 28, 2021

FragmentShader now implements hashCode and the == operator.

flutter/flutter#89082

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide and the C++, Objective-C, Java style guides.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. See testing the engine for instructions on
    writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the CLA.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@chriscraws chriscraws requested review from clocksmith and flar August 28, 2021 01:05
@google-cla google-cla bot added the cla: yes label Aug 28, 2021
'FragmentShader floatUniforms size: ${floatUniforms.length} must match given shader uniform count: $_uniformFloatCount.');
}
_update(floatUniforms);
_hashCode = hashValues(_spirvHash, hashList(floatUniforms));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How expensive are these precalculated hash values? Can this be done lazily? (If we want to implement == correctly then we'd need to save the buffers any way.

@chriscraws chriscraws requested a review from flar September 2, 2021 01:42
Copy link
Contributor

@flar flar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel that for correctness we should either drop back to using identity for == or go all the way and keep the data around for eventual array comparison.

From some of the comments it appears that developers might be pre-disposed to creating and saving these shaders for reuse so the opportunity cost of requiring identity == for the optimizations might be favorable? If so, then let's just go back to identity ==.

Another point to consider is that if apps frequently recreate the objects with different uniforms on most all "reuses" then a full-on comparison might be wasted effort?

(Also, consider that we only recently implemented == for some of the shader/filter objects in the past couple of years, so there is a history of identity comparisons in the framework).

If you agree, then perhaps turn this PR into an implementation comment that a full == implementation would be expensive.

On another front, one of the tasks on the DisplayList follow-up list (flutter/flutter#85737) is to stop turning these Flutter objects immediately into Skia objects at the native level, but only do so on the fly at the point of first use. If that gets done, then we might be keeping these arrays around at the native level and be able to implement a native == implementation that does array comparisons.

Also, I'm curious how large these arrays are in practice? Do we have any data on how much shader language info apps would tend to have?

@flar
Copy link
Contributor

flar commented Sep 7, 2021

I notice that there is an update method for the uniforms. This would create problems for the framework which tends to assume that Shader/Filter objects are immutable. These objects get stored in the framework and reused on subsequent frames. If their values can change without any reconstruction of the associated widgets then that could cause a problem.

I would consider using a constructor to construct a new shader from an existing shader which reuses its program, but contains new uniforms.

@flar
Copy link
Contributor

flar commented Sep 7, 2021

Tagging @Hixie on the issue of FragmentShader objects implementing the mutable method "update(new uniform values)" and how that might break assumptions in the framework.

@Hixie
Copy link
Contributor

Hixie commented Sep 7, 2021

@flar is correct that Shaders should be immutable.

Separate from this, there's the extra issue that you can't implement hashCode and operator== on a mutable class in any case. That's just semantically incorrect. Equality of objects must be consistent in time.

@flar
Copy link
Contributor

flar commented Sep 7, 2021

My suggestion for hashcode/== would be to back off to identity implementations for now and to change the update() method to any of the following:

  • FragmentShader withUniforms(...uniforms...) method
  • FragmentShader.withUniforms(FragmentShader original, ...uniforms...) factory (alongside existing constructor, or a constructor that only provides the program and assumes default uniforms)
  • Break the Spirv encoder out into a new object and make the constructor FragmentShader(FragmentProgram, ...uniforms...)
  • Only provide the current constructor, but then you can't amortize the translation of the Spirv program.

Note that if you break the program out into a separate object then hashcode/== could be implemented with identity on the program and array comparison on the uniforms. This would assume that developers would cache the program for reuse since it has a (potentially) expensive translation and so identity hash and == would work well for it, but if they frequently change the uniforms and the uniform arrays are small, then keeping them stored in the Dart object would allow us to hash and compare them directly.

@flar
Copy link
Contributor

flar commented Sep 7, 2021

(Sorry for so many edits to my last comment. I'm done with the rewrites now.)

Another way to view this is that things like ImageFilter.blur are specifying a specific predetermined program and giving it new values that tend to be implemented as uniforms. ImageShader is a specific program that only needs an image to bind as the source pixels and a few parameters that get turned into uniforms for that program.

As such, FragmentShader(Program, ...uniforms...) becomes an instantiation of a specific program specified elsewhere (the Program constructor) with a given set of uniforms. This then mirrors the built-in GPU-shader-based classes better.

Also, one could potentially replace the FragmentShader constructor with FragmentProgram.createShader(...uniforms...)

@flar
Copy link
Contributor

flar commented Sep 7, 2021

At this point I'm leaning strongly towards having FragmentProgram be its own class and towards having it be a factory class for the Shader objects themselves instead of a FragmentShader class with a constructor.

This would help steer the developer towards considering the Program as something they create and reuse (since it is a factory class), and we could implement == on the constructed shaders easily by remembering the array of uniforms.

Shader FragmentProgram.createShader(...uniforms...) returns an internal subclass of Shader with a handle to the program and the remembered uniforms and implements hashcode/== fully.

Thoughts on this style @Hixie ?

@Hixie
Copy link
Contributor

Hixie commented Sep 7, 2021

Seems reasonable.

@chriscraws
Copy link
Contributor Author

Thank you both for looking into this, I appreciate the examples!

It sounds like using a factory pattern is the way to go given that Shader instances must be immutable, I wasn't previously aware of that - makes sense. I can draft a new PR for that, I agree with the last style given by @flar of a FragmentProgram or similar with createShader and a private Shader implementation.

In the meantime, I think it would make sense to close this PR.

Separate from this, there's the extra issue that you can't implement hashCode and operator== on a mutable class in any case. That's just semantically incorrect. Equality of objects must be consistent in time.

That makes sense - is this written down somewhere? I was following the Dart documentation for hashCode and operator== which mentions that equality can change only "if one of the objects was modified".

@Hixie
Copy link
Contributor

Hixie commented Sep 8, 2021

https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#common-boilerplates-for-operator--and-hashcode mentions "If you do override equality, you should use @immutable on the class hierarchy in question" but in general the reason you don't want operator== or hashCode to ever change during the lifetime of an object is that they mostly exist to allow the objects to be put in Maps or Sets, and if they change identity while in a Map or Set things will just break horribly (e.g. you won't be able to find the instance anymore since its hash code will be different).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants