System designTroubleshooting

Camera frame memory lifetime in managed code: avoid access violations

Why GC wrappers reclaim native camera frames while your code still uses them, and how explicit ownership, using blocks and copies prevent the crash.

LumaSpec Vision8 min read Share on LinkedIn

On this page 10 sections
  1. 01Symptom: the crash that moves when you fix it
  2. 02Why garbage collection releases frames early
  3. 03First test: prove it is a lifetime problem
  4. 04Fix 1: scope every frame in a using block
  5. 05Fix 2: copy pixel data before the frameset dies
  6. 06Fix 3: hold an explicit reference across processing calls
  7. 07Sizing the host for multi-camera loops
  8. 08Prevention checklist
  9. 09When the usual answer is wrong
  10. 10Questions

An access violation a few seconds into a managed streaming loop rarely means corrupt memory in your own code. Far more often it means the camera SDK released a native frame while your managed code still held a pointer to it. Garbage collection and unmanaged handles interact in ways a compiler will not warn about, and the failure shows up as System.AccessViolationException deep inside a wrapper's native call, sometimes only after tens of frames. This guide walks through the failure pattern, the tests that isolate it, and three fixes that keep every frame alive for exactly as long as you need it. It applies to any camera whose SDK exposes frames as disposable wrapper objects, from a compact GigE model such as the MV-CU020-90GC to a multi-camera cell driven by a vision controller.

Symptom: the crash that moves when you fix it

The classic report looks like this. You call a processing block, such as a depth-to-color alignment, on a frameset from the pipeline. A null check passes, then the native call throws an access violation; under the debugger the depth frame inside the frameset reads as null at the moment of the call. You add using blocks around every frame and the exception moves, now firing inside the Release method of the frame wrapper instead.

Both symptoms are the same bug seen from two sides. In the first case the frame was released before the call, so the processing block received a dead handle. In the second, your explicit using disposed a frame whose wrapper reference count had already dropped, so the release ran twice. The exception location tells you where the last good handle was consumed, not where the ownership went wrong.

Why garbage collection releases frames early

A managed wrapper is a thin object holding an unmanaged handle. The finalizable object can be collected as soon as the JIT sees no further use of the local variable, even while a native call in the same method is still running with a raw pointer taken from that handle. Nothing in the pixel buffer keeps the wrapper alive; the pointer is not a reference the collector tracks.

Processing blocks make this worse because ownership crosses a boundary. When an alignment or colorizer block consumes an input frameset and returns a new one, the wrapper must take an additional reference on the input for the lifetime of the call. If that reference is missing, the returned frameset can be populated while the source is torn down under it. For a related failure mode where wrapper objects leak instead of dying early, see the guide on camera feature access memory leak troubleshooting.

First test: prove it is a lifetime problem

Before changing architecture, confirm the diagnosis with two quick, low-cost tests.

  1. Keep a rooted reference to every wrapper for the whole loop: store the frameset and both frames in class fields for the duration of one iteration, and null the fields only at the end. If the crash disappears or moves much later, ownership was the issue.
  2. Call GC.Collect() and GC.WaitForPendingFinalizers() immediately after acquiring a frameset, before using it. If frames now go null or crash consistently, the collector is finalizing them the moment your code yields a local reference.

Also disable concurrent finalization as a diagnostic, not a fix, and run a release build: timing differences between debug and release change when collection happens and are themselves a clue. If the crash persists with everything rooted, suspect a wrapper bug or a driver-level fault instead; the article on tracing camera integration access violations in C++ covers native-side fault isolation.

Fix 1: scope every frame in a using block

Acquire the frameset, and derive and consume all frames inside the same scope:

scope = one pipeline frame using (frames) { using (aligned = align.Process(frames)) { using (colorized = colorize(aligned.DepthFrame)) { upload(colorized) } } }

Disposal order then matches the dependency order: nothing derived outlives its source. This alone does not fix a missing internal reference in the processing block, but it removes every case where your own code releases a frame too soon or too late. Do not cache Frame objects across iterations; re-acquire them each loop.

Fix 2: copy pixel data before the frameset dies

Any pixel access that outlives the using block, feeds another thread, or is queued for display must read from memory you own. Copy the buffer out while the frame is valid. The size of the copy is fixed by the frame geometry:

bytes = stride × height stride = width × bits_per_pixel / 8

