Metadata-Version: 2.4
Name: rapidshot
Version: 2.2.0
Summary: A high-performance screencapture library for Windows using Desktop Duplication API
Home-page: https://github.com/Zaatra/Rapidshot
Author: Rapidshot Contributors
Author-email: RapidShot Contributors <imzaatra@gmail.com>
Maintainer-email: Zaatra <imzaatra@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/Zaatra/Rapidshot
Project-URL: Bug Reports, https://github.com/Zaatra/Rapidshot/issues
Project-URL: Source, https://github.com/Zaatra/Rapidshot
Project-URL: Documentation, https://github.com/Zaatra/Rapidshot
Keywords: screencapture,desktop-duplication,screen-recorder,windows
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: Microsoft :: Windows :: Windows 10
Classifier: Operating System :: Microsoft :: Windows :: Windows 11
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Capture
Classifier: Topic :: Multimedia :: Graphics :: Capture :: Screen Capture
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.19.0
Requires-Dist: comtypes>=1.1.0
Provides-Extra: cv2
Requires-Dist: opencv-python>=4.5.0; extra == "cv2"
Provides-Extra: gpu
Requires-Dist: cupy-cuda11x>=11.0.0; extra == "gpu"
Requires-Dist: opencv-python>=4.5.0; extra == "gpu"
Provides-Extra: gpu-cuda12
Requires-Dist: cupy-cuda12x>=12.0.0; extra == "gpu-cuda12"
Requires-Dist: opencv-python>=4.5.0; extra == "gpu-cuda12"
Provides-Extra: pil
Requires-Dist: pillow>=8.0.0; extra == "pil"
Provides-Extra: all
Requires-Dist: opencv-python>=4.5.0; extra == "all"
Requires-Dist: pillow>=8.0.0; extra == "all"
Provides-Extra: dev
Requires-Dist: black>=22.3.0; extra == "dev"
Requires-Dist: isort>=5.10.1; extra == "dev"
Requires-Dist: mypy>=0.950; extra == "dev"
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=3.0.0; extra == "dev"
Requires-Dist: flake8>=4.0.1; extra == "dev"
Requires-Dist: tox>=3.24.5; extra == "dev"
Provides-Extra: docs
Requires-Dist: sphinx>=4.5.0; extra == "docs"
Requires-Dist: sphinx-rtd-theme>=1.0.0; extra == "docs"
Requires-Dist: myst-parser>=0.18.0; extra == "docs"
Dynamic: author
Dynamic: home-page
Dynamic: license-file
Dynamic: requires-python

# RapidShot

