WPF 中的 OpenGL 控件崩溃并出现 OOM 异常
本文关键字:OOM 异常 崩溃 中的 OpenGL 控件 WPF | 更新日期: 2023-09-27 18:34:01
每次打开包含 OpenGL 控件的模块时,都会在 WPF PRISM 应用程序中调用以下代码段。
代码"bmp。Save(ms, ImageFormat.Bmp);" 在连续打开模块 30 到 40 次后出现 OOM 异常。
我们通过使用内存分析工具删除幸存的对象来优化代码。我们还能在这里做什么?
public BitmapImage SaveToImage(bool automaticOrientation = false)
{
this.MakeCurrent();
// 1. Render the scene offscreen and get framebuffer bytes
// -------------------------------------------------------------------------------
byte[] bmpBuffer = this.renderer.GetOffscreenBytes(automaticOrientation);
// 2. Save the bytes to a memory stream
// -------------------------------------------------------------------------------
using (MemoryStream ms = new MemoryStream())
{
using (Bitmap bmp = new Bitmap(GLCore.HWSETTINGS.OFFBUFF_SIZE, GLCore.HWSETTINGS.OFFBUFF_SIZE, PixelFormat.Format24bppRgb))
{
BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, bmp.PixelFormat);
Marshal.Copy(bmpBuffer, 0, bmpData.Scan0, bmpBuffer.Length);
bmp.UnlockBits(bmpData);
bmp.Save(ms, ImageFormat.Bmp);
//NEW
bmp.Dispose();
}
// 3. Save the memory stream to a BitmapImage and return-it
// -------------------------------------------------------------------------------
ms.Position = 0;
BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.StreamSource = ms;
bi.EndInit();
// NEW
bi.Freeze();
// END NEW
return bi;
}
}
...
public bool MakeCurrent()
{
if (this.context.deviceContext == IntPtr.Zero || this.context.renderingContext == IntPtr.Zero) return false;
// 1. Exit if we can't activate the rendering context
// -------------------------------------------------------------------------------
if (!Wgl.wglMakeCurrent(this.context.deviceContext, this.context.renderingContext))
{
MessageBox.Show("Can not activate the GL rendering context.", "Fatal Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
Environment.Exit(-1);
}
return true;
}
...
public byte[] GetOffscreenBytes(bool automaticOrientation = false)
{
// 1. Save current orientation
// -------------------------------------------------------------------------------
GLSceneOrientation currentOrientation = GLCore.SCENESETTINGS.ORIENTATION;
// 2. Reset orientation if necessary. Otherwise copy zoom to the offscreen
// buffer's viewport in case we are using orthogonal ptojection
// -------------------------------------------------------------------------------
if (automaticOrientation)
{
GLCore.SCENESETTINGS.ORIENTATION.rotX = GLCore.SCENESETTINGS.ORIENTATION.rotY = .0f;
GLCore.SCENESETTINGS.ORIENTATION.zoom = this.offscreenRenderBuff.Viewport.Zoom = 1.0f;
}
else
this.offscreenRenderBuff.Viewport.Zoom = this.viewport.Zoom;
// 3. Bind offscreen buffer
// -------------------------------------------------------------------------------
this.offscreenRenderBuff.Bind();
// 4. Perform rendering
// -------------------------------------------------------------------------------
Render();
// 5. Copy result of rendering to a byte array. Use a standard size of 1024*1024
// -------------------------------------------------------------------------------
byte[] bmpBuffer = new byte[GLCore.HWSETTINGS.OFFBUFF_SIZE * GLCore.HWSETTINGS.OFFBUFF_SIZE * 3];
Gl.glReadPixels(0, 0, GLCore.HWSETTINGS.OFFBUFF_SIZE, GLCore.HWSETTINGS.OFFBUFF_SIZE, Gl.GL_BGR, Gl.GL_UNSIGNED_BYTE, bmpBuffer);
// 6. Unbind offscreen buffer
// -------------------------------------------------------------------------------
this.offscreenRenderBuff.Unbind();
// 7. Restore orientation
// -------------------------------------------------------------------------------
GLCore.SCENESETTINGS.ORIENTATION = currentOrientation;
// 8. Return byte array
// -------------------------------------------------------------------------------
return bmpBuffer;
}
首先,我注意到您正在从opengl字节数组创建一个System.Drawing.Bitmap
,然后将其转换为System.Windows.Media.Imaging.BitmapImage
。 为什么不跳过中间步骤,而是做一个WriteableBitmap
呢?
public static BitmapSource CreateBitmap(int width, int height, byte[] bmpBuffer, PixelFormat pixelFormat)
{
var bitmap = new WriteableBitmap(width, height, 96, 96, pixelFormat, null);
bitmap.WritePixels(new Int32Rect(0, 0, width, height), bmpBuffer, bitmap.BackBufferStride, 0);
bitmap.Freeze();
return bitmap;
}
对于pixelFormat
,您将通过PixelFormats.Bgr24
(如果没有,则PixelFormats.Rgb24
),我相信这对应于Gl.GL_BGR
. 这更直接,理论上应该为中间步骤使用更少的内存。
接下来,创建位图后,您将如何处理位图? 是将它们保存到磁盘,然后删除对它们的所有引用,还是将它们保留在内存中列表中? 由于 WPF 位图不是一次性的,因此释放它们使用的内存的唯一方法是不再在任何位置引用它们。
我能够使用您的输入解决问题,因此首先非常感谢大家:)
@dbc:你是对的,我是错的;) => 事实证明,显示生成的位图的 WPF 图像在退出模块时未正确发布。如果我将其源设置为 null,然后将图像本身设置为 null,那就没有问题了。
这里是工作实现:
public BitmapSource SaveToImage(bool automaticOrientation = false)
{
// Set OpenGL context
// -------------------------------------------------------------------------------
this.MakeCurrent();
// Render the scene offscreen and get framebuffer bytes
// -------------------------------------------------------------------------------
byte[] bmpBuffer = this.renderer.GetOffscreenBytes(automaticOrientation);
// Convert the bytes to a Bitmap source
// -------------------------------------------------------------------------------
BitmapSource srcBMP = CreateBitmap(GLCore.HWSETTINGS.OFFBUFF_SIZE, GLCore.HWSETTINGS.OFFBUFF_SIZE, bmpBuffer, System.Windows.Media.PixelFormats.Bgr24);
bmpBuffer = null;
return srcBMP;
}
public static BitmapSource CreateBitmap(int width, int height, byte[] bmpBuffer, System.Windows.Media.PixelFormat pixelFormat)
{
var bitmap = new WriteableBitmap(width, height, 96, 96, pixelFormat, null);
bitmap.WritePixels(new Int32Rect(0, 0, width, height), bmpBuffer, bitmap.BackBufferStride, 0);
bitmap.Freeze();
return bitmap;
}
在保存 OpenGL 控件的模块(在 Windows 窗体控件主机中)和 WPFImage 的 Dispose 函数中:
public void Dispose()
{
this.GLControl.Release(); // Releases OpenGL resources on the GPU
this.GLControlHost.Dispose(); // Needed otherwise the child control is retained
this.GLImage.Source = null; // Needed to avoid memory leak
this.GLImage = null; //
}
周末愉快!