mono / SkiaSharp

SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library. It provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.
MIT License
4.56k stars 543 forks source link

[Android] Uncapped framerates (300fps) on SKGLView.PaintSurface after updating package #533

Open Suprndm opened 6 years ago

Suprndm commented 6 years ago

Description

Application framerate went from a capped 60fps to a ~300fps.

Upgrading to SkiaSharp.Views.Forms version="1.60.1" from version="1.59.3" Using SKGLView with HasRenderLoop to true,

Expected behavior

Like in version="1.59.3" framerate should not go higher than 60fps

Actual behavior

Framerate is not fixed, going from 60fps to 1000fps. It modifies all the animations of the application that are frame based.

Basic Information

Framerate is based on the elapsedTime between each PaintSurface event.

mattleibow commented 6 years ago

This never was a feature. Maybe they changed something in Forms... Did you update to a newer forms version?

SkiaSharp will always render as fast as possible, with as many frames as possible.

Did you maybe change devices, OS versions or platform?

I will have a look, but I don't think anything significant has changed.

mattleibow commented 6 years ago

I see the change, and I think it has to do with the fact that we are no longer using the old GLSurfaceView to render the content, but the new TextureView.

Unfortunately, I can't seem to find where the original code limits the framerate. Also, I am not sure that we want to limit the frame rate.

I will look further and see what I can do.

Suprndm commented 6 years ago

Ok, I see

I use Skiasharp as a game renderer. And the physical Engine of the game is based on the frame rate, which is wrong ! It should have a separated refresh rate based on time.

I knew I had to make this change anytime soon. Now is just the perfect timing to do it =)

I was curious about what produced this change. Thank you for the hints of answers. The current app impacted by this change makes a massive use of SKCanvas.DrawBitmap(...). It does mean that the perfomances of SkiaSharp have increased, which is very good !

As it is not really an issue, should I close now ?

mattleibow commented 6 years ago

Let's leave it open, and I will try and find a real reason the GLSurfaceView has a frame rate limit, but the TextureView does not.

Just for reference later on in life:

This is the code I used the measure the FPS:

Stopwatch sw = Stopwatch.StartNew();
TimeSpan last;

private void OnPaint(object sender, SKPaintGLSurfaceEventArgs e)
{
    var c = sw.Elapsed;
    var ts = c - last;
    last = c;

    var fps = 1.0 / (ts.TotalSeconds);
    var fpsString = fps.ToString("0,000.00");

    Log.Debug("FPS", fpsString);
}
mattleibow commented 6 years ago

Also, with regards to the frame rate... It is never good to use a "constant" frame rate - either an assumption or using some limiter API. There is never a guarantee because another app may suddenly use the CPU/GPU, or particular logic in a particular frame may take longer to run, or even the time it takes to swap buffers may change between frames due to reasons.

mattleibow commented 4 years ago

I really need to look at this. Noticing 1.2K FPS in some emulators.

A simple FPS snippet: https://gist.github.com/mattleibow/4eb9cd26bcaaaefd5b2f7499d21bf4bd

entdark commented 4 years ago

I mean, isn't that good to have the FPS uncapped. As a user of SkiaSharp for backend game renderer as well, I find it quiet good improvement. Also if you are looking for the cause of capped vs uncapped FPS, then the explanation could be simple: GLSurfaceView probably uses V-Sync (*glSwapInterval call), when TextureView doesn't use that.

mattleibow commented 4 years ago

Having uncapped is nice, but a heavy drain. Also, it is no point having the updates faster than the screen - that wastes resources.

In the case of "capping" it is usually with regards to the screen refresh rate.

mattleibow commented 4 years ago

We might be able to look at Choreographer: https://stackoverflow.com/questions/55028881/how-to-determine-device-screen-refresh-start-stop-on-mobile-devices

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.Choreographer;

private class DisplayFrameSync extends Thread implements Choreographer.FrameCallback {
    private volatile Handler signal;
    private Consumer<Long> update;

