Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

color banding on android #1065

Closed
mvayngrib opened this issue Aug 3, 2019 · 58 comments
Closed

color banding on android #1065

mvayngrib opened this issue Aug 3, 2019 · 58 comments

Comments

@mvayngrib
Copy link

Bug

i'm seeing color banding on some android devices. Below is a screenshot from a OnePlus 6t.
image

Environment info

React native info output:

  React Native Environment Info:
    System:
      OS: macOS 10.14.5
      CPU: (12) x64 Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
      Memory: 632.31 MB / 16.00 GB
      Shell: 5.0.2 - /usr/local/bin/bash
    Binaries:
      Node: 8.10.0 - ~/.nvm/versions/node/v8.10.0/bin/node
      Yarn: 1.17.3 - ~/.nvm/versions/node/v8.10.0/bin/yarn
      npm: 3.10.10 - ~/.nvm/versions/node/v8.10.0/bin/npm
      Watchman: 4.9.0 - /usr/local/bin/watchman
    SDKs:
      iOS SDK:
        Platforms: iOS 12.4, macOS 10.14, tvOS 12.4, watchOS 5.3
      Android SDK:
        API Levels: 26, 27, 28
        Build Tools: 26.0.3, 27.0.3, 28.0.3, 29.0.0
        System Images: android-26 | Google APIs Intel x86 Atom, android-28 | Intel x86 Atom, android-28 | Google APIs Intel x86 Atom, android-29 | Google APIs Intel x86 Atom
    IDEs:
      Android Studio: 3.4 AI-183.6156.11.34.5522156
      Xcode: 10.3/10G8 - /usr/bin/xcodebuild
    npmPackages:
      react: 16.8.3 => 16.8.3 
      react-native: 0.59.2 => 0.59.2 

Library version: 9.5.3

Steps To Reproduce

see branch mv/svg here: https://github.com/mvayngrib/gradientbanding/tree/mv/svg

Describe what you expected to happen:

smooth gradient

Reproducible sample code

see branch mv/svg here: https://github.com/mvayngrib/gradientbanding/tree/mv/svg

here's the main code though (colors hardcoded):

import React from 'react'
import { Dimensions } from 'react-native'

import Svg, { Defs, LinearGradient, Stop, Rect } from 'react-native-svg'
const { width } = Dimensions.get('window')

const SvgBg = (props) => (
	<Svg width={width} height="100%" {...props}>
		<Defs>
			<LinearGradient id="a" x1="50%" x2="50%" y1="0%" y2="100%">
				<Stop offset="0%" stopColor="#0066ff" />
				<Stop offset="100%" stopColor="#00aaff" />
			</LinearGradient>
		</Defs>
		<Rect fill="url(#a)" width={width} height="100%" />
	</Svg>
)

export default React.memo(SvgBg)
@msand
Copy link
Collaborator

msand commented Aug 3, 2019

Can you try adding this to your MainActivity.java

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        Window window = getWindow();
        window.setFormat(PixelFormat.RGBA_8888);
        super.onCreate(savedInstanceState);
    }

I'm unable to replicate the issue on either emulator or real device.

@mvayngrib
Copy link
Author

