How to get total GPU RAM (over 4gb memory)? - c#

I am trying to get total GPU memory value. I tried with WMI but it returns not higher value than 4gb.
After reading few articles I found out Cudafy.
public static ulong GetGpuMemory()
{
GPGPU gpu = CudafyHost.GetDevice(CudafyModes.Target, CudafyModes.DeviceId);
var c = gpu.GetDeviceProperties(true);
var p = c.TotalMemory;
return p;
}
For some reason it is returning max ulong value.
Any ideas?
Do you know any other way to get correct GPU memory value from both - NVIDIA and AMD GPUs? What about taking hex value from registry and then converting it? I tried to write some code but without success. Thanks for any help in advance

This should work:
using Microsoft.Win32;
...
long getvideomem()
{
try
{
using (RegistryKey key = Registry.LocalMachine.OpenSubKey(#"SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0000"))
{
if (key != null)
{
object o = key.GetValue("HardwareInformation.qwMemorySize");
if (o != null)
return (long)o;
}
}
}
catch { }
return 0;
}
Multiple GPU systems would need some tweaks however.

Related

When using System.IO.Stream, how can I avoid OutOfMemoryException?

I am making a scanning component, but when I set a high resolution for the document (600 dpi), I tend to get System.OutOfMemoryException after just 1 or 2 scans.
My code is as follows
public ScannedImage SaveScannedImage(DataTransferredEventArgs e)
{
if (e == null) throw new IOException();
BitmapSource fullResImage;
using (var fullResImageStream = e.GetNativeImageStream())
{
fullResImage = fullResImageStream.ConvertToWpfBitmap(e.ImageInfo.ImageWidth, e.ImageInfo.ImageLength);
}
BitmapSource lowResImage;
using (var lowResImageStream = e.GetNativeImageStream())
{
lowResImage = lowResImageStream.ConvertToWpfBitmap(800, 0);
}
return new ScannedImage(lowResImage, fullResImage);
}
It is usually happening at the
using (var lowResImageStream = e.GetNativeImageStream())
Help would be much appreciated.
What you see may be caused by large object heap (LOH) fragmentation.
That is hard to avoid, but you can compact the LOH explicitely.
GCSettings.LargeObjectHeapCompactionMode =
GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
Also, make sure you run as a 64 bit process. Turn of the "Prefer 32 bit" option if it is on.
For more information, you might want to read
https://msdn.microsoft.com/en-us/library/system.runtime.gcsettings.largeobjectheapcompactionmode.aspx
https://blogs.msdn.microsoft.com/ericlippert/2009/06/08/out-of-memory-does-not-refer-to-physical-memory/

Total Physical Memory retrieved from Win32_ComputerSystem doesn't match with DirectX tool value

I am trying to retrieve the total physical memory available value of my machine through Win32_OperatingSystem class in C#. Below is how I am retrieving this value.
ManagementClass mc = new ManagementClass("Win32_ComputerSystem");
ManagementObjectCollection moc = mc.GetInstances();
if (moc.Count != 0)
{
foreach (ManagementObject MO in mc.GetInstances())
{
try
{
computerSystemDetails["TotalPhysicalMemory"] = (MO["TotalPhysicalMemory"] == null) ? new string[] { } : new string[] { MO["TotalPhysicalMemory"].ToString() };
}
catch (Exception ex)
{
logger.Error("GetSystemDetails", "Error occurred when retrieving the computer system information", ex);
}
}
}
return computerSystemDetails;
The value I'm getting from the above is 16243 MB. But when I use DirectX Diagnostic tool I am getting the value 16384MB. What is the explanation for this difference? Please advice.
The documentation for that property explains why:
Be aware that, under some circumstances, this property may not return an accurate value for the physical memory. For example, it is not accurate if the BIOS is using some of the physical memory. For an accurate value, use the Capacity property in Win32_PhysicalMemory instead.

Controlling Application's Volume & VU Meter

I am using NAudio for a screen recording software I am designing and I need to know if it's possible to not only control the specific application's volume but also display a VU Meter for the application's sound.
I've Googled all over the place and it seems I can only get a VU Meter for the devices currently on my computer and set the volume for those devices.
Even though I am using NAudio, I am open to other solutions.
I asked the question in more detail after this question. I have since found the answer so I will leave the answer here for those who stumble upon it. Trying to use NAudio & CSCore has gotten me quite familiar with so please ask if you need further assistance.
This block of code uses CSCore and is a modified and commented version of the answer found here:Getting individual windows application current volume output level as visualized in audio Mixer
class PeakClass
{
static int CurrentProcessID = 0000;
private static void Main(string[] args)
{
//Basically gets your default audio device and session attached to it
using (var sessionManager = GetDefaultAudioSessionManager2(DataFlow.Render))
{
using (var sessionEnumerator = sessionManager.GetSessionEnumerator())
{
//This will go through a list of all processes uses the device
//the code got two line above.
foreach (var session in sessionEnumerator)
{
//This block of code will get the peak value(value needed for VU Meter)
//For whatever process you need it for (I believe you can also check by name
//but I found that less reliable)
using (var session2 = session.QueryInterface<AudioSessionControl2>())
{
if(session2.ProcessID == CurrentProcessID)
{
using (var audioMeterInformation = session.QueryInterface<AudioMeterInformation>())
{
Console.WriteLine(audioMeterInformation.GetPeakValue());
}
}
}
//Uncomment this block of code if you need the peak values
//of all the processes
//
//using (var audioMeterInformation = session.QueryInterface<AudioMeterInformation>())
//{
// Console.WriteLine(audioMeterInformation.GetPeakValue());
//}
}
}
}
}
private static AudioSessionManager2 GetDefaultAudioSessionManager2(DataFlow dataFlow)
{
using (var enumerator = new MMDeviceEnumerator())
{
using (var device = enumerator.GetDefaultAudioEndpoint(dataFlow, Role.Multimedia))
{
Console.WriteLine("DefaultDevice: " + device.FriendlyName);
var sessionManager = AudioSessionManager2.FromMMDevice(device);
return sessionManager;
}
}
}
}
The following code block will allow you to change the volume of the device using NAudio
MMDevice VUDevice;
public void SetVolume(float vol)
{
if(vol > 0)
{
VUDevice.AudioEndpointVolume.Mute = false;
VUDevice.AudioEndpointVolume.MasterVolumeLevelScalar = vol;
}
else
{
VUDevice.AudioEndpointVolume.Mute = true;
}
Console.WriteLine(vol);
}
I have code from two different libraries only to answer the question I posted directly which was how to both set the volume and get VU Meter values (peak values). CSCore and NAudio are very similar so most of the code here is interchangeable.

Cosmos custom OS, addmapping?

I am new to C# and is currently using COSMOS to make a simple FileSystem for my OS class. Currently I'm trying to implement a "reformat" function that, when the word "reformat" is typed into the console, the OS (emulated via QEMU), partitions the disk. Currently this is my code:
public static void console()
{
while (true)
{
Console.WriteLine("Console: ");
String input = Console.ReadLine();
if (input == "exit")
{
Cosmos.Sys.Deboot.ShutDown();
}
else if (input == "cpumem")
{
Console.WriteLine(Cosmos.Kernel.CPU.AmountOfMemory.ToString());
}
else if (input == "restart")
{
Cosmos.Sys.Deboot.Reboot();
}
else if (input == "devices")
{
var devices = Cosmos.Sys.FileSystem.Disk.Devices.ToArray();
}
else if (input == "reformat")
{
try
{
Partition part = null;
for (int j = 0; j < Cosmos.Hardware.BlockDevice.Devices.Count; j++)
{
if (Cosmos.Hardware.BlockDevice.Devices[j] is Partition)
{
part = (Partition)Cosmos.Hardware.BlockDevice.Devices[j];
}
}
var fs = new Cosmos.Sys.FileSystem.FAT32.FAT32(part);
uint cluster = 100;
fs.Format("newCluster", cluster);
}
catch
{
//Do Something warn user.
}
}
}
}
Most important is this bit:
else if (input == "reformat")
{
try
{
Partition part = null;
for (int j = 0; j < Cosmos.Hardware.BlockDevice.Devices.Count; j++)
{
if (Cosmos.Hardware.BlockDevice.Devices[j] is Partition)
{
part = (Partition)Cosmos.Hardware.BlockDevice.Devices[j];
}
}
var fs = new Cosmos.Sys.FileSystem.FAT32.FAT32(part);
uint cluster = 100;
fs.Format("newCluster", cluster);
}
catch
{
//Do Something warn user.
}
}
Which is analogous to what is located here: http://cosmos-tutorials.webs.com/atafat.html
However, when I run it, I get this error:
I believe this is because I lack this line:
Cosmos.System.Filesystem.FileSystem.AddMapping("C", FATFS);
FATFileList = FATFS.GetRoot();
Located in the link above. Is there any other way to map? Or am I missing something completely? The COSMOS documentation doesn't really tell much, the source code is honestly confusing for a beginner like me as it has no comments whatsoever on how the functions work or what they do. I am using an older version of COSMOS (Milestone 4) as it's the only one that works for Visual Studio C# 2008. Newer versions run only in Visual Studio C# 2010.
Ah, I recognize this... had to debug a similar situation on a Cosmos project I'm working on myself (I'm using the VS2010-compatible Cosmos but the same situation might apply to older versions as well...)
This can happen if you try to call a method on a null object. Type 0x........, Method 0x........ is specifically mentioning the location in the compiled code where the call failed. "Not FOUND!" means that the method it is looking for cannot be found, presumably because you called it on a null reference.
I'm testing with VirtualBox myself, and found that if you're using a brand-new blank hard disk image, there will be no Partitions on it. Thus, the condition will never get satisfied, your Partition will never get set and then Cosmos will try to execute a method on the null Partition!
Look closely at how you set the Partition (it's initialized to null). For starters I would print a simple message each time the "if (block device is partition)" condition is satisfied... I would be willing to bet it will never print.
Hope this helps... I am still learning about Cosmos and custom kernels myself but fixing the null reference in my case solved my occurrence of the problem. If that's the problem, then the next step, of course, is figuring out why you're not getting any Partitions in the first place...
The rest of your code looks fine but I am not sure how you implemented the rest of your classes. Kernel debugging can be a nightmare, good luck to you!

Debugging OoM Exception

Edit: Added code (Exception on line 095, 5th time it's hit.)
public DataTable ParseBarcodes(String[] files, BarcodeZoneScan[] scanParameters)
{
message = null;
//gmseBitmap img = null;
gmseBitmap rotImg = null;
gmseBitmap parseImage = null;
gmseBitmap tempImage = null;
DataTable codes = new DataTable();
codes.Columns.Add("PageNumber");
codes.Columns.Add("Text");
codes.Columns.Add("Type");
codes.Columns.Add("RegionName");
try
{
gmseBarcodeInfoCollection bcc;
gmseBarcodeReaderParameter param = new gmseBarcodeReaderParameter();
gmseLicense.License = "plaintext license key ommited";
String dvImageName;
int searchCount = 0;
for (int dvCount = 0; dvCount < files.Length; dvCount++)
{
if (cancelled) //If cancelled, end the loops
{
dvCount = files.Length;
break;
}
dvImageName = files[dvCount].ToString();
using (gmseBitmap img = new gmseBitmap(dvImageName))
{
int framecount = img.GetFrameCount();
for (int e = 0; e < framecount; e++)
{
for (int j = 0; j < scanParameters.Length; j++)
{
if (scanParameters[j].Range == PageRange.All ||//All
(scanParameters[j].Range == PageRange.Even && (searchCount == 0 || searchCount % 2 == 0)) || //even
(scanParameters[j].Range == PageRange.Odd && (searchCount != 0 && searchCount % 2 != 0)) ||
(scanParameters[j].Range == PageRange.First && searchCount == 0))
{
//Setup what barcodes are going to be search for
param.BarcodeType = 0;
if (scanParameters[j].BarcodeTypes == BarcodeType.All) //All
{
param.BarcodeType = (int)gmseBarcodeType.All;
}
else
{
if ((scanParameters[j].BarcodeTypes & BarcodeType.Code39) != 0) //Code 39
param.BarcodeType = param.BarcodeType | (int)gmseBarcodeType.Code39;
if ((scanParameters[j].BarcodeTypes & BarcodeType.Code11) != 0) //Code 11
param.BarcodeType = param.BarcodeType | (int)gmseBarcodeType.Code11;
if ((scanParameters[j].BarcodeTypes & BarcodeType.Code93) != 0) //Code 93
param.BarcodeType = param.BarcodeType | (int)gmseBarcodeType.Code93;
if ((scanParameters[j].BarcodeTypes & BarcodeType.Code128) != 0) //Code 128
param.BarcodeType = param.BarcodeType | (int)gmseBarcodeType.Code128;
if ((scanParameters[j].BarcodeTypes & BarcodeType.Ean8) != 0) //EAN 8
param.BarcodeType = param.BarcodeType | (int)gmseBarcodeType.EAN8;
if ((scanParameters[j].BarcodeTypes & BarcodeType.Ean13) != 0) // EAN 13
param.BarcodeType = param.BarcodeType | (int)gmseBarcodeType.EAN13;
if ((scanParameters[j].BarcodeTypes & BarcodeType.I2of5) != 0) //I2of5
param.BarcodeType = param.BarcodeType | (int)gmseBarcodeType.i2of5;
}
param.IgnoreCheckSum = 1;
param.ReadMode = gmseBarcodeReadMode.WholeBitmap;
using (rotImg = new gmseBitmap(img.ExtractFrame(e)))
{
// do some basic image enhancement for better results
rotImg.ChangePixelFormat(System.Drawing.Imaging.PixelFormat.Format32bppArgb);
rotImg.SelectActiveFrame(e);
if (scanParameters[j].WholePage)
{
parseImage = rotImg.ExtractFrame(e);
}
else
{
using (tempImage = rotImg.ExtractFrame(e))
{
Rectangle convertedRect = returnConvertedRectangle(tempImage, scanParameters[j].Dimensions);
if (convertedRect.IntersectsWith(new Rectangle(0, 0, tempImage.Width, tempImage.Height)))
{
//GC.Collect(); //Test so I can see what objects are still alive in dump
parseImage = tempImage.CopyRectangle(convertedRect); //Exception here
}
}
}
}
//rotImg.Dispose();
//rotImg = null;
if (parseImage != null)
{
//Now we will apply the image enhancements:
if (scanParameters[j].Enhancements != ImageEnhancement.None)
{
rotImg = EnhanceImage(parseImage, scanParameters[j].Enhancements);
parseImage.Dispose();
parseImage = null;
}
if ((scanParameters[j].BarcodeScanDirection & ScanDirection.LeftToRight) != 0 && !cancelled)
{
if (parseImage == null)
{
tempImage = new gmseBitmap(rotImg.Image, 1);
}
else
{
tempImage = new gmseBitmap(parseImage.Image, 1);
}
bcc = tempImage.ReadBarcodes(param);
foreach (gmseBarcodeInfo bc in bcc)
{
addBarcode(codes, new object[] { searchCount, bc.Text, gmseBarcodeTypeConvert(bc.BarcodeType), scanParameters[j].ZoneName });
}
tempImage.Dispose();
tempImage = null;
}
if ((scanParameters[j].BarcodeScanDirection & ScanDirection.RightToLeft) != 0 && !cancelled)
{
if (parseImage == null)
{
tempImage = new gmseBitmap(rotImg.Image, 1);
}
else
{
tempImage = new gmseBitmap(parseImage.Image, 1);
}
tempImage.RotateFlip(RotateFlipType.Rotate180FlipNone);
bcc = tempImage.ReadBarcodes(param);
foreach (gmseBarcodeInfo bc in bcc)
{
addBarcode(codes, new object[] { searchCount, bc.Text, gmseBarcodeTypeConvert(bc.BarcodeType), scanParameters[j].ZoneName });
}
tempImage.Dispose();
tempImage = null;
}
if ((scanParameters[j].BarcodeScanDirection & ScanDirection.TopToBottom) != 0 && !cancelled)
{
if (parseImage == null)
{
tempImage = new gmseBitmap(rotImg.Image, 1);
}
else
{
tempImage = new gmseBitmap(parseImage.Image, 1);
}
tempImage.RotateFlip(RotateFlipType.Rotate90FlipNone);
bcc = tempImage.ReadBarcodes(param);
foreach (gmseBarcodeInfo bc in bcc)
{
addBarcode(codes, new object[] { searchCount, bc.Text, gmseBarcodeTypeConvert(bc.BarcodeType), scanParameters[j].ZoneName });
}
tempImage.Dispose();
tempImage = null;
}
if ((scanParameters[j].BarcodeScanDirection & ScanDirection.BottomToTop) != 0 && !cancelled)
{
if (parseImage == null)
{
tempImage = new gmseBitmap(rotImg.Image, 1);
}
else
{
tempImage = new gmseBitmap(parseImage.Image, 1);
}
tempImage.RotateFlip(RotateFlipType.Rotate270FlipNone);
bcc = tempImage.ReadBarcodes(param);
foreach (gmseBarcodeInfo bc in bcc)
{
addBarcode(codes, new object[] { searchCount, bc.Text, gmseBarcodeTypeConvert(bc.BarcodeType), scanParameters[j].ZoneName });
}
tempImage.Dispose();
tempImage = null;
}
if (parseImage != null)
{
parseImage.Dispose();
parseImage = null;
}
if (rotImg != null)
{
rotImg.Dispose();
rotImg = null;
}
}
}
}
searchCount++;
if (cancelled) //If cancelled, end the loops
{
e = framecount;
dvCount = files.Length;
}
}
} //end using img
//img.Dispose();
//img = null;
}
}
catch (Exception ex)
{
message = ex.Message;
}
finally
{
if (img != null)
{
img.Dispose();
img = null;
}
if (rotImg != null)
{
rotImg.Dispose();
rotImg = null;
}
if (tempImage != null)
{
tempImage.Dispose();
tempImage = null;
}
if (parseImage != null)
{
parseImage.Dispose();
parseImage = null;
}
}
if (!String.IsNullOrEmpty(message))
throw new Exception(message);
return codes;
}
We use this GMSE Imaging plugin to assist in OCR reading barcodes from scans, it deals with skew by rotating the image by 10 degrees until it gets a read. A bug was discovered where scanning different sized sheets would throw an error.
I traced it from our main program to one of our DLLs, where I found it was catching an OutOfMemoryException.
The original TIF is 300kb, but there is a fair amount of copying done to rotate the images. (between 4 bitmaps)
However I have followed the program through and monitored the locals and it appears that each bitmap is being disposed and assigned null correctly before the method at fault loops.
I've also tried adding GC.Collect() at the end of my loop.
I am on a 32bit W7 machine, which I have read has 2GB limit per object, with copious amounts of RAM so nothing lacking that that respect.
Been watching it on Task Manager and my RAM usage only goes from 1.72GB to 1.78GB.
This has been a tricky one to research, as OoM seems to be an unusual occurring error.
I was wondering if anyone had any advice in dealing with this kind of exception? I'm not a Visual Studio master, is there an easy way of monitoring resources/memory usage?
Or knows of any utilities I can use to assist?
Dumping the error message here, not sure how useful the code snippets would be in this situation...
System.OutOfMemoryException was caught
Message=Out of memory.
Source=System.Drawing
StackTrace:
at System.Drawing.Bitmap.Clone(Rectangle rect, PixelFormat format)
at gmse.Imaging.gmseBitmap.CopyRectangle(Rectangle r)
at ImagingInterface.ImagingFunctions.ParseBarcodes(String[] files, BarcodeZoneScan[] scanParameters) in C:\Working\Scan.backup\Global Dlls\v2.6.0.02\ScanGlobalDlls\ImagingInterface\ImagingFunctions.cs:line 632
InnerException:
(currently reading more into GC/Memory management http://msdn.microsoft.com/en-us/library/ee851764.aspx )
Working on a step of this guide, using SOS debugger in the Immediate window, with the aim of pinpointing whether the exception is generated from managed or unmanaged code.
Steps from above have indicated it's a problem with the managed code, as exception type from SOS is shown.
Exception object: 39594518
Exception type: System.OutOfMemoryException
Message: <none>
InnerException: <none>
StackTrace (generated):
The Heapdump I took doesn't seem to be thousands of bitmaps like I had kinda expected. Not 100% sure how to interpret the dump so seeing what I can find on it.
Not sure where to move from here right now! (searches..)
edit:
I have been trying to apply the lessons in this blog to my problem.
Started with PerfMon
This graph shows my program from execution to where it catches the exception.
The first two sharp peaks occur after triggering parsing of the scanned image, the last drop off occurs when the exception is caught.
Q: Compare the curves for Virtual Bytes, Private Bytes and #Bytes in all Heaps, do they follow eachother or do they diverge?
What is the significance of #Bytes in all heaps diverging from? (As its flat on mine)
Examined Memory with !address -summary
MEM_IMAGE corresponded PrivateBytes(113MB) pretty much spot on.
Q: Where is most of the memory going (which RegionType)?
RegionUsageFree 87.15%
RegionUsageIsVAF 5.64% (Busy 43.89%) [memory allocated through VirtualAlloc]
RegionUsageImage 5.54% (Busy 43.13%) [Memory that is mapped to a file that is part of an executable image.]
In WinDbg with SOS loaded, I did a !DumpHeap
//...
7063424c 1201 28824 System.Collections.ArrayList
706228d4 903 28896 System.EventHandler
7062f640 1253 30072 System.RuntimeType
6ec2be78 833 31216 System.Windows.Forms.PropertyStore+IntegerEntry[]
6ec2b0a4 654 34008 System.Windows.Forms.CreateParams
7063547c 318 35472 System.Collections.Hashtable+bucket[]
6ec2aa5c 664 37184 System.Windows.Forms.Control+ControlNativeWindow
70632938 716 40400 System.Int32[]
6c546700 48 49728 System.Data.RBTree`1+Node[[System.Data.DataRow, System.Data]][]
70634944 85 69600 System.Byte[]
6ec2b020 931 85972 System.Windows.Forms.PropertyStore+ObjectEntry[]
6c547758 156 161616 System.Data.RBTree`1+Node[[System.Int32, mscorlib]][]
705e6c28 2107 238912 System.Object[]
00305ce8 18 293480 Free
7062f9ac 5842 301620 System.String
Total 35669 objects
And here are the top memory hogging objects.
I was hoping something would stick out like a sore thumb, like a giant amount of bitmaps or something. Is anything here scream out "I'm acting unusually!" to anyone?
(I am trying to examine the top ones individually for suspect things, but would be nice to narrow down the possible culprits a bit more)
This page (Address summary explained) has been a big help.
However C# is my first language, so I have no prior experience debugging memory issues. Would like to know if I am on the right track (Is GC an issue at all?) as I haven't found anything that's given me any clear indications yet.
Answer: Problem was caused in 3rd party library. Nothing I can do.
Found out through deliberation and some tests with stripped down code involving just the method producing the error.
Bounty awarded to what I felt I learnt the most from.
Okay, the added info helps. The problem is not that your program uses too much memory, it uses too little. The garbage collected heap has very little data in it. That's not uncommon for a program that manipulates bitmaps. The Bitmap class is a very small wrapper around GDI+ functions, it uses only a handful of bytes in the GC heap. So you can create an enormous amount of bitmaps before you fill up the gen #0 heap and trigger a garbage collection. This is also visible from Perfmon, you want to look at the .NET CLR Memory, Gen 0 Collections counter. A healthy program triggers a collection about 10 times per second when it is doing work.
Not getting collections is fine, but there's something else that doesn't work when there are no collections. The finalizer thread never runs. Finalizers are important to release unmanaged resources other than memory. Like operating system handles and any unmanaged memory pointers held by managed objects. Bitmap has those.
First thing to do is to run Taskmgr.exe, Processes tab. Click View + Select Columns and tick Handles, USER objects and GDI objects. Observe these counters while your program is running. If you see one climbing up without bound then you have a problem that could cause GDI+ to generate an OOM exception. GDI objects being the common cause.
Carefully review your code and check that you are calling Dispose() on any Image or Bitmap that you no longer use. Beware of the subtle ones, like assigning the Image property of a PictureBox. You'd have to dispose the old one if it isn't null. That's painful of course and it is too easy to miss one. So use a simple strategy, count the number of bitmaps you created and, say, on the hundredth call GC.Collect + GC.WaitForPendingFinalizers() to trigger a collection and a finalizer sweep.
In the past I've always used ANTS Memory Profiler to troubleshot this sort of thing. Its not free but it works pretty pretty well for memory/reference leaks in managed code. You just take a couple of snapshots when the application should be at steady state and look at the changes.
You can safely add a using block around the img variable and with a little refactoring you can do the same to the other image-variables you are declaring.
That should at least make code more readable, and reduce the chance to forget to add one to the finally block; I may even contribute to solving the problem. You seem to be manually disposing each and every created image object, though.

Categories

Resources