Vectorial Polygon Rasterizer (VPR)
Technical Architecture and Algorithmic Reference Overview
This document provides a technical, deep-dive description of the Vectorial Polygon Rasterizer (VPR) algorithm implemented in Graphics32 (Source/GR32_VPR.pas). Designed and implemented by Mattias Andersson, VPR is an analytical coverage-based polygon rasterizer that computes exact pixel coverage for optimal anti-aliasing without the performance bottlenecks of traditional scanline edge-sorting rasterizers.
1. Introduction & Theoretical Background
Traditional high-quality vector rasterizers, such as those in FreeType or Anti-Grain Geometry (AGG), compute anti-aliased coverage by tracking and sorting the horizontal crossings of polygon edges for every scanline. While highly accurate, the sorting step introduces an
VPR eliminates horizontal sorting entirely by leveraging a fundamental property of calculus: the cumulative prefix sum (prefix integration).
Core Architectural Concepts:
- Vertical Subdivision All polygon edges are subdivided vertically so that each segment fragment is exactly bounded within a single scanline (local
). - Horizontal Crossing Propagation (1D Delta Buffers) When an edge crosses a scanline boundary (specifically the bottom boundary at local
), it changes the winding state for all pixels to the right of that crossing. VPR represents this change as a local delta at the crossing coordinate. A subsequent horizontal cumulative prefix sum propagates this winding state across the scanline in time (where is the scanline width). - Local Signed Area Integration For pixels that contain an active edge fragment, VPR analytically computes the local signed trapezoidal area under the segment within that pixel column and adds it directly to the cumulative-summed base.
By separating the global boundary crossings (handled via horizontal 1D delta propagation) and local edge integration (handled analytically per pixel containing an edge), VPR achieves perfect analytical accuracy with outstanding linear
2. Architectural Pipeline & Data Structures
VPR's rendering pipeline flows through several distinct phases:
+-----------------------------------------------------------+
| 1. Clipping: Clip polygon vertices against ClipRect |
+-----------------------------------------------------------+
|
v
+-----------------------------------------------------------+
| 2. Y-Range Determination: Find YMin and YMax of vertices |
+-----------------------------------------------------------+
|
v
+-----------------------------------------------------------+
| 3. Prefix-Count Allocation: Calculate exact segment counts|
| per scanline using Y-interval delta accumulation |
+-----------------------------------------------------------+
|
v
+-----------------------------------------------------------+
| 4. Subdivision (DivideSegment): Split edges into |
| scanline-high fragments (local Y within [0, 1]) |
+-----------------------------------------------------------+
|
v
+-----------------------------------------------------------+
| 5. Span Extraction (ExtractSingleSpan): |
| - Apply crossing deltas at bottom boundary (Y = 1) |
| - Perform horizontal Cumulative Prefix Sum |
| - Apply analytical local trapezoidal integration |
+-----------------------------------------------------------+
|
v
+-----------------------------------------------------------+
| 6. Rendering (FillSpan / RenderSpan): |
| Convert analytical coverages to alpha and blend |
+-----------------------------------------------------------+Key Data Structures
TFloatPointA 2D point coordinate represented by single or double-precision floats (X, Y: TFloat).TLineSegmentAn array of twoTFloatPointcoordinates representing a segment bounded within a single scanline:pascalTLineSegment = array[0..1] of TFloatPoint;TScanLineRepresents a horizontal scanline bucket containing divided edge segments:pascalTScanLine = record Segments: PLineSegmentArray; Count: Integer; Y: Integer; end;TValueSpanDefines the horizontal span of pixels on a scanline that require rendering:pascalTValueSpan = record LowX, HighX: Integer; Values: PSingleArray; // Pointer to the coverage values end;
3. Step-by-Step Algorithmic Walkthrough
Step 1: Polygon Clipping
The input polygons (represented as TArrayOfArrayOfFloatPoint) are first clipped against the target bounding box (ClipRect) using ClipPolygon. This ensures that all vertices processed by the rasterizer lie within or on the boundaries of the viewport, preventing out-of-bounds memory accesses during coordinate-to-pixel mapping.
Step 2: Y-Range & Segment-Count Determination
Rather than using dynamic resizing arrays or linked lists (which cause heap fragmentation and cache misses), VPR uses a two-pass prefix count optimization to pre-allocate memory for all scanlines.
First Pass (Range Determination) VPR scans all vertices to find the minimum (
YMin) and maximum (YMax) integer scanlines.Second Pass (Segment-Count Delta Accumulation) For each edge going from
to : - If the edge goes downwards (
), it increments the count at and decrements it at : pascalInc(pScanLines[Y0].Count); Dec(pScanLines[Y1 + 1].Count); - If the edge goes upwards (
), it increments the count at and decrements it at : pascalInc(pScanLines[Y1].Count); Dec(pScanLines[Y0 + 1].Count);
- If the edge goes downwards (
Prefix Sum Allocation VPR computes the prefix sum of these counts across all scanlines. The prefix sum yields the exact number of segments that intersect each scanline. A single, contiguous block of memory is allocated for each scanline's segments:
pascalSegmentCount := 0; for i := 0 to High(ScanLines) do begin Inc(SegmentCount, ScanLines[i].Count); GetMem(ScanLines[i].Segments, SegmentCount * SizeOf(TLineSegment)); ScanLines[i].Count := 0; // Reset for actual population end;
Step 3: Vertical Subdivision (DivideSegment)
Every clipped polygon edge is subdivided into scanline-high fragments where the local
For a segment from
- Let
and . - If
, the segment lies entirely within a single scanline. It is added directly with its coordinates offset by . - If
, the segment crosses scanline boundaries. The inverse slope is calculated. - The segment is split at the horizontal grid lines
(for downward segments) or (for upward segments). - The intermediate
intersection coordinates are calculated linearly: - Each fractional and whole-scanline segment is added to its corresponding scanline bucket. To protect against floating-point rounding errors accumulating over long edges,
is clamped using Max(0, ...)to prevent negative coordinate index underflow.
Step 4: Span Extraction (ExtractSingleSpan)
For each scanline, VPR extracts the horizontal span of pixel coverage values. This is the heart of the VPR algorithm.
Scanline Y
+-------------------------------------------------------------------+
| Pixel X-1 | Pixel X | Pixel X+1 | Pixel X+2 |
| | | | |
| | Segment Start (Y=0) | |
| | \ | | |
| | \ | | |
| | \ | | |
| | Segment End (Y=1) | |
+---------------+---------+-----+---------------+-------------------+
|
Crossing at Bottom BoundaryA. Apply Crossing Deltas at Bottom Boundary ( )
If an edge fragment intersects the bottom of the scanline (
- For downward segments (odd-indexed points in the segment array, i.e., end-points):
- For upward segments (even-indexed points in the segment array, i.e., start-points):
B. Perform Horizontal Cumulative Prefix Sum
VPR runs a prefix integration (CumSum) from the minimum active LowX) to the maximum active HighX).
This propagates the boundary crossing transitions to all pixels to the right, establishing the base winding number coverage.
C. Local Trapezoidal Area Integration
Finally, VPR iterates over all segment fragments belonging to this scanline and accumulates their local analytical trapezoidal areas into the same SpanData buffer (see Section 4 for detailed math).
Step 5: Color Mapping & Raster Blending
Once the exact coverage values are extracted into SpanData, they are mapped to alpha values based on the polygon's fill rule:
- Even-Odd Fill Rule (
pfEvenOdd): The coverage valueis mapped using: - Non-Zero / Winding Fill Rule (
pfWinding/pfNonZero): The coverage valueis mapped using:
The resulting alpha value is combined with the paint color's alpha channel and blended onto the destination scanline buffer using BlendLine or MergeLine depending on the CombineMode.
4. Mathematical Principles of Local Edge Integration
The local analytical area integration within a pixel column is performed by the IntegrateSegment procedure. Let's analyze the exact mathematics behind it.
For a segment from
x1 x2
+---------+
y1 | * |
| * |
| * |
y2 | * |
+---------+Case A: Vertical Segment ( )
If the segment is perfectly vertical within a pixel column, its horizontal width is zero. Thus, the segment itself covers zero area inside the vertical column strip.
Case B: Non-Vertical Segment ( )
The slope parameters are:
The line equation within the scanline is:
1. Left-to-Right Segments ( )
Let
and . First Pixel (
): The horizontal span of the segment within the first pixel column is from to . The width is . At the right boundary of the pixel ( ), the vertical height is: Using the trapezoidal rule, the area under the segment from
to is: The code computes this as:
pascalValues[X1] := Values[X1] + 0.5 * (P1.Y + Y) * fracX1;Intermediate Pixels (
): For intermediate pixels, the horizontal span covers the entire pixel column width ( ). At the left boundary of pixel column , the height is . At the right boundary, the height is . The trapezoidal area under the segment within this pixel is: The code accumulates this efficiently and increments the running
: pascalValues[i] := Values[i] + (Y + DyDx * 0.5); Y := Y + DyDx;Last Pixel (
): The horizontal span of the segment within the last pixel column is from to . The width is . The starting height is the running , and the ending height is . The trapezoidal area under the segment within this pixel is: The code computes this as:
pascalValues[X2] := Values[X2] + 0.5 * (Y + P2.Y) * fracX2;
2. Right-to-Left Segments ( )
For right-to-left segments, the orientation is reversed. VPR computes the identical trapezoidal areas but subtracts them from the SpanData buffer. This elegant signed area formulation naturally implements the winding number mathematics without needing separate code paths or conditional branching for area orientation.
5. Algorithmic Pseudocode
The following pseudocode details the core logic of the VPR rasterizer.
Extracting and Rasterizing Scanline Spans
def ExtractSingleSpan(scanline, span_data):
# Initialize span bounds
low_x = infinity
high_x = -infinity
# Step A: Apply bottom-boundary (Y = 1) crossing deltas
# scanline.segments consists of segment endpoints: [P0, P1, P2, P3, ...]
points = scanline.segments
n = scanline.count * 2
for i in range(n):
P = points[i]
X = floor(P.X)
# Track active horizontal boundaries of the scanline
if X < low_x:
low_x = X
if P.Y == 1:
fracX = P.X - X
if i % 2 == 1: # Right edge (downward segment endpoint)
span_data[X] += (1.0 - fracX)
X += 1
span_data[X] += fracX
else: # Left edge (upward segment startpoint)
span_data[X] -= (1.0 - fracX)
X += 1
span_data[X] -= fracX
if X > high_x:
high_x = X
# Step B: Perform horizontal Cumulative Prefix Sum
# This propagates 1D boundary crossing deltas across the scanline
cumulative_sum = 0.0
for x in range(low_x, high_x + 1):
cumulative_sum += span_data[x]
span_data[x] = cumulative_sum
# Step C: Integrate local segment areas analytically
for i in range(scanline.count):
segment = scanline.segments[i]
IntegrateSegment(segment[0], segment[1], span_data)
return low_x, high_x
def IntegrateSegment(P1, P2, span_data):
X1 = floor(P1.X)
X2 = floor(P2.X)
# Perfectly vertical segment inside a single pixel strip
if X1 == X2:
span_data[X1] += 0.5 * (P2.X - P1.X) * (P1.Y + P2.Y)
return
Dx = P2.X - P1.X
Dy = P2.Y - P1.Y
DyDx = Dy / Dx
# Left-to-Right orientation
if X1 < X2:
fracX1 = 1.0 - (P1.X - X1)
fracX2 = P2.X - X2
Y = P1.Y + fracX1 * DyDx
# Integrate first fractional pixel column
span_data[X1] += 0.5 * (P1.Y + Y) * fracX1
# Integrate intermediate whole pixel columns
for x in range(X1 + 1, X2):
span_data[x] += Y + DyDx * 0.5
Y += DyDx
# Integrate last fractional pixel column
span_data[X2] += 0.5 * (Y + P2.Y) * fracX2
# Right-to-Left orientation
else:
fracX1 = P1.X - X1
fracX2 = 1.0 - (P2.X - X2)
Y = P1.Y - fracX1 * DyDx
# Subtract integrated first fractional pixel column
span_data[X1] -= 0.5 * (P1.Y + Y) * fracX1
# Subtract integrated intermediate whole pixel columns
for x in range(X1 - 1, X2, -1):
span_data[x] -= Y - DyDx * 0.5
Y -= DyDx
# Subtract integrated last fractional pixel column
span_data[X2] -= 0.5 * (Y + P2.Y) * fracX26. Key Performance Optimizations
VPR contains several low-level optimizations that make it one of the fastest analytical vector rasterizers available:
- No Horizontal Sorting: By using 1D delta buffers and a single horizontal
CumSumpass, VPR completely avoids sorting crossing coordinates horizontally. This turns anoperation into an population pass plus a fast prefix integration. - Segment Count Prefix Pre-allocation: The use of the Y-interval delta accumulation (
pScanLines[Y0].Count++,pScanLines[Y1+1].Count--) allows VPR to compute the exact size of the segment array needed for each scanline before allocating memory. This enables allocating a single, contiguous block of memory per scanline, maximizing CPU L1/L2 cache locality. - Optimized Floor/Ceil Routines (
PolyFloor/PolyCeil): Delphi’s standardTruncfunction is notoriously slow on x86 because it modifies the hardware FPU control word. VPR bypasses this by implementing optimized assembly or fast SSE routines (FastFloorSingle/FastFloorDouble) to perform floor and ceil operations without altering the FPU state. - Active Span Clipping: The horizontal rendering bounds are clamped to
ClipX1andClipX2immediately after span extraction. This ensures that regions outside the clipping rectangle are skipped during cumulative summing and local integration, minimizing wasted CPU cycles. - Fast Reset of Coverage Buffers: After a span is rendered, the
SpanDatabuffer is reset back to zero only within the active horizontal span bounds usingFillLongWord. This avoids the overhead of clearing the entire viewport width, keeping the algorithm cache-friendly even on extremely wide viewports.