@msand thanks for the quick response! Unfortunately, I already tried that to no avail :(

@mvayngrib
Copy link
Author

@msand it replicates on a Pixel 3 Android 9 emulator

@msand
Copy link
Collaborator

msand commented Aug 8, 2019

Created a Pixel 3 emulator with Google APIs Intel x86 Atom_64 System Image revision: 9 (system-images;android-28;google_apis;x86_64)
https://dl.google.com/android/repository/sys-img/google_apis/x86_64-28_r09.zip

With this code:

import React, { Component } from 'react';
import { StyleSheet, View, Dimensions } from 'react-native';
import Svg, { Defs, LinearGradient, Stop, Rect } from 'react-native-svg';
const { width } = Dimensions.get('window');

const SvgBg = props => (
  <Svg width={width} height="100%" {...props}>
    <Defs>
      <LinearGradient id="a" x1="50%" x2="50%" y1="0%" y2="100%">
        <Stop offset="0%" stopColor="#0066ff" />
        <Stop offset="100%" stopColor="#00aaff" />
      </LinearGradient>
    </Defs>
    <Rect fill="url(#a)" width={width} height="100%" />
  </Svg>
);

export default class App extends Component {
  render() {
    return (
      <View style={styles.container}>
        <SvgBg />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#F5FCFF',
  },
});

I get this (Emulator screenshot):
Screenshot_1565297553

System screenshot tool:
Screen Shot 2019-08-08 at 23 53 42

Looks quite fine to me?

@mvayngrib
Copy link
Author

@msand hey man, thanks for pursuing this. I do see some banding in the image you posted, but let me see if i can pick two colors that make it even more obvious...give me a couple of minutes

@msand msand added the Missing repro This issue need minimum repro scenario label Aug 8, 2019
@mvayngrib
Copy link
Author

@msand what does #aaa and #bbb look like for you? I see:

image

@msand
Copy link
Collaborator

msand commented Aug 8, 2019

Screenshot_1565300682

@msand
Copy link
Collaborator

msand commented Aug 8, 2019

Screen Shot 2019-08-09 at 0 45 09

@mvayngrib
Copy link
Author

@msand do you see the banding on your image? Moving away from the monitor a bit and scrolling up and down on it helps the eye catch it. Then you can't unsee it...

@msand
Copy link
Collaborator

msand commented Aug 8, 2019

Well, you know, there are only 16 possible colors from #aaaaaa to #bbbbbb

@mvayngrib
Copy link
Author

mvayngrib commented Aug 8, 2019

@msand that's the thing, if i do this with css gradients in Chrome on a OnePlus 6t, then open chrome://flags and set "Force color profile" to "Display P3," the banding disappears, which means the device can handle a wider color range ("wide color gamut")

btw, this flag doesn't seem to change anything on the emulator

@mvayngrib
Copy link
Author

and if you open this on a mac, you'll see no color banding:

<html>
	<style rel="stylesheet">
		.gradient {
			width: 100%;
			height: 100%;
			background: linear-gradient(to bottom, #aaa, #bbb);
		}
	</style>
	<body style="padding:0px;margin:0px;">
		<div class="gradient"></div>
	</body>
</html>

@msand
Copy link
Collaborator

msand commented Aug 8, 2019

Tried changing to RGBA_F16 and use setDither(true)

Screenshot_1565303288

@mvayngrib
Copy link
Author

haha yeah, i tried that too, but i still see banding (in the image u posted too btw)

@msand
Copy link
Collaborator

msand commented Aug 8, 2019

Well, I think making it into png will force it to use a byte per color channel, not sure how to make RGBA_F16 screenshots. Seems chrome has some dithering/noise/turbulence to hide the banding.

@mvayngrib
Copy link
Author

mvayngrib commented Aug 8, 2019

i also tried adding android:colorMode="wideColorGamut" to activity in AndroidManifest.xml, and it helps with some things (like this image - see this article), but not others, like the banding

@mvayngrib
Copy link
Author

mvayngrib commented Aug 8, 2019

@msand do u mean that banding disappears for you on the emulator when u set RGBA_F16 and use setDither(true), but that the screenshot didn't capture it? If so, where exactly did u set it?

@mvayngrib
Copy link
Author

mvayngrib commented Aug 8, 2019

this is a css gradient (in Chrome) on my mac (no banding, even after it's a png):

image

@msand
Copy link
Collaborator

msand commented Aug 8, 2019

No, I think it still has the issue, also tried:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window.setColorMode(COLOR_MODE_WIDE_COLOR_GAMUT);
}
But no luck. I think the LinearGradient implementation only uses RGBA_8888, as it takes int colors. API 29 seems to introduce long colors: https://developer.android.com/reference/kotlin/android/graphics/LinearGradient?hl=en#init_1

@msand
Copy link
Collaborator

msand commented Aug 8, 2019

You'll probably need to use wide-color gamut images, opengl or vulkan: https://developer.android.com/training/wide-color-gamut

@mvayngrib
Copy link
Author

You'll probably need to use wide-color gamut images, opengl or vulkan:

ic, that doesn't sound like a quick fix then

@msand
Copy link
Collaborator

msand commented Aug 9, 2019

You might want to check out https://github.com/googlesamples/android-ndk/blob/ae568ca67d908040fb69aa08d8296ef64d4473c4/display-p3/README.md for how to get a wide color gamut opengl context. Then you'd need to render the gradient using that.

http://www.cs.princeton.edu/~mhalber/blog/ogl_gradient/

If you only have a single rectangular area with a two color linear gradient. Then you only need to render a single quad. Should probably be learnable in a day, as it's among the smallest possible things that can be done using opengl.

http://openglbook.com/

@msand
Copy link
Collaborator

msand commented Aug 9, 2019

I have a gradient with banding rendered in the opengl context now, I replaced ImageViewEngine::DrawFrame in ImageViewEngine.cpp with this:

#define SHADER_HEADER "#version 300 es\n"
#define SHADER_STR(x) #x
void mygl_GradientBackground( float top_r, float top_g, float top_b, float top_a,
                              float bot_r, float bot_g, float bot_b, float bot_a )
{
  glDisable(GL_DEPTH_TEST);

  static GLuint background_vao = 0;
  static GLuint background_shader = 0;

  if (background_vao == 0)
  {
    glGenVertexArrays(1, &background_vao);

    const char* vs_src = (const char*) SHADER_HEADER SHADER_STR
    (
            out vec2 v_uv;
            void main()
            {
              uint idx = uint(gl_VertexID);
              gl_Position = vec4(idx & 1U, idx >> 1U, 0.0, 0.5 ) * 4.0 - 1.0;
              v_uv = vec2( gl_Position.xy * 0.5 + 0.5 );
            }
    );

    const char* fs_src = (const char*) SHADER_HEADER SHADER_STR
    (
            uniform vec4 top_color;
            uniform vec4 bot_color;
            in vec2 v_uv;
            out vec4 frag_color;

            void main()
            {
              frag_color = bot_color + (top_color - bot_color) * v_uv.y;
            }
    );
    GLuint vs_id, fs_id;
    vs_id = glCreateShader( GL_VERTEX_SHADER );
    fs_id = glCreateShader( GL_FRAGMENT_SHADER );
    glShaderSource(vs_id, 1, &vs_src, NULL);
    glShaderSource(fs_id, 1, &fs_src, NULL);
    glCompileShader(vs_id);
    glCompileShader(fs_id);
    background_shader = glCreateProgram();
    glAttachShader( background_shader, vs_id );
    glAttachShader( background_shader, fs_id );
    glLinkProgram(  background_shader );
    glDetachShader( background_shader, fs_id );
    glDetachShader( background_shader, vs_id );
    glDeleteShader( fs_id );
    glDeleteShader( vs_id );
    glUseProgram( background_shader );
  }

  glUseProgram( background_shader );
  GLuint top_color_loc = glGetUniformLocation( background_shader, "top_color" );
  GLuint bot_color_loc = glGetUniformLocation( background_shader, "bot_color" );
  glUniform4f( top_color_loc, top_r, top_g, top_b, top_a );
  glUniform4f( bot_color_loc, bot_r, bot_g, bot_b, bot_a );

  glBindVertexArray( background_vao );
  glDrawArrays(GL_TRIANGLES, 0, 3);
  glBindVertexArray(0);

  glEnable(GL_DEPTH_TEST);
}

/*
 * Draw quad(s) to view texture
 */
void ImageViewEngine::DrawFrame(void) {
  if (display_ == NULL) {
    return;
  }
  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT);
  /*
  mygl_GradientBackground( 1.0, 0.0, 0.0, 1.0,
                           0.0, 1.0, 0.0, 1.0 );
  */
  mygl_GradientBackground( 0.67, 0.67, 0.67, 1.0,
                           0.73, 0.73, 0.73, 1.0 );
  eglSwapBuffers(display_, surface_);
}

@mvayngrib
Copy link
Author

mvayngrib commented Aug 9, 2019

@msand did you add android:colorMode="wideColorGamut" to the <activity> in AndroidManifest.xml?

@msand
Copy link
Collaborator

msand commented Aug 9, 2019

Or, actually, it might be a limitation of my device as well, I don't think I even have any wide color gamut displays available.

@msand
Copy link
Collaborator

msand commented Aug 9, 2019

At least when I ran the original display-p3 example, both images looked the same, so my android doesn't support it at least.

@mvayngrib
Copy link
Author

At least when I ran the original display-p3 example, both images looked the same, so my android doesn't support it at least.

oh, what about on the pixel 3 emulator?

@msand
Copy link
Collaborator

msand commented Aug 9, 2019

@mvayngrib Can you try running the display-p3 example? And if the images look different for you, then try changing the code to render the gradient instead?

@msand
Copy link
Collaborator

msand commented Aug 9, 2019

@mvayngrib The emulators don't have any gpu, I think you can only run the native gpu examples on real devices.

@msand
Copy link
Collaborator

msand commented Aug 9, 2019

Interesting, that should be the only part affecting it as far as I understand at the moment. The gradient colors should be uniformly interpolated between 0 and 1, and the surface should decide the colorspace.

Can you try adding this:

            precision mediump float;

to the fragment shader:

    const char* fs_src = (const char*) SHADER_HEADER SHADER_STR
    (
            precision mediump float;
            uniform vec4 top_color;
            uniform vec4 bot_color;
            in vec2 v_uv;
            out vec4 frag_color;

            void main()
            {
              frag_color = bot_color + (top_color - bot_color) * v_uv.y;
            }
    );

@msand
Copy link
Collaborator

msand commented Aug 9, 2019

@mvayngrib And might want to change v_uv.y to v_uv.x to exaggerate the banding more.

@mvayngrib
Copy link
Author

@msand added that to the fragment shader, not sure what I should have expected, but it's still banding :)

@msand
Copy link
Collaborator

msand commented Aug 9, 2019

Not sure how to proceed at this point. I would need a device with support for it to do more testing.

@mvayngrib
Copy link
Author

@msand makes sense. If you're up for it, I'm happy to jump on video and experiment together on my device

@msand
Copy link
Collaborator

msand commented Aug 9, 2019

I don't have any specific idea to explore atm, but thanks for the offer. I'll need to focus on more work related matters at this point 😄

@msand
Copy link
Collaborator

msand commented Aug 9, 2019

This might be of tangential interest: http://litherum.blogspot.com/2017/07/wide-and-deep-color-in-metal-and-opengl.html
Might be coming up against the practical limit of number of colors that the colorspace and display combination can show. To get rid of the banding at that point might require high quality dithering.

@mvayngrib
Copy link
Author

@msand thanks, i'll have a read! Just fyi, the banding only occurs on android

@lvalentine
Copy link

lvalentine commented Sep 24, 2019

Did you try using the Paint.DITHER_FLAG?

https://developer.android.com/reference/android/graphics/Paint.html#DITHER_FLAG

react-native-svg\android\src\main\java\com\horcrux\svg\RenderableView.java

private boolean setupFillPaint(Paint paint, float opacity) {
    if (fill != null && fill.size() > 0) {
        paint.reset();
        paint.setFlags(Paint.DITHER_FLAG | Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG | Paint.SUBPIXEL_TEXT_FLAG);
        paint.setStyle(Paint.Style.FILL);
        setupPaint(paint, opacity, fill);
        return true;
    }
    return false;
}

I see significantly less banding in testing with a RadialGradient when using that flag.

@mvayngrib
Copy link
Author

@lvalentine just tried, didn't help :(

@msand msand added enhancement good first issue help wanted and removed Missing repro This issue need minimum repro scenario labels Sep 28, 2019
@msand
Copy link
Collaborator

msand commented Nov 5, 2019

@mvayngrib Have you tried setting
android:hardwareAccelerated="false"
as an attribute of the application tag in your AndroidManifest.xml
https://developer.android.com/guide/topics/graphics/hardware-accel#controlling
Seems this helps:
Screenshot 2019-11-05 at 22 31 39

@mvayngrib
Copy link
Author

@msand thanks for the suggestion, but it doesn't seem to help on the device (testing on oneplus 6t)

@msand
Copy link
Collaborator

msand commented Nov 6, 2019

@mvayngrib Hmm, perhaps some other change I tried in addition to that then, can you try the latest commit from the dither branch: ee894b2

@msand
Copy link
Collaborator

msand commented Nov 6, 2019

Sorry, wrong commit pushed: ee894b2

@mvayngrib
Copy link
Author

@msand you did it! on ee894b2 with android:hardwareAccelerated="false" I don't see banding!

@mvayngrib
Copy link
Author

@msand do u mind explaining why android:hardwareAccelerated="false" is necessary? Is it possible to get rid of banding without it? Thanks!

@msand
Copy link
Collaborator

msand commented Nov 6, 2019

@mvayngrib Great to hear :) Some (all?) devices when using hardware acceleration, do not apply any dithering algorithm, partially to make things faster, partially because it can be tricky to do well with a gpu, and partially because of laziness I guess. Probably main reason is performance. Would you mind attempting to reduce those changes to the smallest possible ones which still allow it to work the way you wish?