    public DisplayFrameSync(Consumer<Long> update) {
        super();

        this.update = update;
    }

    @Override
    public void run() {
        setName("DisplayFrameSyncThread");

        Looper.prepare();

        signal = new Handler() {
            public void handleMessage(Message msg) {
                Looper.myLooper().quit();
            }
        };

        Choreographer.getInstance().postFrameCallback(this);

        Looper.loop();
        Choreographer.getInstance().removeFrameCallback(this);
    }

    public void signal() {
        if (this.signal != null) {
            this.signal.sendEmptyMessage(0);
            this.signal = null;
        }
    }

    @Override
    public void doFrame(long timeNano) {
        if (this.update != null) {
            this.update.accept(timeNano);
        }

        Choreographer.getInstance().postFrameCallback(this);
    }
}
kestlerio commented 3 years ago

I am getting

Maybe this could also be useful: https://developer.android.com/games/sdk/frame-pacing It mentions Choreographer as good but sub-optimal and suggests the Frame Pacing Library (which uses Choreographer but plus some additional optimizations).

A little bit of a luxury problem, but as you said, it drains the battery unnecessarily.

Matthew, can you point me into some code places in SkiaSharp where the Choreographer or even the Frame Pacing Library has to be put? I am quite unexperienced with this whole rendering thing and SkiaSharp, but would maybe have some time to experiment. Or would it have to be put into Xamarin.Forms even? 😮

Anyway, this whole SkiaSharp is really an excelent piece of work! 🐱‍💻

Oribow commented 3 years ago

Frame Pacing Library is going to be a headache to add to a Xamarin project, being a C++ library. Has anyone any experience with that? I know its possible, but probably not worth the hassle?

I wonder how bad just using the choreographer is. Android calls it suboptimal, but what is suboptimal really? ;)

I've also looked at how monogame handles it frame pacing. [https://github.com/MonoGame/MonoGame/blob/fca271a296f0d2ebeec817a07c67dfd43139659f/MonoGame.Framework/Game.cs](Mono Game) They just use a timer setup. Could be enough?

Its hard to find information on this. I guess not many people use skia with a renderloop.

taublast commented 8 months ago

Would say Skiasharp not capping anything out-of-the box is rather a good thing. The more freedom we have the more we can create. For capping FPS i could suggest the following code:

// can exec this globally at app startup. 
// every canvas can subscribe then to `OnFrame`.

 protected static void SetupFrameLooper() 
 {
     Tasks.StartDelayed(TimeSpan.FromMilliseconds(1), async () =>
     {
         await StartFrameLooperAsync(CancellationToken.None);
     });
 }

 protected static async Task StartFrameLooperAsync(CancellationToken cancellationToken)
 {
     var frameStopwatch = new Stopwatch();  
     var loopStopwatch = Stopwatch.StartNew();  
     long lastFrameEnd = loopStopwatch.ElapsedMilliseconds;
     var targetIntervalMs = 1000.0 / 120.0; // target fps

     while (!cancellationToken.IsCancellationRequested)
     {
         frameStopwatch.Restart(); // Start measuring frame execution time

         // Render DrawnView
         OnFrame?.Invoke(0);

         frameStopwatch.Stop();  

         var frameExecutionTimeMs = frameStopwatch.Elapsed.TotalMilliseconds;
         var elapsedTimeSinceLastFrame = loopStopwatch.ElapsedMilliseconds - lastFrameEnd;
         var timeToWait = targetIntervalMs - elapsedTimeSinceLastFrame - frameExecutionTimeMs;

         if (timeToWait > 0)
             Thread.Sleep(TimeSpan.FromMilliseconds(timeToWait));

         lastFrameEnd = loopStopwatch.ElapsedMilliseconds;
     }
 }

In your callback you could check if the frame is dirty/other checks and invalidate the canvas in a proper way.