Worked example: a 1280 × 720 8-bit depth frame has a stride of 1280 bytes, so the copy is 1280 × 720 = 921,600 bytes, about 0.9 MB per frame. At 30 fps that is roughly 27 MB/s of allocation churn if you allocate per frame, so reuse one pinned buffer per stream and copy into it. Allocating unmanaged memory each frame and freeing it late is exactly the pattern behind a C# camera memory leak from buffer ownership; a single reused buffer avoids both the leak and the crash.

The copy also sidesteps a second gotcha: the pixel format you asked for is not always the format you get. Verify bits per pixel and channel order from the frame object itself, not from your configuration, before you build a bitmap from the copy.

Fix 3: hold an explicit reference across processing calls

When a processing block returns a new frameset built from your input, keep the input alive across the call, and require the wrapper to reference-count correctly. If you maintain or patch a wrapper, the check is simple: the call that consumes an input frameset should acquire an additional reference on it before processing and release it afterwards. If you cannot patch the wrapper, the workaround is on your side: hold the source frameset in a using block that encloses the processing call and every use of its result, and never dispose the source before the derived frameset.

Sizing the host for multi-camera loops

Lifetime bugs get harder to reproduce as camera count rises, because buffer pools grow and collection timing shifts. Budget roughly stride × height × frame rate per stream, plus headroom for the copies. A machine with an Intel Core i3, 8 GB of memory and a 128 GB SSD, as in the MV-VC3301H-128G66, comfortably hosts two or three GigE streams at these sizes; larger fleets belong on a controller with more cores and memory. The camera SDK build mismatch guide covers a sibling failure, where mismatched native binaries produce timeouts rather than crashes.

For a general grounding in alignment and timestamp behaviour across depth and color streams, read depth and color synchronization: check the right timestamps; mis-synchronized streams often get misdiagnosed as memory faults. A controls-side view of access violations in scripted automation is in this C-script memory access violation walkthrough.

Prevention checklist

  • Every Frame and FrameSet is inside a using block whose scope ends after last use.
  • No Frame object is cached across loop iterations or stored in a collection without copying.
  • Pixel data needed later is copied into a reused pinned or unmanaged buffer.
  • Pixel format, stride and bits per pixel are read from the frame, not assumed.
  • The processing block input is held alive for the whole call, ideally by reference count.
  • A soak test runs at target frame rate for at least 30 minutes with memory tracked; steady memory with no rising trend confirms both leaks and double-releases are gone.

When the usual answer is wrong

If the access violation survives everything above, the crash is probably not a lifetime issue. Three alternatives, in order of likelihood: a mismatch between wrapper and native library versions, which also produces odd pixel formats; a hardware or driver fault, worth testing with the vendor's own viewer on the same machine; and genuine buffer corruption in your own unsafe code, which a bounds-checked, copy-based rewrite will expose. Report the last case with a minimal repro to the SDK vendor rather than working around it.

To plan a complete acquisition chain around these cameras, start with the area scan cameras category, then use the system configurator to match camera, controller and cabling, or contact us for help sizing a multi-camera cell.

Questions

Why does my camera wrapper crash with an access violation after a few frames?

Most often a native frame is released while managed code still points at it. A garbage-collected wrapper owns an unmanaged handle; once the wrapper object is collected or disposed, the SDK frees the pixel buffer. Any pointer or retained frame that outlives that moment reads freed memory, and the crash surfaces as System.AccessViolationException inside the native call.

How do I keep a camera frame alive until my processing finishes?

Scope the frame and everything derived from it inside the same using block, so disposal happens only after your code is done. If a processing block returns a new frameset, make sure the wrapper takes an additional reference on the input, or hold the original frameset open as well. For pixel access that must outlive the call, copy the data into memory you own first.

Should I copy pixel data out of the camera frame or process it in place?

Process in place when the work finishes before the using block exits: it avoids an allocation per frame. Copy when the data must survive the frameset, when another thread will read it later, or when you queue frames for display or logging. A copy costs stride times height bytes per frame, so budget the bandwidth at your frame rate.

Is a null check enough to prevent the crash before calling a native processing block?

No. The frame can pass a null check and be released between the check and the native call, because release happens on another path or in the finalizer. The fix is reference ownership, not testing for null: an additional reference on the input frameset for the lifetime of the processing call, or holding the source frameset open across the call.