[![CI](https://github.com/Zaatra/Rapidshot/actions/workflows/ci.yml/badge.svg)](https://github.com/Zaatra/Rapidshot/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/rapidshot)](https://pypi.org/project/rapidshot/)
[![Python](https://img.shields.io/pypi/pyversions/rapidshot)](https://pypi.org/project/rapidshot/)
[![License](https://img.shields.io/pypi/l/rapidshot)](LICENSE)

[![BGRA to RGB](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Zaatra/Rapidshot/main/.github/badges/convert-rgb.json)](ROADMAP.md#3-measured-baseline)
[![BGRA to GRAY](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Zaatra/Rapidshot/main/.github/badges/convert-gray.json)](ROADMAP.md#3-measured-baseline)
[![shot to buffer](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Zaatra/Rapidshot/main/.github/badges/shot.json)](ROADMAP.md#3-measured-baseline)
[![measured on](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Zaatra/Rapidshot/main/.github/badges/measured-on.json)](benchmarks/baseline.json)

<sub>Generated from [`benchmarks/baseline.json`](benchmarks/baseline.json), recorded
**with the optional native extension built**. A plain `pip install rapidshot`
needs no toolchain and uses pure-NumPy conversion, which is slower — those
numbers are in
[`benchmarks/baseline-nonative.json`](benchmarks/baseline-nonative.json)
(BGRA→RGB 1.9 ms rather than 0.32 ms). These are all deterministic synthetic
benchmarks. The end-to-end `grab()` and `grab_frame()` figures are measured too
but deliberately not badged: they depend on what is on screen, and
`grab_frame()` alone spanned 0.17–0.77 ms across six recordings of unchanged
code. See [ROADMAP.md § 3](ROADMAP.md#3-measured-baseline).</sub>

Windows screen capture built for feeding models, not for saving screenshots.

RapidShot captures the desktop through DXGI Desktop Duplication and can hand a
frame straight to a GPU consumer as a model-ready NCHW float32 tensor that never
touches the CPU. Colour conversion runs in byte-exact SIMD kernels, frames carry
the compositor's dirty-rect metadata so only what changed is converted, and a
captured frame can be moved to the discrete GPU on hybrid laptops that Desktop
Duplication refuses to capture from.

**RapidShot is not the fastest library by raw frame count, and says so.** Measured
against DXcam, BetterCam and mss on one 1080p 100 Hz display with a motion source
fast enough to saturate it (`benchmarks/compare_libraries.py`, full numbers in
`benchmarks/library-comparison.json`):

Median of three independent runs per cell, 1080p BGRA on a 100 Hz display, with
the spread across those runs:

| | frames/s | CPU | per-call p50 | memory |
| --- | --- | --- | --- | --- |
| RapidShot | 99.9 ±0.5% | **13.6% ±15%** | 9.97 ms | 124.4 MB |
| RapidShot (`timeout_ms=0`) | 100.1 ±0.1% | 95.9% ±17% | 2.73 ms | 125.2 MB |
| DXcam | 100.5 ±1.1% | 79.7% ±2% | 5.43 ms | **87.1 MB** |
| BetterCam | 100.5 ±17% | 74.4% ±12% | 2.31 ms | **80.0 MB** |
| mss | 48.1 ±5.8% | 35.0% ±2.6% | 20.12 ms | **60.4 MB** |

**Nobody beats the compositor.** Every DXGI-based library lands at ~100 fps
because that is where the ceiling is, RapidShot included — so a frame-rate win in
this space is almost always measuring something else, most often a still desktop
where capture returns stale buffers instantly. The interesting question is not
who captures fastest. It is what those frames cost you.

**RapidShot's answer is 13.6% of a core where DXcam spends 79.7%** — the same
frames, several times cheaper. Part of that is a default you could set elsewhere:
DXcam and BetterCam poll, racking up 100,000 empty returns in six seconds at a
0.3% hit rate, while RapidShot waits. Row two of the table is there so you can
see exactly that — `timeout_ms=0` buys DXcam's latency at DXcam's price, whenever
you want it.

**Memory is where RapidShot is behind**: 124 MB against BetterCam's 80 MB. It was
174 MB until the pool default dropped from 10 frames to 4, and `pool_size_frames`
takes it lower still if that matters more to you than reusing buffers.

**The part that is genuinely engineering** shows up when you isolate the colour
conversion, by subtracting each library's BGRA cost (no conversion) from its RGB
cost:

| | CPU cost of BGRA→RGB conversion |
| --- | --- |
| **RapidShot** | **+28.2 points** |
| BetterCam | +78.1 points |
| DXcam | +89.0 points |

**RapidShot converts colour for about a third of the CPU**, which is the AVX2
kernels rather than a scheduling choice.

### What this table cannot show

It measures `grab()` — the CPU round trip. The capabilities RapidShot is actually
built around have no column here because the other libraries have no equivalent:

- **`grab_frame()`** hands back a GPU-resident frame in 0.17–0.21 ms that never
  crosses to system memory.
- **The GPU tensor path** turns a frame into NCHW float32 in one dispatch, 2 µs of
  calling-thread time.
- **Cross-adapter transfer** moves a frame to the discrete GPU on hybrid laptops,
  which Desktop Duplication cannot capture from at all.
- **Dirty-rect metadata**, so a consumer can skip regions that did not change.

So the claim worth making is not "RapidShot is faster" — these numbers will not
support it, and neither will anyone else's. It is this: **the same frame rate for
a fraction of the CPU, colour conversion for a third of it, and a route to the GPU
the others do not have, at the price of ~40 MB more memory.**

Which is the better trade depends entirely on what else your machine is doing. If
capture is the only thing running and you want the lowest per-call latency, DXcam
and BetterCam are excellent and you should use them. If capture is one stage of a
pipeline that needs its cores for inference, encoding, or anything else, that CPU
column is the reason this library exists.

### Reading the error bars

Reproduce with `python benchmarks/compare_libraries.py --motion --with-motion`.
Every library runs in its own process, because all three DXGI libraries declare
the same COM interfaces and whichever imports first breaks the others.

Treat the CPU column as directional. The dev machine was not quiet during this
run — the control benchmark, whose code never changes, varied 42% across it — so
the *gaps* are far larger than the noise and trustworthy, while the exact
multiples are not. Longer windows and exact CPU accounting were both tried and
did not tighten it; a quiet machine is what that needs.

It began as a merge of several DXcam forks and keeps a broadly familiar API, but
the capture path, the colour pipeline and the GPU interop have since been
rewritten.

## Features

- **Capture as fast as the desktop actually changes**: Desktop Duplication
  reports compositor *presents*, not display refreshes, so the ceiling is how
  often the screen is redrawn — measured at 117 fps of distinct frames on a
  100 Hz panel here, with the compositor itself producing ~188/s
- **GPU-resident frames**: `grab_frame()` hands back a frame that never leaves
  the GPU, skipping the CPU round-trip entirely — for consumers that feed a model
  directly
- **Colour conversion is essentially free** with the optional native extension:
  BGRA→RGB in 0.31 ms and BGRA→GRAY in 0.26 ms per 1080p frame, at 80–96% of what
  the memory system can move, and byte-identical to the pure-Python path
- **Only process what changed**: frames carry the compositor's dirty-rect
  metadata, typically under 1% of the screen on a normal desktop
- **Hybrid GPU laptops**: move a frame to the discrete GPU that Desktop
  Duplication cannot capture from
- **Multi-backend support**: NumPy, PIL, and CUDA/CuPy backends
- **Cursor capture**: Capture mouse cursor position and shape
- **Direct3D support**: Capture Direct3D exclusive full-screen applications without interruption
- **NVIDIA GPU acceleration**: GPU-accelerated processing using CuPy
- **Multi-monitor setup**: Support for multiple GPUs and monitors
- **Flexible output formats**: RGB, RGBA, BGR, BGRA, and grayscale support
- **Region-based capture**: Efficient capture of specific screen regions
- **Rotation handling**: Automatic handling of rotated displays
- **Actionable diagnostics**: headless machines and hybrid GPU setups are
  detected and explained rather than failing opaquely

## Installation

> **Note:** The package is installed as `rapidshot` and imported as `import rapidshot`.

### Basic Installation

```bash
pip install rapidshot
```

### With OpenCV Support (recommended)

```bash
pip install rapidshot[cv2]
```

### With NVIDIA GPU Acceleration

```bash
pip install rapidshot[gpu]
```

### With All Dependencies

```bash
pip install rapidshot[all]
```

## Quick Start

### Basic Screencapture

```python
import numpy as np
import rapidshot

# Create a ScreenCapture instance on the primary monitor
screencapture = rapidshot.create()

# Take a screencapture
frame = screencapture.grab()

# Display the screencapture
from PIL import Image
Image.fromarray(np.asarray(frame)).show()

# Hand the buffer back when done (see "Frame buffers" below)
frame.release()
```

> **New in 2.0:** `grab()` returns a pooled buffer that you `release()` when
> done. It indexes and converts like the array it wraps, so most code needs only
> the added `release()`. See [Frame buffers](#frame-buffers).

### Region-based Capture

```python
# Define a specific region
left, top = (1920 - 640) // 2, (1080 - 640) // 2
right, bottom = left + 640, top + 640
region = (left, top, right, bottom)

# Capture only this region
frame = screencapture.grab(region=region)  # 640x640x3 frame; release() when done
```

### Continuous Capture

```python
# Start capturing at 60 FPS
screencapture.start(target_fps=60)

# Get the latest frame
for i in range(1000):
    image = screencapture.get_latest_frame()  # Blocks until new frame is available
    # Process the frame...

# Stop capturing
screencapture.stop()
```

### Video Recording

```python
import rapidshot
import cv2

# Create a ScreenCapture instance with BGR color format for OpenCV
screencapture = rapidshot.create(output_color="BGR")

# Start capturing at 30 FPS in video mode
screencapture.start(target_fps=30, video_mode=True)

# Create a video writer
writer = cv2.VideoWriter(
    "video.mp4", cv2.VideoWriter_fourcc(*"mp4v"), 30, (1920, 1080)
)

# Record for 10 seconds (300 frames at 30 FPS)
for i in range(300):
    writer.write(screencapture.get_latest_frame())

# Clean up
screencapture.stop()
writer.release()
```

### NVIDIA GPU Acceleration

```python
# Create a ScreenCapture instance with NVIDIA GPU acceleration
screencapture = rapidshot.create(nvidia_gpu=True)

# Screenshots will be processed on the GPU for improved performance
frame = screencapture.grab()
frame.release()
```

### Cursor Capture

RapidShot provides comprehensive cursor capture capabilities, allowing you to track cursor position, visibility, and shape in your screen captures.

```python
# Take a screenshot
frame = screencapture.grab()

# Get cursor information
cursor = screencapture.grab_cursor()

# Check if cursor is visible in the capture area
if cursor.PointerPositionInfo.Visible:
    # Get cursor position
    x, y = cursor.PointerPositionInfo.Position.x, cursor.PointerPositionInfo.Position.y
    print(f"Cursor position: ({x}, {y})")
    
    # Cursor shape information is also available
    if cursor.Shape is not None:
        width = cursor.PointerShapeInfo.Width
        height = cursor.PointerShapeInfo.Height
        print(f"Cursor size: {width}x{height}")
```

#### Advanced Cursor Handling

The cursor information provided by RapidShot can be used in various ways:

1. **Overlay cursor on captured image:**

```python
import numpy as np
import cv2

def overlay_cursor(frame, cursor):
    """Overlay cursor on captured frame."""
    if not cursor.PointerPositionInfo.Visible or cursor.Shape is None:
        return frame
    
    # Create an overlay from cursor shape data
    shape_type = cursor.PointerShapeInfo.Type
    width = cursor.PointerShapeInfo.Width
    height = cursor.PointerShapeInfo.Height
    
    # Different processing based on cursor type (monochrome, color, or masked)
    if shape_type & DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME:
        pass  # Process monochrome cursor
    elif shape_type & DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR:
        pass  # Process color cursor
    elif shape_type & DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR:
        pass  # Process masked color cursor
    
    # Position the cursor on the frame at its current coordinates
    x, y = cursor.PointerPositionInfo.Position.x, cursor.PointerPositionInfo.Position.y
    
    # Ensure cursor is within frame boundaries
    # ...
    
    # Blend cursor with frame
    # ...
    
    return frame_with_cursor

# Usage example
frame = screencapture.grab()
cursor = screencapture.grab_cursor()
composite_image = overlay_cursor(frame, cursor)
```

2. **Track cursor movements:**

```python
import time

# Record cursor positions over time
positions = []
screencapture = rapidshot.create()

for i in range(100):
    cursor = screencapture.grab_cursor()
    if cursor.PointerPositionInfo.Visible:
        positions.append((
            time.time(),
            cursor.PointerPositionInfo.Position.x,
            cursor.PointerPositionInfo.Position.y
        ))
    time.sleep(0.05)  # Sample at 20Hz

# Analyze cursor movement
# ...
```

## Multiple Monitors / GPUs

```python
# Show available devices and outputs
print(rapidshot.device_info())
print(rapidshot.output_info())

# Create ScreenCapture instances for specific devices/outputs
capture1 = rapidshot.create(device_idx=0, output_idx=0)  # First monitor on first GPU
capture2 = rapidshot.create(device_idx=0, output_idx=1)  # Second monitor on first GPU
capture3 = rapidshot.create(device_idx=1, output_idx=0)  # First monitor on second GPU
```

### Frame buffers

`grab()` returns a `PooledBuffer` — a reused buffer rather than a freshly
allocated array. Allocating one per frame costs ~1.6 ms on a 1080p RGB frame,
because the page faults on first touch cost more than the conversion itself;
reusing buffers makes `grab()` **1.3–2.1× faster**.

It behaves like the array it wraps, so most code is unchanged:

```python
frame = camera.grab()
if frame is not None:
    frame.shape, frame.dtype, frame.ndim   # as before
    pixel = frame[y, x]                    # indexing works
    arr = np.asarray(frame)                # zero-copy, for cv2 / PIL / models
    frame.release()                        # the one new line
```

**Release when done.** The buffer goes back to the pool and is handed to the
next capture, so anything still holding it would see the wrong frame. Reading it
after release raises `BufferReleasedError` rather than returning stale pixels.
To keep data beyond the release, `frame.copy()`.

Forgetting to release is not fatal: the pool runs dry and capture falls back to
allocating, which is slower but always correct. It will never hand you a buffer
another caller is reading.

**Migrating from 1.x.** Add `release()`, and wrap in `np.asarray()` anywhere a
true `ndarray` is required (`isinstance` checks, `Image.fromarray`). Or keep the
old behaviour outright:

```python
camera = rapidshot.create(pool_output=False)   # returns plain ndarrays
```

BGRA already worked this way before 2.0 — it does no conversion, so its staging
buffer was always returned pooled.

### Trading CPU for frames

Each capture waits up to `timeout_ms` for the compositor to present. That one
number is the whole difference between this library and the poll-based ones:

```python
camera = rapidshot.create(timeout_ms=0)   # poll instead of waiting
camera.timeout_ms = 10                    # or change it on a live camera
```

Measured on a 100 Hz output against a source presenting at ~610 updates/s:

| `timeout_ms` | frames/s | hit rate | CPU |
| --- | --- | --- | --- |
| 0 (poll) | 127.8 | 2.4% | 68.6% |
| 1 | 119.2 | 74.5% | 19.5% |
| **10 (default)** | 118.9 | 100% | **15.7%** |

The frame rate barely moves across that range and the CPU moves by more than
four times. Polling is what DXcam does — it calls `AcquireNextFrame(0)` and
reached 134 fps at 66% CPU in the same comparison, so those extra frames are
real, just expensive. Use `0` if capture is the only thing the machine is doing;
leave the default if it is one stage of a pipeline that needs its cores.

Frames beyond the display's refresh rate were never shown as distinct images —
useful as extra temporal samples for a model, redundant for a recorder.

### Only process what changed

`grab_frame()` frames carry the compositor's own dirty-rect metadata, so a
consumer can skip regions that did not change:

```python
with camera.grab_frame() as frame:
    if frame.dirty_rects is None or not frame.dirty_rects:
        process_everything(frame)          # unknown, or no metadata reported
    else:
        for left, top, right, bottom in frame.dirty_rects:
            process_region(frame, left, top, right, bottom)
```

Coordinates are relative to the frame, so they index straight into the captured
image even when `region=` is in use.

Two things to get right. An **empty list does not mean nothing changed** — it
means no rects were reported, which a mode change or a coalescing driver can
also produce while the image differs completely; treat it as "assume everything
changed". And `None` means the metadata could not be read at all. Check
`frame.rects_coalesced` too: when true the driver merged rects, so they
over-estimate what actually changed.

### Hybrid GPU laptops and headless machines

`device_info()` only lists adapters that drive a display, because only those can
capture. `topology_info()` lists every adapter and says what that means:

```python
print(rapidshot.topology_info())
```

```
Topology: hybrid
  Adapter[0] (Intel(R) UHD Graphics) (Intel) (128MB VRAM) (1 output)
  Adapter[1] (NVIDIA GeForce RTX 4070 Laptop GPU) (NVIDIA) (8192MB VRAM) (0 outputs)

  Hybrid GPU system detected. Capture runs on Intel(R) UHD Graphics, which
  drives the display. NVIDIA GeForce RTX 4070 Laptop GPU has no outputs, so
  Desktop Duplication cannot run against it at all (DXGI_ERROR_UNSUPPORTED).
  ...
```

On an Optimus/switchable laptop the discrete GPU has no outputs, so Desktop
Duplication cannot run against it — capture is always bound to the adapter that
drives the display. `grab()` is unaffected. A GPU-resident frame is: it lives on
the capture adapter, so feeding it to a model on the *other* adapter needs a
cross-adapter copy. `rapidshot.native` provides one:

```python
from rapidshot import native

with camera.grab_frame() as frame:
    transfer = native.cross_adapter_transfer(frame)   # build once, reuse

with camera.grab_frame() as frame:
    transfer.transfer(frame)
    # Now bind transfer.destination_resource_address on the other adapter.
    print(transfer.source, "->", transfer.destination)
```

Costs about 0.87 ms per 1080p frame — roughly a third of what reading the same
frame to the CPU costs, so crossing adapters beats leaving the GPU. The shared
heap lives in **system memory**, not either adapter's VRAM; this is not
peer-to-peer VRAM-to-VRAM DMA, and the win is that a GPU copy engine moves the
bytes instead of CPU cores.

On a machine with no monitor attached there is no desktop to duplicate at all,
and `rapidshot.create()` raises `HeadlessError` explaining that a virtual
display driver (IDD) is needed. `topology_info()` still works there — it probes
DXGI directly rather than going through capture.

> A virtual display's advertised refresh rate does **not** raise capture rate.
> Desktop Duplication is driven by presents, not by refresh: a 500 Hz virtual
> display does not make an application render 500 fps.

## Advanced Usage

### Custom Buffer Size

```python
# Create a ScreenCapture instance with a larger frame buffer
screencapture = rapidshot.create(max_buffer_len=256)
```

### Different Color Formats

```python
# RGB (default)                      -> (H, W, 3)
screencapture_rgb = rapidshot.create(output_color="RGB")

# RGBA (with alpha channel)          -> (H, W, 4)
screencapture_rgba = rapidshot.create(output_color="RGBA")

# BGR (OpenCV format)                -> (H, W, 3)
screencapture_bgr = rapidshot.create(output_color="BGR")

# BGRA (raw, no conversion)          -> (H, W, 4)
screencapture_bgra = rapidshot.create(output_color="BGRA")

# Grayscale (Rec. 601 luma)          -> (H, W, 1)
screencapture_gray = rapidshot.create(output_color="GRAY")
```

All conversions are pure NumPy — OpenCV is not required. An unsupported
`output_color` raises `ValueError` at creation time.

### Capturing Into Your Own Buffer

`shot()` writes straight into a buffer you own, avoiding a per-frame
allocation. The buffer receives pixels in the instance's `output_color`, so
size it with `bytes_per_frame()`:

```python
import numpy as np

screencapture = rapidshot.create(output_color="RGB", region=(0, 0, 640, 480))

buffer = np.zeros((480, 640, screencapture.channels), dtype=np.uint8)
if screencapture.shot(buffer):
    print("captured", buffer.shape)   # (480, 640, 3)
```

The destination size is checked before anything is written, so an undersized
buffer raises `ValueError` instead of corrupting memory. NumPy arrays,
`ctypes` arrays, `bytearray` and `memoryview` all report their own size. A raw
pointer cannot, so it must be paired with an explicit `buffer_size`:

```python
import ctypes

screencapture.shot(
    ctypes.c_void_p(buffer.ctypes.data),
    buffer_size=buffer.nbytes,
)
```

### GPU-Resident Capture (no CPU round-trip)

`grab()` brings every frame down to the CPU — a staging read plus a color
conversion, about 4.5 ms per 1080p frame. If you are handing the pixels to a GPU
consumer (an inference runtime, a hardware encoder), `grab_frame()` skips all of
that and gives you the Direct3D texture directly:

```python
with screencapture.grab_frame() as frame:
    texture = frame.d3d11_texture        # ID3D11Texture2D, valid in here only
    print(frame.timestamp, frame.accumulated_frames)
```

Both paths are measured in `benchmarks/baseline.json`, but **neither is a stable
number**: each depends on what is on screen, because conversion is limited to the
regions that changed. Over seven recordings of unchanged code, `grab_frame()`
ranged 0.17–0.83 ms and `grab()` 1.65–4.53 ms. The gap is real and consistently
in `grab_frame()`'s favour — it skips the CPU round-trip entirely — but quote the
range, not a ratio. See ROADMAP.md section 3.

> **The `with` block is not optional.** Direct3D cannot capture the next frame
> while a reference to the previous one is outstanding, so an unreleased `Frame`
> stalls capture entirely. Use the context manager (or call `frame.release()`),
> and copy anything you need out before the block ends. `grab()`, `shot()` and
> `grab_frame()` all raise a clear error if a frame is still outstanding.

Frame metadata stays readable after release: `timestamp` / `timestamp_qpc` (when
the compositor presented the frame), `accumulated_frames` (greater than 1 means
the OS dropped frames because your loop fell behind), `protected_content`,
`cursor_visible`, `region`, `width`, `height`, `rotation_angle`.

### GPU Inference: Handing Frames to DirectML

Rapidshot can convert a captured frame into a **model-ready NCHW float32 tensor
that never leaves the GPU** — no staging read, no colour conversion, no
resize on the CPU. This needs the optional native extension (see below).

```python
from rapidshot import native

with screencapture.grab_frame() as frame:
    pre = native.GpuPreprocessor12(frame, 640, 640)   # build once, reuse
    pre.process(frame)                                 # one GPU dispatch

    print(pre.shape)                        # (1, 3, 640, 640)
    resource = pre.output_resource_address  # ID3D12Resource*
    gpu_va = pre.output_gpu_address         # GPU virtual address
```

`process()` resizes, normalises, converts BGRA→RGB and transposes to NCHW in a
single compute shader. On the CPU that same work costs about **8 ms per 1080p
frame**; here it is one dispatch and the result stays in VRAM. Optional
arguments cover the usual normalisation ranges (`scale=2.0, bias=-1.0` for
−1..1) and channel order (`bgr=True`).

**Where Rapidshot stops.** The output is an `ID3D12Resource` on the DirectML
device — exactly what ONNX Runtime's DirectML provider consumes. Rapidshot
deliberately does **not** bind it to a session: that would couple this library
to ONNX Runtime's ABI and release cadence for the sake of an optional feature.
Consuming it is a few lines on your side:

```cpp
const OrtDmlApi* dml = nullptr;
Ort::GetApi().GetExecutionProviderApi(
    "DML", ORT_API_VERSION, reinterpret_cast<const void**>(&dml));

void* allocation = nullptr;
dml->CreateGPUAllocationFromD3DResource(d3d12_resource, &allocation);

Ort::MemoryInfo info("DML", OrtDeviceAllocator, 0, OrtMemTypeDefault);
auto tensor = Ort::Value::CreateTensor(
    info, allocation, byte_size,
    shape.data(), shape.size(), ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT);

// bind with Ort::IoBinding, run, then:
dml->FreeGPUAllocation(allocation);
```

> **Note for Python users:** `OrtDmlApi` has no Python binding — it is reachable
> only from C/C++. That is a gap in ONNX Runtime, not in Rapidshot. If you need
> this from Python today you will need a small native shim of your own;
> `native.probe_onnxruntime()` and `native.onnxruntime_dll_path()` are provided
> to help locate and validate the runtime.

### Building the Optional Native Extension

Everything above except GPU-tensor interop works with no toolchain.
`pip install rapidshot` never requires Rust.

```bash
cd native && cargo build --release
```

```bash
python native/install_dev.py
```

Requires [Rust](https://rustup.rs) and the MSVC C++ build tools. Check
availability at runtime with `rapidshot.native.is_available()`.

### Benchmarking Your Changes

```bash
python benchmarks/perf_suite.py --out baseline.json
python benchmarks/perf_suite.py --out after.json --compare baseline.json
```

The suite compares minimum samples, calibrates against a control benchmark to
divide out machine drift, and pools samples across rounds. Run
`--self-test` to measure your machine's noise floor before trusting a result.

### Resource Management

```python
# Release resources when done
screencapture.release()

# Or automatically released when object is deleted
del screencapture

# Clean up all resources
rapidshot.clean_up()

# Reset the library completely
rapidshot.reset()
```

## Benchmarks and Performance Comparison

RapidShot includes benchmark utilities to compare its performance against other popular screen capture libraries. The benchmark scripts are located in the `benchmarks/` directory and are designed to provide objective performance measurements.

### Benchmark Structure

- **FPS Benchmarks**: Measure the maximum frame rate achievable by each library
  - `rapidshot_max_fps.py` - Tests RapidShot's maximum FPS
  - `bettercam_max_fps.py` - Tests BetterCam's maximum FPS
  - `dxcam_max_fps.py` - Tests DXCam's maximum FPS
  - `d3dshot_max_fps.py` - Tests D3DShot's maximum FPS
  - `mss_max_fps.py` - Tests MSS's maximum FPS

- **Capture Benchmarks**: Test the continuous capture performance
  - `rapidshot_capture.py` - Tests RapidShot's continuous capture
  - `bettercam_capture.py` - Tests BetterCam's continuous capture
  - `dxcam_capture.py` - Tests DXCam's continuous capture

### Running Benchmarks

To run a benchmark comparison:

```bash
# Run RapidShot benchmark
python benchmarks/rapidshot_max_fps.py

# Run with GPU acceleration
python benchmarks/rapidshot_max_fps.py --gpu

# Test with different color formats
python benchmarks/rapidshot_max_fps.py --color BGRA
```

### Benchmark Results

**Run them yourself.** This README used to carry a table of cross-library FPS
figures with no hardware, method or date attached, claiming 240+ for RapidShot
and 300+ with GPU acceleration. Both were unsupportable, though not for the
reason first given here: Desktop Duplication reports compositor *presents*, so
the ceiling is the rate at which the desktop is redrawn — which can exceed the
panel's refresh rate, and did in our own measurements (117 fps of distinct
frames on a 100 Hz display). The figures were unsupportable because no hardware
or method was attached to them, and because CuPy acceleration changes the
*conversion* cost rather than the rate at which frames arrive.

Published FPS claims in this space contradict each other badly — DXcam's README
reports DXcam at 239 fps, BetterCam's reports the same library at 39 — because
they come from different hardware with no shared harness. A number measured on
someone else's machine tells you nothing about yours.

What is measured, reproducibly, is the per-frame cost of RapidShot's own paths
(1920×1080, Intel iGPU, from `benchmarks/baseline.json`):

| Path | Per frame | Notes |
| --- | --- | --- |
| Colour conversion, BGRA→RGB | **0.31 ms** | 1.9 ms without the native extension |
| Colour conversion, BGRA→GRAY | **0.26 ms** | 9.4 ms without it |
| `shot()` → your buffer | **0.32 ms** | staging read plus conversion |
| `grab()` — end to end | 1.7–4.5 ms | **depends on screen activity, see below** |
| `grab_frame()` — texture stays on the GPU | 0.17–0.83 ms | same caveat |

The first three are synthetic and deterministic: they move when the library
changes and not otherwise, which is why they are the ones on the badges above.

**The last two are not stable measurements and should not be quoted as single
numbers.** `grab()` converts only the parts of the frame that changed, so its
cost tracks what is happening on screen; across seven recordings on unchanged
code `grab_frame()` alone spanned 0.17–0.83 ms. They are reported for shape, not
precision.

Neither figure is a capture rate. They are what the *calling thread* pays per
frame; frames still arrive only as fast as the compositor presents them. What
they mean is that capture stops being your bottleneck.

For how these compare against DXcam, BetterCam and mss — including the two
measurements where RapidShot loses — see the table at the top of this file, and
run `python benchmarks/compare_libraries.py --motion --with-motion` to reproduce
it on your own hardware. See `ROADMAP.md` section 3 for the full per-stage
breakdown and how each figure is measured.

## System Requirements

- **Operating System:** Windows 10 or newer. Windows only — Desktop Duplication
  has no cross-platform equivalent.
- **Python:** 3.9 through 3.14 (`pip` will refuse to install on anything older)
- **GPU:** Any GPU that drives a display. A CUDA-capable NVIDIA GPU is needed
  only for the optional CuPy acceleration.
- **RAM:** 8 GB+ (depending on the resolution and number of screencapture instances used)

### Troubleshooting

- **ImportError with CuPy:** Ensure you have compatible CUDA drivers installed.
- **Black screens when capturing:** Verify the application isn't running in exclusive fullscreen mode.
- **Low performance:** Experiment with different backends (NUMPY vs. CUPY) to optimize performance.

## Contributing

Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). It is mostly
a list of the things that will waste your time otherwise: live capture tests
need something moving on screen, CI cannot verify them at all, and a naive
benchmark comparison here once produced eleven false regressions on identical
code.


## License

This project is licensed under the MIT License - see the LICENSE file for details.

## Acknowledgments

RapidShot is a merged version of the following projects:

- Original DXcam by ra1nty
- dxcampil - PIL-based version
- DXcam-AI-M-BOT - Cursor support version
- BetterCam - GPU acceleration version
