HotPDF renders many PDF pages concurrently through two entry points: RenderLoadedPagesParallel, which returns an array of bitmaps, and RenderLoadedPagesParallelOrdered, which delivers each page to a callback in input order as soon as it is ready. The ordered form is the one that scales, because it never holds more than a bounded number of bitmaps in memory regardless of how many pages you asked for
The distinction is the whole article. Rendering 10,000 pages at 300 DPI into an array means 10,000 live bitmaps, which is not a throughput problem but an out-of-memory crash. Rendering them through an ordered callback with a queue depth of two means two live bitmaps, and the job runs in a fixed footprint no matter how long the document is
Why does parallel rendering need two stages?
Rendering a PDF page is two different jobs wearing one name. First the content stream is tokenised and compiled into a display list, which is CPU-bound and touches shared caches. Then the display list is played back into a bitmap, which is also CPU-bound but allocates heavily. Treating them as one indivisible unit forces a choice between serialising the cache-touching half and duplicating work
HotPDF splits them. The display-list cache lock protects only the frequency accounting, the LRU order, use counts and admission decisions; the compile itself runs outside the lock. After compiling, the worker re-queries the cache: if another worker published the same page in the meantime, it releases its own copy and takes a use count on the published entry. That keeps duplicate work bounded and prevents two entries for the same page, without ever serialising compilation across pages
The measured effect on a 32-page complex fixture is the part worth remembering. Total time on Win64 fell from 1,565 ms with one worker to 696 ms with four, roughly 2.25 times. Time to the first result fell from 1,255 ms to 63 ms, about 95 percent, because workers begin playing back finished display lists while others are still compiling instead of waiting at a full compile barrier
Ordered delivery and the reserved slot
The ordered API guarantees that your callback sees pages in the order you requested them, while workers finish in whatever order they finish. Completed bitmaps are published into an output slot indexed by input position, and the callback runs serially on the calling thread
Two rules make that bounded rather than merely ordered. The output queue count includes bitmaps currently being used by the callback, so a slow callback cannot let producers slip past the queue depth. And later pages may occupy at most queueDepth - 1 slots, because one slot is permanently reserved for the next page due for delivery. Without that reservation a slow first page can be locked out by finished later pages filling the queue, and the pipeline deadlocks at the head of the line with every worker idle
uses
HPDFDoc;
procedure TExportJob.PageReady(Sender: TObject; InputIndex,
PageIndex: Integer; Bitmap: TBitmap; var Cancel: boolean);
begin
// The bitmap is borrowed: valid for this call only. Consume it here
// (write to disk, encode, hash) and do not keep the reference
Bitmap.SaveToFile(Format('page-%.4d.bmp', [InputIndex + 1]));
Cancel := FUserCancelled;
end;
procedure TExportJob.Run;
var
Pdf: THotPDF;
Pages: array of Integer;
Info: THPDFParallelRenderPipelineInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('manual.pdf') <= 0 then
Exit;
SetLength(Pages, Pdf.LoadedPageCount);
for I := 0 to High(Pages) do
Pages[I] := I;
Pdf.RenderLoadedPagesParallelOrdered(Pages, 200, 4, 2,
PageReady, Info);
Writeln(Format('workers=%d peak queued=%d peak bytes=%d',
[Info.WorkerCount, Info.PeakQueuedPageCount, Info.PeakQueuedBytes]));
Writeln(Format('first result=%d ms total=%d ms backpressure waits=%d',
[Info.FirstResultMilliseconds, Info.TotalMilliseconds,
Info.BackpressureWaitCount]));
finally
Pdf.Free;
end;
end;
The borrowed-bitmap contract is the reason the footprint stays flat. The library releases the bitmap as soon as your callback returns, so a 10,000-page fixture at queue depth 2 peaks at exactly two bitmaps on both Win32 and Win64. If you need the bitmap after the callback, copy it, and then you own the memory and the accounting
How the memory budget picks a worker count
More workers is not always faster and is often fatal. Six workers rendering A3 pages at 600 DPI need several hundred megabytes of live bitmap regardless of how many cores are available, and the machine that has eight cores may not have that much free address space
HotPDF therefore normalises the requested worker count against the request size, the page count and a hard ceiling, then divides the currently available global budget by the largest conservative single-page estimate in the request. At least one worker always survives, so an oversized page still renders, exclusively. In the reference measurement, a six-page 600 DPI job under a 512 MiB budget ran with three workers rather than six, with a reservation peak of 504,583,296 bytes
Each worker takes a memory reservation after claiming a page and before compiling, and the reservation covers compilation and playback together. Reserving only around the raster stage would miss the display list and the content stream working set, which on graphics-heavy pages is the larger half. The two APIs then differ: the array form releases its reservation once the bitmap is complete, because the caller owns the result and the scheduler can no longer account for it, while the ordered form transfers the reservation along with the bitmap into the output slot and holds it until the callback returns. That is another reason to prefer the ordered API when the output set is large
Cancellation without deadlock
Setting Cancel in the callback stops new work and wakes any worker waiting on backpressure. Pages already inside the renderer finish and are cleaned up rather than abandoned, and only the first failure diagnostic is retained, raised after every worker has joined
The ordering here is load-bearing. Cancellation state is set first, unpublished output reservations are released second, and only then are the workers joined. Reversing the last two steps creates a closed loop: a worker waits for admission to a budget that will not be released until the consumer finishes, while the consumer waits for that worker to exit
Interactive applications should also know about foreground pre-emption. Page prefetching runs on its own cancellation tokens and the foreground rendering calls cancel and join the prefetch workers before doing their own work, so background throughput never adds latency to the page a user is waiting for. Cancellation is checked at intervals bounded by 4 KiB of byte scanning or 256 tokens, though an atomic codec or OS call cannot be interrupted mid-flight — the responsiveness guarantee covers library-owned loops, not third-party decoders. The queue-based approach to interactive rendering is described in background rendering with a request queue
Reading the pipeline statistics
THPDFParallelRenderPipelineInfo separates numbers that are easy to conflate. RequestedWorkerCount against WorkerCount and MemoryBudgetWorkerLimit tells you whether the budget reduced your concurrency. CompiledPageCount against CacheHitPageCount tells you how much the display-list cache saved. CompiledPagesAtFirstResult shows whether the pipeline really overlapped compile and playback or degenerated into a barrier
For tuning, the two most useful are BackpressureWaitCount and MemorySchedulerWaitMilliseconds. High backpressure waits mean your callback is the bottleneck, so either make it faster or raise the queue depth if memory allows. High scheduler waits mean the budget is the bottleneck, so reduce DPI, reduce concurrency, or raise the budget. Raising worker count in either case makes throughput worse, which is the counterintuitive part
A practical starting point for batch export: concurrency equal to physical cores minus one, queue depth two or three, and DPI chosen from the actual output requirement rather than from habit. Then read the statistics from a real document and adjust once, rather than guessing twice. For jobs where the document itself is too large to hold in memory, combine this with the streaming access model in the direct file API for large PDF workflows
Parallel rendering, prefetching and the display-list cache are all part of the same rendering stack for Delphi and C++Builder; the complete feature list is on the HotPDF Delphi PDF component page