The rasterrocket crate is the integration entry point. It wraps rasterrocket-interp and rasterrocket-render behind a simple, stable API.
pub fn raster_pdf(
path: &Path,
opts: &RasterOptions,
) -> impl Iterator<Item = (u32, Result<RenderedPage, RasterError>)>Renders a range of pages from a PDF file. Returns an iterator that yields (page_num, result) for each page in opts.first_page..=opts.last_page.
Behaviour:
- Pages are rendered in ascending order.
- A per-page error does not abort remaining pages. The caller decides whether to skip or propagate.
- If
opts.last_pageexceeds the document's page count, rendering stops at the last page — no error is returned for the overshoot. - GPU resources are initialised lazily on first use and reused across pages.
Errors (yielded as iterator items):
RasterError::InvalidOptions—optsviolates constraints (e.g.dpi ≤ 0,first_page > last_page). Yielded as(1, Err(...)), iterator ends immediately.RasterError::Pdf— document cannot be opened or parsed. Yielded as(1, Err(...)), iterator ends immediately.RasterError::PageOutOfRange— page number exceeds document length.RasterError::PageDegenerate— page has zero pixel dimensions.RasterError::PageTooLarge— pixel dimensions exceedMAX_PX_DIMENSION(32 768).RasterError::InvalidPageGeometry—UserUnitoutside[0.1, 10.0].RasterError::Deskew— deskew rotation failed (rare; graceful fallback applied when possible).
pub fn raster_pdf_from_bytes(
bytes: Vec<u8>,
opts: &RasterOptions,
) -> impl Iterator<Item = (u32, Result<RenderedPage, RasterError>)>Identical to raster_pdf but renders a PDF held entirely in
memory — no file path, no temp file. For callers that already hold the document
bytes (e.g. a PDF extracted from an archive). Backend selection, page ordering,
and the per-page error contract are the same as raster_pdf.
Note: the in-memory path has no transparent-decrypt step (that path
decrypts to a temp file, which only applies to an on-disk source). An encrypted
PDF passed as bytes yields RasterError::Pdf (encrypted-document) rather than
being decrypted — extract and render it from a path with decryption authorised.
pub fn render_channel(
path: &Path,
opts: &RasterOptions,
capacity: usize,
) -> std::sync::mpsc::Receiver<(u32, Result<RenderedPage, RasterError>)>Renders pages concurrently in a background Rayon task, streaming results through a bounded sync channel.
capacity — maximum number of rendered pages buffered before the producer blocks (natural backpressure). capacity = 0 is silently raised to 1. Use 2–8 for typical OCR pipelines.
Error contract:
- Options validation failure →
(1, Err(RasterError::InvalidOptions(...))), channel closes. - File open failure →
(1, Err(RasterError::Pdf(...))), channel closes. - Per-page failures →
(page_num, Err(...)), remaining pages continue.
If the Receiver is dropped before the producer finishes, the producer exits cleanly on its next send — no panic.
Lower-level API for explicit control over PDF opening and per-page rendering. All items are also re-exported at the crate root for backward compatibility.
pub mod session {
pub use super::{
open_session, open_session_from_bytes, prescan_session,
render_page_rgb, render_page_rgb_hinted, rgb_to_gray,
};
}pub fn open_session(path: &Path, config: &SessionConfig) -> Result<RasterSession, RasterError>
pub fn open_session_from_bytes(bytes: Vec<u8>, config: &SessionConfig) -> Result<RasterSession, RasterError>open_session_from_bytes is the in-memory counterpart to open_session (the session-level analogue of raster_pdf_from_bytes): same behaviour, taking owned PDF bytes instead of a path, with no transparent-decrypt step (an encrypted in-memory PDF errors rather than being decrypted).
Opens the PDF and builds an O(1) page-ID map. Also initialises the shared GPU context (for gpu-aa / gpu-icc features) according to config.policy. Errors with RasterError::Pdf if the file is unreadable or corrupt. A JavaScript-bearing PDF is not an error — it opens and renders normally; a loud WARN is emitted per detected JS entry point (see open below) and no /JS is ever decoded or executed. Errors with RasterError::BackendUnavailable if config.policy is ForceCuda or ForceVaapi and the required GPU cannot be initialised.
pub fn render_page_rgb(
session: &RasterSession,
page_num: u32,
scale: f64,
) -> Result<Bitmap<Rgb8>, RasterError>Renders one page to an RGB bitmap. scale is the pixel-per-point multiplier: dpi / 72.0 for uniform DPI, or sqrt((rx/72) × (ry/72)) for non-square pixels.
Safe to call from multiple Rayon threads concurrently. GPU image decoders are managed per-thread via thread_local!.
pub fn rgb_to_gray(src: &Bitmap<Rgb8>) -> Bitmap<Gray8>BT.709 luminance conversion: Y = 0.2126·R + 0.7152·G + 0.0722·B.
pub fn prescan_session(
session: &RasterSession,
page_num: u32,
) -> Result<PageDiagnostics, RasterError>Classifies page page_num without rendering any pixels. Returns PageDiagnostics with image/text presence, dominant filter, and a PPI hint. Use this before render_page_rgb to choose the backend policy (e.g. skip GPU init for vector-only pages).
Errors with RasterError::PageOutOfRange or RasterError::InvalidPageGeometry if the page is invalid.
pub struct RasterSession { /* opaque */ }
impl RasterSession {
pub const fn total_pages(&self) -> u32
pub const fn policy(&self) -> BackendPolicy
// doc() and resolve_page() are #[doc(hidden)] — use prescan_session instead
}An opened, read-only document. Sync + Send — safe to share across Rayon threads.
#[derive(Debug, Clone)]
pub struct RasterOptions {
pub dpi: f32,
pub first_page: u32,
pub last_page: u32,
pub deskew: bool,
pub pages: Option<PageSet>,
}| Field | Constraints | Notes |
|---|---|---|
dpi |
> 0, finite |
Render resolution. Pass effective_dpi (not dpi) to Tesseract. |
first_page |
≥ 1 |
1-based inclusive. Ignored when pages is Some. |
last_page |
≥ first_page |
1-based inclusive. Clamped to document length silently. Ignored when pages is Some. |
deskew |
— | Applies intensity-weighted projection-profile deskew (±7°, sub-0.05° accuracy). Disable for native-text PDFs. |
pages |
— | When Some, only the pages in the PageSet are rendered; first_page/last_page are ignored. |
Default: RasterOptions implements Default:
// Short form (dpi only, everything else defaults)
let opts = RasterOptions { dpi: 150.0, ..RasterOptions::default() };Defaults: dpi = 300.0, first_page = 1, last_page = u32::MAX, deskew = false, pages = None.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageSet(/* opaque Arc<[u32]> */);
impl PageSet {
pub fn new(pages: impl IntoIterator<Item = u32>) -> Result<Self, RasterError>
pub fn contains(&self, page: u32) -> bool // O(log n)
pub fn first(&self) -> u32
pub fn last(&self) -> u32
pub fn len(&self) -> usize
pub fn is_empty(&self) -> bool // always false by invariant
}A validated, sorted, deduplicated set of 1-based page numbers. The internal storage is reference-counted (Arc<[u32]>); clone() is O(1). Used in RasterOptions::pages to render a sparse subset of pages without visiting intermediate ones.
PageSet::new accepts any IntoIterator<Item = u32> (Vec, array, slice, range). The input is sorted and deduplicated before storage. Returns RasterError::InvalidOptions if the resulting set is empty or contains a zero page number.
let pages = PageSet::new(vec![1, 5, 23, 100])?;
let opts = RasterOptions {
dpi: 300.0,
first_page: 1, // ignored — PageSet controls the range
last_page: u32::MAX, // ignored — PageSet controls the range
deskew: true,
pages: Some(pages),
};pub struct RenderedPage {
pub page_num: u32,
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
pub dpi: f32,
pub effective_dpi: f32,
pub diagnostics: PageDiagnostics,
}| Field | Notes |
|---|---|
page_num |
1-based page number. |
width, height |
Output bitmap dimensions in pixels. |
pixels |
8-bit grayscale, tightly packed (stride == width), top-to-bottom. Length is exactly width * height. |
dpi |
Raw render DPI (opts.dpi). |
effective_dpi |
opts.dpi × UserUnit. Pass this to tesseract::set_source_resolution. For the vast majority of documents UserUnit = 1.0 and this equals dpi. |
diagnostics |
Lightweight rendering metadata — see PageDiagnostics. |
pub use rasterrocket_interp::renderer::PageDiagnostics;Collected at zero extra cost during rendering.
| Field | Type | Notes |
|---|---|---|
has_images |
bool |
At least one image XObject or inline image was rendered. |
has_vector_text |
bool |
At least one text-showing operator (Tj, TJ, ', ") was executed. false on scan-only pages. |
dominant_filter |
Option<ImageFilter> |
Most common image decode filter on this page (None for pure-vector pages). |
source_ppi_hint |
Option<f32> |
Estimated native pixels-per-inch of the dominant image. Computed as (image_width_px / page_width_pts) × 72. None when no images were blitted. |
pub fn suggested_dpi(&self, min_dpi: f32, max_dpi: f32) -> Option<f32>Suggests a re-render DPI based on source_ppi_hint. Returns None for vector/text-only pages. Snaps to the nearest standard step (72, 96, 150, 200, 300, 400, 600) and clamps to [min_dpi, max_dpi].
if let Some(native_dpi) = page.diagnostics.suggested_dpi(150.0, 600.0) {
if (native_dpi - opts.dpi).abs() > 10.0 {
// Re-render at native resolution
}
}Use diagnostics to route pages to different OCR configurations:
// Page looks like a scan (has images, no vector text)
if page.diagnostics.has_images && !page.diagnostics.has_vector_text {
// Sauvola handles uneven scan backgrounds better than Otsu
tesseract.set_variable("thresholding_method", "2");
}
// Avoid deskew overhead on native-text pages
if !page.diagnostics.has_images && page.diagnostics.has_vector_text {
// deskew: false for this page
}#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BackendPolicy {
Auto, // GPU when available, silent CPU fallback (default)
CpuOnly, // Skip all GPU init entirely
ForceCuda, // Require CUDA; error if unavailable
#[cfg(feature = "vaapi")]
ForceVaapi, // Require VA-API JPEG; error if unavailable — only when `vaapi` feature is enabled
ForceVulkan, // Require the Vulkan compute backend; error if unavailable
// (or if the binary was built without `--features vulkan`)
}Controls which compute backend is used. Auto matches pre-v0.4.0 behaviour. The Force* variants convert silent GPU fallbacks into hard RasterError::BackendUnavailable errors so you know immediately whether the expected hardware path is actually active. ForceVaapi is only present when the vaapi Cargo feature is enabled.
ForceVulkan runs the AA-fill, tile-fill, and parallel-Huffman JPEG decode kernels on the Vulkan compute backend (cross-vendor: NVIDIA, AMD, Intel, Apple via MoltenVK). The device-resident image cache is CUDA-only, so under ForceVulkan the renderer runs uncached; ICC CMYK→RGB stays on the CPU AVX-512 fallback. JPEG dispatch goes through the GPU parallel-Huffman path. The path is dormant by default (GPU_JPEG_HUFFMAN_THRESHOLD_PX = u32::MAX); enable it by setting PDF_RASTER_HUFFMAN_THRESHOLD=0 at runtime.
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct SessionConfig {
pub policy: BackendPolicy,
pub vaapi_device: String, // default: "/dev/dri/renderD128"
// `prefetch: bool` exists only when `cache` feature is enabled
}
impl Default for SessionConfig { /* Auto policy, default DRM node */ }Passed to open_session. SessionConfig is #[non_exhaustive] — construct it via SessionConfig::default() or SessionConfig::with_policy(policy), not a struct literal. This is required because the prefetch field only exists when the cache Cargo feature is enabled. Set vaapi_device to override the VA-API DRM render node (useful when /dev/dri/renderD128 is not the correct device on your system).
The deskew module exposes a limited public API:
deskew::apply— applies a deskew angle to a bitmap.deskew::DeskewError— error type returned byapply.
The deskew::detect and deskew::rotate submodules are pub(crate) and are not part of the public API.
pub fn release_gpu_decoders()Eagerly drops GPU decoder state (nvJPEG, nvJPEG2000, Vulkan Huffman) on the calling rayon worker thread. Call via pool.broadcast before dropping the pool:
let _ = pool.broadcast(|_| rasterrocket::release_gpu_decoders());
drop(pool);No-op when none of the GPU decoder features (nvjpeg, nvjpeg2k, gpu-jpeg-huffman + vulkan) are compiled in. Never panics.
pub enum RasterError {
InvalidOptions(String),
Pdf(rasterrocket_interp::InterpError),
PageOutOfRange { page: u32, total: u32 },
PageDegenerate { width: u32, height: u32 },
PageTooLarge { width: u32, height: u32 },
PageAreaTooLarge { width: u32, height: u32, area: u64 },
Deskew(String),
InvalidPageGeometry(String),
BackendUnavailable(String), // forced backend failed to init
ImageDecodeFailed(Vec<String>),
PageBudgetExceeded(String),
RenderPanic { /* isolated per-page panic */ },
}PageAreaTooLarge is distinct from PageTooLarge: each side can be within
MAX_PX_DIMENSION while the product still exceeds MAX_PX_AREA. RenderPanic
carries an isolated per-page panic so one bad page cannot abort a batch.
Implements std::error::Error with a source() chain. RasterError::Pdf(e) has e as its source for chained error reporting. BackendUnavailable is only returned when SessionConfig.policy is ForceCuda or ForceVaapi.
pub const MAX_PX_DIMENSION: u32 = 32_768;
pub const MAX_PX_AREA: u64 = 600_000_000;MAX_PX_DIMENSION is the maximum accepted pixel dimension (width or height);
PageTooLarge is returned if either exceeds it. At 150 DPI this corresponds to
~366 inches (~9.3 metres).
MAX_PX_AREA caps total raster area and is computed in u64 (closing a latent
overflow). A page whose sides are each legal but whose product exceeds this
returns PageAreaTooLarge.
pub use rasterrocket_interp::renderer::PageDiagnostics;
pub use rasterrocket_interp::resources::ImageFilter;ImageFilter identifies which decode filter was used for an embedded image (DCTDecode, JPXDecode, FlateDecode, etc.). Available through PageDiagnostics for routing decisions.
pub fn encode_for_gcv(
page: &RenderedPage,
budget: &GcvBudget,
) -> Result<GcvImage, GcvError>;
pub struct GcvBudget {
pub max_base64_bytes: usize, // default 10 * 1024 * 1024
pub min_quality: u8, // default 60
pub start_quality: u8, // default 90
}
pub struct GcvImage {
pub jpeg: Vec<u8>, // fitted baseline grayscale JPEG bytes
pub quality: u8,
pub width: u32,
pub height: u32,
}
impl GcvImage {
pub fn to_base64(&self) -> String;
}
pub enum GcvError {
Unfittable { smallest_base64: usize, budget: usize },
Encode(rasterrocket_encode::EncodeError),
}Encodes a RenderedPage to a grayscale JPEG guaranteed to fit Google Cloud Vision's binding request limit — 10 MB of base64 inside the images:annotate JSON request, not the 20 MB raw-file limit — decided deterministically with no network call.
Strategy: try start_quality at native resolution; binary-search quality down toward min_quality; only if the quality floor still overflows, box-downscale (aspect-preserving, never below GCV's ~1024 px OCR short-side floor, never over the 75 MP server-side-resize cap) and retry. If no candidate fits without breaking those floors, returns GcvError::Unfittable — never an over-budget or oversized payload. GcvImage::to_base64() reproduces exactly the length measured during fitting (the proxy is byte-exact against the real encoding).
GcvBudget::default() encodes the GCV limits directly. Pass RasterOptions { deskew: false, .. } for this path — GCV deskews internally. The raw jpeg field is the universal artifact (disk, GCS files:asyncBatchAnnotate); to_base64() is the inline-content form. No HTTP/auth/batching is performed — rasterrocket renders pixels, it is not a GCV API client. See the LLM Vision OCR Integration wiki for end-to-end examples.
The rasterrocket-encode crate also exposes the underlying codec directly:
pub fn jpeg_gray<P: Pixel>(bitmap: &Bitmap<P>, quality: u8)
-> Result<Vec<u8>, EncodeError>;Baseline 8-bit grayscale JPEG (Gray8/Mono8 only; quality clamped 1–100). A plain codec with no consumer-specific policy.
Turns a comic/scan archive (.cbz ZIP, .cb7 7-Zip, .cbt TAR) of page
images into the same grayscale RenderedPage stream the PDF path produces. Use
it when your input is an image archive rather than a PDF.
pub fn open_comic(
path: &Path,
opts: &ComicOptions,
) -> Result<Vec<(u32, Result<RenderedPage, ComicError>)>, ComicError>Opens an archive and renders its pages. Returns a Vec of (page_num, result)
in reading order; a per-page decode failure is an inner Err and does not abort
the rest, matching raster_pdf.
Content routing (decided after the entry list is built):
- Page images present → render the images (numeric-aware natural sort by filename, magic-byte format detection over JPEG/PNG/WebP/TIFF). Any PDF entry is ignored with a warning — images always win.
- No images, exactly one PDF → render that PDF via
raster_pdf_from_bytes(the PDF's own page numbering). - No images, two or more PDFs →
ComicError::AmbiguousArchive, naming the PDFs rather than guessing. - No images, no PDF →
ComicError::NoImages.
Whole-archive errors (outer Err): RarUnsupported (a .cbr input),
Open/BadArchive (unreadable or malformed container), AmbiguousArchive,
NoImages. Per-page errors (inner Err): Decode, TooLarge, Deskew,
Pdf.
ComicOptions mirrors the relevant subset of RasterOptions: dpi: f32,
first_page: u32, last_page: u32, deskew: bool. The DPI rides along on each
RenderedPage for downstream OCR; comic images carry no intrinsic resolution.
Hardening: entries are read under a 512 MiB per-entry decompressed-size cap
(decompression-bomb defence), and image dimensions are validated before a codec
allocates its pixel buffer (decode-bomb defence). .cbr (RAR) is refused — no
permissively-licensed Rust RAR decoder is linked.
Direct use of rasterrocket-interp is not required for most consumers. Use it when building a custom render loop or accessing document metadata without rendering.
pub fn open(path: impl AsRef<Path>) -> Result<pdf::Document, InterpError>Opens and validates a PDF. If a JavaScript entry point is detected (checked locations: /OpenAction, catalog /AA, /Names/JavaScript, /AcroForm/AA, and page-level /AA / per-annotation /A//AA, each flagged only when genuinely a /S /JavaScript action), a loud WARN is emitted per location and the document still opens — rasterrocket has no JavaScript engine and never executes /JS, so a script's structural presence cannot change the static rendered page. No JS is parsed or evaluated. The page/annotation walk is bounded and stops at the first hit, so a pathological multi-thousand-page document may not be fully scanned.
The returned pdf::Document comes from the in-tree pdf crate (replaced lopdf 0.40 in v0.6.0). It is a lazy, mmap-backed reader: opening the file parses only the xref table and trailer; individual objects resolve on demand via byte-offset seek with a per-object Arc cache shared safely across worker threads. Object stream (ObjStm) decompression is cached once and reused. The API mirrors the lopdf method names previously used here.
pub fn page_count(doc: &Document) -> u32Total pages. Saturates at u32::MAX for pathological documents (> 4 billion pages).
pub fn page_size_pts(doc: &Document, page_num: u32) -> Result<PageGeometry, InterpError>Returns geometry for page page_num (1-based). width_pts and height_pts are already adjusted for rotation and UserUnit scaling — use them directly as output bitmap dimensions.
Falls back to US Letter (612 × 792 pt) when neither CropBox nor MediaBox can be read.
pub fn parse_page(doc: &Document, page_num: u32) -> Result<Vec<Operator>, InterpError>Parses the content stream for page page_num and returns the decoded operator sequence. Typically called internally by the renderer; exposed for tooling (e.g. dump_ops example).
pub struct PageGeometry {
pub width_pts: f64, // output width in PDF points (rotation + UserUnit applied)
pub height_pts: f64, // output height in PDF points (rotation + UserUnit applied)
pub rotate_cw: u16, // 0, 90, 180, or 270
pub user_unit: f64, // UserUnit scale factor, validated to [0.1, 10.0]
}Dimensions are swapped for 90°/270° rotations so that width_pts always corresponds to the horizontal extent of the rendered bitmap.
To get pixel dimensions: (width_pts × dpi / 72.0).round().
pub enum InterpError {
Pdf(pdf::PdfError),
PageOutOfRange { page: u32, total: u32 },
MissingResource(String),
InvalidPageGeometry(String),
FontInit(String),
PageBudget(String),
}Implements std::error::Error. InterpError::Pdf(e) chains to pdf::PdfError.
Supported: x86-64 (AMD and Intel) and aarch64 (ARM / Apple Silicon).
x86-64:
- AVX2 SIMD blend and fill paths are runtime-detected with a scalar fallback.
- AVX-512 (
avx512f/bw/vl/dq/vnni/vpopcntdqand related sub-extensions) is activated by building with-C target-cpu=nativeon a compatible CPU. Developed and benchmarked on an AMD Ryzen 9900X3D. - All Intel consumer CPUs (Alder/Raptor/Arrow Lake) have AVX2; AVX-512 is disabled on them — Xeon only.
aarch64:
- NEON is used unconditionally (mandatory on all ARMv8-A). No runtime detection needed.
- SVE2 (
svcnt_u8_zpopcount tier) is available behind thenightly-sve2Cargo feature on nightly Rust. Gives up to 4× NEON throughput on wide-SVE2 server chips (Graviton4 full width). cargo check --target aarch64-unknown-linux-gnuis clean; no Apple Metal native backend yet (Vulkan viaMoltenVKcovers Apple).
NVIDIA (CUDA 13.0–13.3):
| Feature flag | Minimum requirement | Notes |
|---|---|---|
nvjpeg |
CUDA-capable NVIDIA GPU | libnvjpeg.so ships with the CUDA 13 toolkit |
nvjpeg2k |
CUDA-capable NVIDIA GPU | libnvjpeg2k.so is a separate download; build script probes /13 then /12 |
gpu-aa |
CUDA-capable NVIDIA GPU | CUDA runtime only |
gpu-icc |
CUDA-capable NVIDIA GPU | CUDA runtime only |
gpu-deskew |
CUDA-capable NVIDIA GPU | Requires CUDA NPP: libnppig.so + libnppc.so |
cache |
CUDA-capable NVIDIA GPU | Phase 9 3-tier image cache (CUDA-only) |
cudarc is pinned to the cuda-13030 driver-API binding; a CUDA 13.x driver is required.
Vulkan compute (cross-vendor — NVIDIA, AMD, Intel, Apple via MoltenVK):
| Feature flag | Supported hardware | Libraries required |
|---|---|---|
vulkan |
Any Vulkan 1.3+ device | Vulkan ICD (e.g. mesa-vulkan-drivers, NVIDIA driver); slangc from the LunarG Vulkan SDK at build time. Implies gpu-aa. |
Vulkan covers the AA-fill, tile-fill, and parallel-Huffman JPEG decode kernels (vulkan implies gpu-jpeg-huffman). cache and nvjpeg stay CUDA-only; under --backend vulkan the renderer runs uncached but JPEG decode routes through the GPU parallel-Huffman path.
VA-API (Linux iGPU/dGPU — AMD VCN, Intel Quick Sync, Intel Arc):
| Feature flag | Supported hardware | Libraries required |
|---|---|---|
vaapi |
AMD VCN (Raphael+), Intel UHD 630+, Intel Arc | libva.so.2, libva-drm.so.2 |
VA-API provides hardware JPEG baseline decode. CMYK and progressive JPEGs fall through to CPU. When both nvjpeg and vaapi are active, nvJPEG takes priority; VA-API fires as fallback.
GPU initialisation failures at runtime print a warning to stderr and fall back to CPU — no error is returned, rendering continues normally.
| Platform | CPU SIMD | GPU acceleration | Status |
|---|---|---|---|
| x86-64 AMD (Ryzen) | AVX-512 + AVX2 | NVIDIA CUDA + AMD VA-API + Vulkan | Supported |
| x86-64 Intel (consumer) | AVX2 | NVIDIA CUDA + Intel VA-API + Vulkan | Supported |
| x86-64 Intel (Xeon) | AVX-512 + AVX2 | NVIDIA CUDA + Intel VA-API + Vulkan | Supported |
| aarch64 Linux (Graviton, RPi) | NEON + SVE2 † | Vulkan (Mesa) | CPU full, Vulkan untested on aarch64 |
| Apple Silicon (M1–M4) | NEON | Vulkan via MoltenVK (untested) |
CPU full, Vulkan untested |
| AMD/Radeon ROCm | — | — | Not implemented (Vulkan covers Radeon) |
| Apple Metal (native) | — | — | Not implemented (Vulkan via MoltenVK is the path) |
† SVE2 requires nightly-sve2 Cargo feature and nightly Rust.
| Feature | Requires | Effect |
|---|---|---|
nvjpeg |
CUDA 13, libnvjpeg.so |
GPU JPEG decode (DCTDecode). Falls back to CPU zune-jpeg below 512×512 px. |
nvjpeg2k |
CUDA 13, libnvjpeg2k.so |
GPU JPEG 2000 decode (JPXDecode). Falls back to CPU OpenJPEG below 512×512 px or for sub-sampled chroma. |
gpu-aa |
CUDA 13 | GPU supersampled AA fill (64-sample warp-ballot kernel). Falls back to CPU 4× scanline AA below 256 px. |
gpu-icc |
CUDA 13 | GPU ICC CMYK→RGB via 4D CLUT. Falls back to CPU AVX-512 matrix formula below 500 000 px. |
gpu-deskew |
CUDA 13, CUDA NPP | GPU bilinear rotation (nppiRotate). Falls back to CPU bilinear when disabled. |
cache |
CUDA 13 | Phase 9 device-resident image cache (3-tier VRAM/host/disk). Cross-document content-hash dedup. CUDA-only; no Vulkan support today. Disk-tier persistence is opt-in via PDF_RASTER_CACHE_DIR. |
vaapi |
libva.so.2, libva-drm.so.2 |
VA-API JPEG baseline decode on Linux iGPU/dGPU. Falls back to CPU on CMYK/progressive JPEG. When nvjpeg is also active, nvJPEG takes priority. |
vulkan |
Vulkan 1.3+ ICD; LunarG slangc at build time. Implies gpu-aa and gpu-jpeg-huffman. |
Vulkan compute backend. AA-fill, tile-fill, and parallel-Huffman JPEG decode kernels run on any Vulkan 1.3+ device (NVIDIA, AMD, Intel, Apple via MoltenVK). No nvJPEG / cache support under this backend. |
gpu-validation |
CUDA device at test time | Enables GPU vs CPU parity tests (cargo test -p rasterrocket-gpu --features gpu-validation). |
GPU initialisation failures print a warning to stderr and fall back to CPU — they do not return errors. cudarc is pinned to the cuda-13030 driver-API binding, so a CUDA 13.x driver is required.
| Path | Threshold | Constant |
|---|---|---|
| nvJPEG (DCTDecode) | disabled (u32::MAX) |
GPU_JPEG_THRESHOLD_PX |
| GPU parallel Huffman (DCTDecode) | dormant (u32::MAX) |
GPU_JPEG_HUFFMAN_THRESHOLD_PX |
| nvJPEG2000 (JPXDecode) | ≥ 262 144 px (area) | GPU_JPEG2K_THRESHOLD_PX |
| GPU AA fill | ≥ 256 px (longest edge) | GPU_AA_FILL_THRESHOLD |
| GPU tile fill | ≥ 256 px (longest edge) | GPU_TILE_FILL_THRESHOLD |
| GPU ICC CLUT | ≥ 500 000 px (area) | GPU_ICC_CLUT_THRESHOLD |
Both JPEG thresholds are u32::MAX by design, so JPEG decode runs on the
CPU by default: nvJPEG's GPU_HYBRID backend lost to 24-thread CPU decode on
consumer hardware, and the custom parallel-Huffman path is wired end-to-end but
off pending threshold tuning. Set PDF_RASTER_HUFFMAN_THRESHOLD=0 to enable the
Huffman path for benchmarking. nvJPEG2000 is genuinely active above its area
threshold.
Fill dispatch order: GPU tile fill → GPU AA fill → CPU scanline AA.