jjx*_*tra 6 performance android drawing opengl-es textureview
我试图TextureView在Android上使用绘图/绘画应用程序.我想支持高达4096x4096像素的绘图表面,这对于我的最小目标设备(我用于测试)似乎是合理的,这是一款具有漂亮的四核CPU和2GB内存的Google Nexus 7 2013.
我的一个要求是我的视图必须在一个允许它放大和缩小的视图中,这是我编写的所有自定义代码(想想来自iOS的UIScrollView).
我尝试过使用TextureViewOnDraw 的常规View(不是),性能非常糟糕 - 每秒不到1帧.即使我Invalidate(rect)只用改变的矩形调用,也会发生这种情况.我尝试关闭视图的硬件加速,但没有渲染,我假设因为4096x4096对于软件来说太大了.
然后我尝试使用TextureView并且性能稍微好一点 - 大约每秒5-10帧(仍然很糟糕但更好).用户绘制一个位图,然后使用后台线程将其绘制到纹理中.我正在使用Xamarin,但希望代码对Java人员有意义.
private void RunUpdateThread()
{
try
{
TimeSpan sleep = TimeSpan.FromSeconds(1.0f / 60.0f);
while (true)
{
lock (dirtyRect)
{
if (dirtyRect.Width() > 0 && dirtyRect.Height() > 0)
{
Canvas c = LockCanvas(dirtyRect);
if (c != null)
{
c.DrawBitmap(bitmap, dirtyRect, dirtyRect, bufferPaint);
dirtyRect.Set(0, 0, 0, 0);
UnlockCanvasAndPost(c);
}
}
}
Thread.Sleep(sleep);
}
}
catch
{
}
}
Run Code Online (Sandbox Code Playgroud)
如果我将lockCanvas更改为传递null而不是rect,则性能在60 fps时很好,但TextureView闪烁的内容会被破坏,这是令人失望的.我原本以为它只是在下面使用OpenGL帧缓冲区/渲染纹理,或者至少有一个保留内容的选项.
有没有其他选择在Android中的原始OpenGL中做所有事情,以便在绘制调用之间保留的表面上进行高性能绘图和绘画?
fad*_*den 19
首先,如果您想了解幕后发生的事情,您需要阅读Android图形架构文档.这很长,但如果你真的想要了解"为什么",那就是开始的地方.
关于TextureView
TextureView的工作原理如下:它有一个Surface,它是一个具有生产者 - 消费者关系的缓冲区队列.如果您正在使用软件(Canvas)渲染,则可以锁定Surface,从而为您提供缓冲区; 你借鉴它; 然后你解锁Surface,它将缓冲区发送给消费者.在这种情况下,消费者处于相同的过程中,称为SurfaceTexture或(内部,更恰当地)GLConsumer.它将缓冲区转换为OpenGL ES纹理,然后渲染到View.
如果关闭硬件加速,GLES将被禁用,并且TextureView无法执行任何操作.这就是为什么当你关闭硬件加速时什么都没有. 文档非常具体:"TextureView只能在硬件加速窗口中使用.在软件中渲染时,TextureView不会绘制任何内容."
如果指定脏矩形,则软件渲染器将在渲染完成后将先前内容存储到帧中.我不相信它设置了一个剪辑矩形,所以如果你调用drawColor(),你将填满整个屏幕,然后覆盖这些像素.如果您当前没有设置剪辑矩形,则可能会从这样做中看到一些性能优势.(虽然我没有检查代码.)
脏rect是一个输入输出参数.您在调用时传递所需的矩形lockCanvas(),并允许系统在调用返回之前修改它.(实际上,这样做的唯一原因是,如果没有前一帧或者Surface调整大小,在这种情况下它会扩展它以覆盖整个屏幕.我认为这样可以更直接地处理"我拒绝你的rect"信号.)你需要更新你回来的矩形内的每个像素.你是不是允许改变矩形,你似乎是想你的样品中做的-无论是在脏矩形后,lockCanvas()成功就是你需要什么样的借鉴.
我怀疑脏的直接误操作是你闪烁的根源.遗憾的是,这是一个容易犯的错误,因为lockCanvas() dirtyRectarg 的行为只记录在Surface类本身中.
表面和缓冲
所有Surface都是双缓冲或三缓冲.没有办法解决这个问题 - 你不能同时读写,也不能撕裂.如果您需要可以在需要时修改和推送的单个缓冲区,则需要锁定,复制和解锁该缓冲区,这会在合成管道中创建停顿.为了获得最佳吞吐量和延迟,翻转缓冲区更好.
如果你想要锁定 - 复制 - 解锁行为,你可以自己编写(或找到一个可以执行它的库),它会像系统为你做的那样高效(假设你很好) blit循环).绘制到屏幕外的Canvas并将位图blit,或者绘制到OpenGL ES FBO并对缓冲区进行blit.您可以在Grafika的" 记录GL应用程序 "活动中找到后者的示例,该活动具有一次在屏幕外呈现的模式,然后两次点亮(一次用于显示,一次用于录制视频).
更快的速度等
在Android上绘制像素有两种基本方法:使用Canvas或使用OpenGL.对Surface或Bitmap的Canvas渲染始终在软件中完成,而OpenGL渲染则使用GPU完成.唯一的例外是,在渲染到自定义视图时,您可以选择使用硬件加速,但这在渲染到SurfaceView或TextureView的Surface时不适用.
绘图或绘画应用程序可以记住绘图命令,或只是将像素放在缓冲区并将其用作其内存.前者允许更深的"撤销",后者更简单,并且随着渲染内容的增长而具有越来越好的性能.听起来你想做后者,所以从屏幕外的blitting是有道理的.
对于GLES纹理,大多数移动设备的硬件限制为4096x4096或更小,因此您无法将单个纹理用于任何更大的纹理.您可以查询大小限制值(GL_MAX_TEXTURE_SIZE),但最好使用大小合适的内部缓冲区,并只渲染适合屏幕的部分.我不知道Skia(Canvas)的限制是什么,但我相信你可以创建更大的Bitmaps.
根据您的需要,SurfaceView可能比TextureView更好,因为它避免了中间GLES纹理步骤.您在Surface上绘制的任何内容都将直接转到系统合成器(SurfaceFlinger).这种方法的缺点是,由于Surface的消费者不在进行中,因此View系统没有机会处理输出,因此Surface是一个独立的层.(对于绘图程序,这可能是有益的 - 正在绘制的图像位于一个图层上,您的UI位于顶部的单独图层上.)
FWIW,我没有查看代码,但Dan Sandler的Markers应用程序可能值得一看(源代码在这里).
更新我放弃了TextureView,现在使用OpenGL视图,我在其中调用glTexSubImage2D来更新渲染纹理的更改部分。
旧答案 我最终将TextureView平铺在4x4网格中。根据每帧的脏矩形,我刷新相应的 TextureView 视图。任何未更新该帧的视图我都将其称为“无效”。
某些设备(例如 Moto G 手机)存在双缓冲一帧损坏的问题。您可以通过在父视图调用 onLayout 时调用 lockCanvas 两次来解决此问题。
private void InvalidateRect(int l, int t, int r, int b)
{
dirtyRect.Set(l, t, r, b);
foreach (DrawSubView v in drawViews)
{
if (Rect.Intersects(dirtyRect, v.Clip))
{
v.RedrawAsync();
}
else
{
v.Invalidate();
}
}
Invalidate();
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
for (int i = 0; i < ChildCount; i++)
{
View v = GetChildAt(i);
v.Layout(v.Left, v.Top, v.Right, v.Bottom);
DrawSubView sv = v as DrawSubView;
if (sv != null)
{
sv.RedrawAsync();
// why are we re-drawing you ask? because of double buffering bugs in Android :)
PostDelayed(() => sv.RedrawAsync(), 50);
}
}
}
Run Code Online (Sandbox Code Playgroud)