@mvayngrib
Copy link
Author

@msand yep, let me play with it later today and get back to u

@mvayngrib
Copy link
Author

@msand these are the minimal changes that it takes to get it to work on my device (plus android:hardwareAccelerated="false"): 0cc5330...ExodusMovement:mv/dither

@stale
Copy link

stale bot commented Feb 7, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. You may also mark this issue as a "discussion" and I will leave this open.

@stale stale bot added the stale label Feb 7, 2020
@stale
Copy link

stale bot commented Feb 14, 2020

Closing this issue after a prolonged period of inactivity. Fell free to reopen this issue, if this still affecting you.

@stale stale bot closed this as completed Feb 14, 2020
@gordonel
Copy link

gordonel commented Oct 22, 2020

Hey @msand. I'd like to resurface this, if possible. I'm not a software engineer, but I do know a thing or two about digital colour.

I did some research on this by taking screenshots of the same app in the same state but on iOS and Android. All screenshots were PNG, which is a lossless format. Here's what I have found out:

  1. iOS gradients definitely have dithering patterns in them while Android ones do not
  2. iOS uses Display P3 colour primaries, while Android uses sRGB. However, this only means that actual red, green and blue chromaticity values are different for those displays. #aabbcc of actual pixel value on iOS will still equal #aabbcc of actual pixel value on Android
  3. iOS uses 16-bit integer buffer, but it's probably because iOS displays always output in 10-bit mode, so you need 16-bit integer to display 10 significant bits. Android, however, can switch the display between 8, 10, and FP16 if the graphics driver says it so (my Pixel 3 has a 10-bit display, but 10 bits are used mostly for HDR output)
  4. The colours on iOS in my experiment, however, don't have 10 or 16 significant bits. I truncated the iOS screenshot from 16 bit int to 8 bit int and had absolutely no change in actual pixel value, leading me to believe that the image I see only has 8 significant bits in it
  5. The actual pixel values between Android and iOS for an app in the same state are different, leading me to believe that there's a broken bitmap somewhere. This applies to non-gradient SVGs and text colour in my experiments. In other words, #aabbcc on Android doesn't equal #aabbcc on iOS

The solution here, in my humble opinion, is to add dithering to Android and don't bother with increased bit depth or wide colour gamut. React native has no support for either of these and it uses 8-bit colour with sRGB primaries anyway unless you wanna mess with OpenGL or Vulkan. Also, something tells me it's RN's Android colour bitmap that might be borked. IDK if you do bitmapping here

Makes sense? If not, I'll be happy to clarify anything

@gordonel
Copy link

Also, my device (Pixel 3) can dither with hardware acceleration turned on, apparently. It would be great to have hardware acceleration exposed for dither controls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants