How to Fix `CapturedBitmap.ToBitmap()` Crashes in Direct3DHook
Fix repeated Direct3DHook capture crashes by disposing the returned Screenshot and converted Bitmap on every code path.

CapturedBitmap.ToBitmap() is the conversion call named in the failure, but the reported fix is to dispose the Screenshot returned by GetScreenshot(). The original loop disposed only the converted Bitmap and dropped the Screenshot object. Retain both objects and dispose both, including when conversion throws.
1. The direct fix
Replace the chained call with a local variable. If Direct3DHook’s Screenshot implements IDisposable, use nested using statements:
public void TestCapture()
{
for (int i = 0; i < 200; i++)
{
using (Screenshot screenshot =
_captureProcess.CaptureInterface.GetScreenshot())
using (Bitmap bitmap = screenshot.CapturedBitmap.ToBitmap())
{
// Consume, save, or copy the bitmap here.
}
}
}
The explicit form is equivalent when you need to separate the lifetime of the bitmap from the screenshot. Dispose the screenshot after conversion and dispose the bitmap when your code has finished using it:
public void TestCapture()
{
for (int i = 0; i < 200; i++)
{
Screenshot screenshot = _captureProcess.CaptureInterface.GetScreenshot();
try
{
Bitmap bitmap = screenshot.CapturedBitmap.ToBitmap();
try
{
// Save or process bitmap.
}
finally
{
bitmap.Dispose();
}
}
finally
{
screenshot.Dispose();
}
}
}
The using version is safer because cleanup still runs if ToBitmap(), image encoding, or later processing throws. Confirm the exact ownership contract in the Direct3DHook version you use: the converted Bitmap and the containing Screenshot are separate resources, but a particular implementation may document additional restrictions.
2. Why the chained expression is dangerous
This expression hides the object that owns the capture resources:

Bitmap bitmap = _captureProcess.CaptureInterface
.GetScreenshot().CapturedBitmap.ToBitmap();
After conversion, there is no reference available for calling Dispose() on the returned Screenshot. Disposing the Bitmap releases the managed image you created, but it does not necessarily release the Direct3DHook screenshot, texture, staging resource, mapping, or related native state. The historical report describes a target DirectX application crashing after repeated captures and says that disposing the Screenshot solved that report. It does not prove a universal leak mechanism or a failure threshold for other applications.
3. A production capture loop
Keep the capture scope small
Capture, convert, consume, and release resources in one iteration. Do not keep a collection of Screenshot objects or Bitmap instances unless you have an explicit ownership plan.
public void CaptureFrames(int count, string directory)
{
Directory.CreateDirectory(directory);
for (int index = 0; index < count; index++)
{
using (Screenshot screenshot =
_captureProcess.CaptureInterface.GetScreenshot())
using (Bitmap bitmap = screenshot.CapturedBitmap.ToBitmap())
{
string path = Path.Combine(directory, $"frame-{index:D5}.png");
bitmap.Save(path, ImageFormat.Png);
}
}
}
Copy pixels when work must outlive the capture
If another thread or queue needs the image after the capture iteration, make an independent copy while the source is valid, then dispose the source objects before enqueueing:
public Bitmap CaptureForQueue()
{
using (Screenshot screenshot =
_captureProcess.CaptureInterface.GetScreenshot())
using (Bitmap source = screenshot.CapturedBitmap.ToBitmap())
{
return new Bitmap(source);
}
}
The copy adds memory and CPU work, but it prevents a worker from using an object whose native capture resources have already been released. Follow the image library’s documented cloning behavior for your pixel format.
Do not share one capture object across unsynchronized threads
The report used a separate capture thread. That detail alone does not establish that threading caused the crash, but Direct3D capture objects often have thread-affinity or synchronization assumptions. Serialize calls through one worker unless the library explicitly documents concurrent capture support.
4. Step-by-step troubleshooting
- Retain the screenshot. Introduce a local
Screenshotvariable before accessingCapturedBitmap. - Dispose it on every path. Use
usingortry/finally; explicit code afterToBitmap()is skipped when conversion throws. - Dispose the converted bitmap. Keep this cleanup separate from screenshot cleanup.
- Repeat the real workload. The reported symptom appeared only after many calls, so one successful capture does not validate a repeated loop.
- Record the failing stage. Log whether failure occurs in
GetScreenshot(),CapturedBitmapaccess,ToBitmap(), image encoding, or disposal. - Check the target mode. Record 32-bit versus 64-bit, DirectX version, windowed versus fullscreen mode, and whether the target was alt-tabbed or minimized.
- Check library ownership rules. Read the version-specific
ScreenshotandCapturedBitmapdocumentation before adding clones, delayed disposal, or cross-thread use.
5. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Target crashes after many captures | The returned Screenshot is never disposed. |
Store it in a variable and dispose it every iteration. |
| Cleanup is skipped after conversion failure | Cleanup statements follow a throwing ToBitmap() call. |
Use nested using statements or finally. |
ObjectDisposedException in a worker |
A bitmap was queued after its source scope ended. | Clone the pixels before disposal, or process inside the scope. |
| Intermittent failures with parallel callers | Capture state is being used concurrently without documented support. | Serialize capture calls through one worker and test again. |
| Failure only in fullscreen or after alt-tab | Swap-chain, lost-device, or presentation behavior differs from windowed capture. | Isolate the display mode and Direct3D version; investigate the hook’s device-recovery path. |
| Memory rises even after adding screenshot disposal | Bitmaps, encoded streams, queued copies, or application-owned buffers remain alive. | Dispose every image and stream, bound queues, and remove references after processing. |
| Capture process survives but target exits | The target’s graphics resources or hook interaction may be failing. | Capture a minimal reproducible loop and separate resource lifetime from compatibility issues. |
6. What the historical report does and does not establish
The Stack Overflow report was posted in 2014 and concerned a 32-bit DirectX application running through BlueStacks. The author requested up to 200 captures and reported a target crash after roughly 150 calls; the accepted answer says calling Dispose() on the Screenshot solved that case. Those numbers describe one user report, not a general limit or benchmark.
The Direct3DHook D3D11 implementation has separate swap-chain, texture, staging, mapping, and cleanup stages. Therefore, a different crash with the same method name may be caused by device loss, an unsupported target, an image conversion problem, or another ownership violation. Do not assume every failure is fixed by changing disposal order.
A related Direct3D capture project discusses fullscreen alt-tab and lost-device concerns and notes that its port still had bugs. That context can guide compatibility investigation, but it is not evidence that switching implementations fixes this resource-lifetime problem.
7. Performance, reliability, and resource use
- Dispose promptly. Native textures and staging resources can outlive the managed wrapper until disposal or finalization. Prompt cleanup keeps repeated captures bounded.
- Reuse only documented-safe objects. Reusing a capture interface may reduce setup overhead, but reuse does not remove the need to dispose each returned
Screenshot. - Bound asynchronous queues. If encoding or analysis is slower than capture, an unbounded queue retains bitmaps and can exhaust memory even with correct screenshot disposal.
- Choose an appropriate image format. PNG preserves pixels but costs more CPU and storage than a lossy format. Measure the complete pipeline, including conversion and encoding.
- Keep diagnostics lightweight. Track capture count, duration, failure stage, process memory, and queue depth. Avoid logging raw frames in a tight loop.
- Stress the same scenario. Test the target mode and capture cadence that matter to your application. A load-test button or different target may exercise a different resource path.
8. A minimal diagnostic harness
public void Diagnose(int count)
{
for (int index = 0; index < count; index++)
{
Stopwatch timer = Stopwatch.StartNew();
try
{
using (Screenshot screenshot =
_captureProcess.CaptureInterface.GetScreenshot())
using (Bitmap bitmap = screenshot.CapturedBitmap.ToBitmap())
{
Console.WriteLine($"{index}: converted {bitmap.Width}x{bitmap.Height}");
}
Console.WriteLine($"{index}: completed in {timer.ElapsedMilliseconds} ms");
}
catch (Exception error)
{
Console.Error.WriteLine($"{index}: {error.GetType().Name}: {error.Message}");
throw;
}
}
}
This harness makes the lifetime visible and identifies whether an exception is thrown before or after conversion. It is a diagnostic shape, not a claim that a particular number of iterations reproduces the historical crash.
9. Or skip the browser setup
If your actual goal is obtaining screenshots of web pages rather than hooking a DirectX application, ScreenshotNeo provides a single HTTP request. The API accepts a URL and returns PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for the complete option list.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account and get 1,000 screenshots each month without adding a card.
10. FAQ
Should I dispose the Screenshot before or after ToBitmap()?
Convert while the screenshot is valid, then dispose both objects. Nested using statements make that order explicit.
Does disposing the Bitmap dispose the Screenshot?
Do not assume so. The reported fix specifically added disposal of the Screenshot, while the converted Bitmap remained a separate resource.
Is the crash guaranteed after 150 captures?
No. Roughly 150 calls was a detail of one historical report, not a library limit.
Can I solve this by forcing garbage collection?
Explicit ownership cleanup is the correct first step. Garbage collection is nondeterministic and does not replace documented disposal of native resources.
Should I change capture libraries?
First isolate disposal, threading, target mode, and the failing stage. A different implementation may have different compatibility tradeoffs and does not prove the original ownership issue is fixed.


