459 lines
17 KiB
Markdown
459 lines
17 KiB
Markdown
# DefaultRasterizer
|
|
|
|
`DefaultRasterizer` is the CPU polygon scanner used by the retained fill path in ImageSharp.Drawing. Its job is narrow but central: take already-prepared geometry, convert that geometry into fixed-point edge contributions, and emit coverage rows that the CPU backend can turn into pixels.
|
|
|
|
This rasterizer is based on ideas and implementation techniques from the Blaze project:
|
|
|
|
- https://github.com/aurimasg/blaze
|
|
|
|
This document explains the rasterizer as a newcomer needs to understand it:
|
|
|
|
- where the rasterizer fits relative to `DrawingCanvas` and `DefaultDrawingBackend`
|
|
- what problem the rasterizer is solving inside the CPU backend
|
|
- why the rasterizer is split into retained geometry building and band execution
|
|
- what retained geometry, bands, and coverage mean in this architecture
|
|
- how scan conversion stays separate from brush shading and frame ownership
|
|
|
|
## Where The Rasterizer Fits
|
|
|
|
`DefaultRasterizer` sits below `DrawingCanvas`, the typed canvas implementation, and `DefaultDrawingBackend`.
|
|
|
|
The canvas records commands, the batcher prepares them into `DrawingCommandBatch` ranges, and `DefaultDrawingBackend` chooses the row-oriented execution plan for each retained CPU scene. `DefaultRasterizer` then handles the narrower geometry-to-coverage problem inside that CPU execution path.
|
|
|
|
That means the rasterizer does not select the backend, own the destination frame, or interpret the public drawing API directly. It receives already-prepared geometry through the CPU backend pipeline, and the backend later routes its coverage into whichever frame exposes the CPU region for the flush.
|
|
|
|
## The Main Problem
|
|
|
|
The CPU backend does not want to rediscover shape geometry every time it touches a destination row.
|
|
|
|
If row execution had to start from raw prepared paths every time, the backend would repeatedly need to:
|
|
|
|
- walk contours
|
|
- split segments against row-band boundaries
|
|
- compute left-of-band winding influence
|
|
- rebuild scan-conversion state for the same shape over and over
|
|
|
|
That would push expensive geometry work into the hottest part of CPU rendering.
|
|
|
|
So the rasterizer solves a different problem:
|
|
|
|
it builds retained rasterizable geometry once, then executes compact band-local scanning work many times, cheaply.
|
|
|
|
That two-phase design is the core idea behind `DefaultRasterizer`.
|
|
|
|
## The Core Idea
|
|
|
|
The rasterizer is a retained fixed-point polygon scanner.
|
|
|
|
Its central idea is:
|
|
|
|
> build band-local retained line data once, then execute fixed-point scan conversion from that retained data
|
|
|
|
This is why the rasterizer has two very different modes of work:
|
|
|
|
1. retained geometry building
|
|
2. band execution
|
|
|
|
The first phase is a preparation phase. The second is the hot execution phase.
|
|
|
|
If that distinction is clear, the code becomes much easier to follow.
|
|
|
|
## The Most Important Terms
|
|
|
|
### Rasterizer
|
|
|
|
`DefaultRasterizer` is the geometry-to-coverage engine.
|
|
|
|
It is responsible for:
|
|
|
|
- converting prepared geometry into retained scan-conversion data
|
|
- rasterizing retained band data with fixed-point arithmetic
|
|
- emitting coverage rows
|
|
|
|
It is not responsible for:
|
|
|
|
- brush color generation
|
|
- destination frame ownership
|
|
- layer composition
|
|
- deciding which scene items should execute
|
|
|
|
Those problems belong to the CPU backend and `FlushScene`.
|
|
|
|
### Retained Geometry
|
|
|
|
Retained geometry is the rasterizer's prepared execution payload.
|
|
|
|
In this codebase, retained geometry means:
|
|
|
|
"the fixed-point, band-local line data and start-cover seeds needed to rasterize one prepared shape later without revisiting its original contour data"
|
|
|
|
That retained form is stored in `RasterizableGeometry`.
|
|
|
|
### Band
|
|
|
|
A band is one small vertical slice of a shape's retained geometry.
|
|
|
|
The rasterizer does not keep one giant scene-wide edge table. It stores data in row bands so execution can stay local and bounded.
|
|
|
|
### Rasterizable Geometry
|
|
|
|
`RasterizableGeometry` is the retained representation of one prepared shape.
|
|
|
|
It stores:
|
|
|
|
- clipped local bounds
|
|
- band-local metadata
|
|
- retained line arrays
|
|
- optional start-cover seeds for bands that need carry-in winding
|
|
|
|
This is the retained object that the CPU backend keeps in `FlushScene`.
|
|
|
|
### Rasterizable Band
|
|
|
|
A `RasterizableBand` is the execution-time view over one retained band of one retained shape.
|
|
|
|
It is the immediate input to `ExecuteRasterizableBand(...)`.
|
|
|
|
### Context
|
|
|
|
`DefaultRasterizer.Context` is the mutable fixed-point scanning state used during band execution.
|
|
|
|
It is a `ref struct` because it is tied directly to worker-owned scratch spans and should not escape the execution scope.
|
|
|
|
### Coverage
|
|
|
|
Coverage is the rasterizer's output.
|
|
|
|
The rasterizer does not decide final pixel colors. It decides how much geometric coverage each pixel receives. The backend later passes that coverage to a `BrushRenderer<TPixel>`, which decides how the destination pixels should be shaded.
|
|
|
|
## Pipeline Placement
|
|
|
|
The rasterizer sits in the middle of the CPU backend pipeline.
|
|
|
|
Upstream:
|
|
|
|
- `CompositionCommand` preparation produces prepared geometry
|
|
- the typed canvas implementation and `DrawingCanvasBatcher<TPixel>` have already selected and called the CPU backend
|
|
- `FlushScene` decides which items are visible and when they execute
|
|
|
|
Downstream:
|
|
|
|
- the rasterizer emits row coverage
|
|
- `DefaultDrawingBackend` routes that coverage into `BrushRenderer<TPixel>.Apply(...)`
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[Prepared geometry] --> B[DefaultRasterizer.CreateRasterizableGeometry]
|
|
B --> C[RasterizableGeometry]
|
|
C --> D[Build RasterizableBand view]
|
|
D --> E[ExecuteRasterizableBand]
|
|
E --> F[Coverage rows]
|
|
F --> G[Brush renderer]
|
|
```
|
|
|
|
That placement is important. The rasterizer is neither the public drawing model nor the final shading model. It is the geometry-to-coverage step between them.
|
|
|
|
## Why The Rasterizer Has Two Phases
|
|
|
|
The rasterizer separates:
|
|
|
|
1. building retained geometry
|
|
2. executing retained geometry
|
|
|
|
### Phase 1: retained geometry building
|
|
|
|
`CreateRasterizableGeometry(...)` converts prepared geometry into a retained representation that is cheap to execute later.
|
|
|
|
This phase:
|
|
|
|
- walks prepared contours
|
|
- converts coordinates into fixed-point
|
|
- clips or splits segments as needed for band boundaries
|
|
- records visible line pieces into retained line storage
|
|
- records left-of-band winding influence into start-cover tables
|
|
|
|
The output is `RasterizableGeometry`.
|
|
|
|
### Phase 2: band execution
|
|
|
|
`ExecuteRasterizableBand(...)` is the hot execution entry point.
|
|
|
|
It does not revisit the original contour data. It receives a `RasterizableBand` view over retained data and performs the minimum work needed to emit coverage rows for that band.
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Exec as ExecuteRasterizableBand
|
|
participant Ctx as Context
|
|
participant Emit as Coverage Row Handler
|
|
|
|
Exec->>Ctx: Reconfigure(...)
|
|
Exec->>Ctx: SeedStartCovers(...)
|
|
Exec->>Ctx: Rasterize retained lines
|
|
Exec->>Ctx: EmitCoverageRows(...)
|
|
Ctx-->>Emit: coverage rows
|
|
Exec->>Ctx: ResetTouchedRows()
|
|
```
|
|
|
|
That separation is one of the key reasons the retained fill path performs well. Expensive geometry work happens once; execution consumes compact band-local data.
|
|
|
|
## Fixed-Point Precision
|
|
|
|
The rasterizer works in 24.8 fixed-point coordinates.
|
|
|
|
That means:
|
|
|
|
- `1` pixel = `256` fixed-point units
|
|
- `FixedShift = 8`
|
|
- `FixedOne = 256`
|
|
|
|
This gives the scanner subpixel precision while keeping the hot execution path integer-based. Geometry may begin as floating-point path data, but once a retained line reaches the scan-conversion core it is treated as fixed-point state.
|
|
|
|
Coverage is converted back into normalized `float` values only at the emission boundary.
|
|
|
|
## Why Bands Exist
|
|
|
|
The rasterizer does not retain one monolithic edge table. It retains geometry in vertical row bands.
|
|
|
|
That matters because it keeps execution local and bounded.
|
|
|
|
When a segment crosses multiple bands, the linearizer splits it so each band receives only the portion it must scan. If a segment influences winding inside the visible band from the left side, that influence is folded into a start-cover seed rather than keeping an invisible off-screen line around forever.
|
|
|
|
This gives the backend several important properties:
|
|
|
|
- execution only touches the band it is currently composing
|
|
- left-of-band winding can be precomputed
|
|
- scratch requirements stay bounded
|
|
- row-oriented execution consumes compact band-local payloads
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[Contour segment] --> B{Touches one band?}
|
|
B -- Yes --> C[Store visible line in that band]
|
|
B -- No --> D[Split across band boundaries]
|
|
D --> E[Store band-local visible pieces]
|
|
D --> F[Accumulate start-cover seeds where needed]
|
|
```
|
|
|
|
## Retained Geometry: What Gets Stored
|
|
|
|
`RasterizableGeometry` stores the retained data needed to rasterize a prepared shape later.
|
|
|
|
That includes:
|
|
|
|
- the local bounds of the prepared shape
|
|
- band count and band-local metadata
|
|
- retained line arrays for each band
|
|
- optional start-cover arrays for bands that need carry-in winding
|
|
|
|
The retained line arrays use specialized storage formats such as:
|
|
|
|
- `LineArrayX16Y16`
|
|
- `LineArrayX32Y16`
|
|
|
|
These are storage-oriented types. They exist to retain compact fixed-point line segments so execution does not need to revisit contour data.
|
|
|
|
## The Linearizer
|
|
|
|
The linearizer is the retained-geometry builder. It is generic over line-array storage, but the conceptual work is the same across variants.
|
|
|
|
Its responsibilities are:
|
|
|
|
- traverse prepared contours
|
|
- apply the residual transform per-point as contours are read
|
|
- clip work to retained bounds
|
|
- convert coordinates into fixed-point
|
|
- decide whether a segment is contained or must be split
|
|
- store visible line pieces
|
|
- accumulate start covers for left-of-band influence
|
|
|
|
For a newcomer, the most important thing to understand is that the linearizer is not the hot coverage emitter. It is the preparation step that turns arbitrary contour geometry into a stable retained scanning payload.
|
|
|
|
### Residual transform application
|
|
|
|
The prepared `LinearGeometry` passed to `CreateRasterizableGeometry(...)` carries scale-baked points — the effective X/Y scale of the drawing matrix has already been absorbed into the flattened contour, so curve subdivision happens at device-scale precision. The remaining rotation, shear, translation, and perspective is handed to the rasterizer as a separate `Matrix4x4 residual`, which the linearizer applies per-point where the contour is read: at segment emission time in `ProcessContained` / `ProcessUncontained` for fills, and at bounds / closure / contour-segment construction sites in the stroke linearizer.
|
|
|
|
This split keeps the scale-baked geometry cacheable across frames (text and panning workloads reuse the same bake at a fixed zoom) while letting per-frame rotation or translation ride through the rasterizer without re-subdividing curves.
|
|
|
|
### Contained lines
|
|
|
|
A contained line is one whose fixed-point endpoints already fit the assumptions of the current retained band representation. Those lines can be pushed directly into retained storage after the required fixed-point and band-boundary handling.
|
|
|
|
### Split lines
|
|
|
|
When a line crosses band boundaries, the linearizer splits it so each band receives only the contribution it needs to scan.
|
|
|
|
### Start-cover seeding
|
|
|
|
When a line contributes winding inside the visible band but lies partially to the left of the visible X range, the retained geometry stores that influence in a start-cover array instead of retaining an off-screen line.
|
|
|
|
This is one of the most important ideas in the retained design:
|
|
|
|
- visible geometry becomes retained lines
|
|
- invisible left-of-band winding becomes retained start-cover seeds
|
|
|
|
## The Execution Context
|
|
|
|
`DefaultRasterizer.Context` is the mutable fixed-point scanning state used during band execution.
|
|
|
|
It owns per-band mutable state such as:
|
|
|
|
- `bitVectors`
|
|
- `coverArea`
|
|
- `startCover`
|
|
- `rowMinTouchedColumn`
|
|
- `rowMaxTouchedColumn`
|
|
- `rowHasBits`
|
|
- `rowTouched`
|
|
- `touchedRows`
|
|
|
|
This state is reused across bands by reconfiguration, not by reallocation.
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
A[WorkerScratch] --> B[Context]
|
|
B --> C[Rasterize retained lines]
|
|
C --> D[Mutate coverArea and bit vectors]
|
|
D --> E[Emit coverage rows]
|
|
E --> F[Reset touched rows]
|
|
```
|
|
|
|
The `Context` bridges retained geometry and emitted coverage.
|
|
|
|
## How Coverage Accumulation Works
|
|
|
|
The rasterizer uses the classic area-and-cover formulation.
|
|
|
|
When a fixed-point line is rasterized, it is broken into cell contributions. Those contributions eventually reach `AddCell(...)`, which updates:
|
|
|
|
- delta cover
|
|
- delta area
|
|
|
|
Rows also track sparse touched-column information through bit vectors, so the emitter can avoid scanning the full width of empty rows.
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[Rasterize fixed-point line] --> B[Decompose into touched cells]
|
|
B --> C["AddCell(row, column, deltaCover, deltaArea)"]
|
|
C --> D[Update coverArea]
|
|
C --> E[Mark bitVectors]
|
|
C --> F[Track touched rows and bounds]
|
|
C --> G{column < 0?}
|
|
G -- Yes --> H[Fold into startCover]
|
|
G -- No --> I[Keep visible cell contribution]
|
|
```
|
|
|
|
This is why the rasterizer can honor fill rules later. It accumulates signed contributions first and applies the fill rule during coverage emission.
|
|
|
|
## Coverage Emission
|
|
|
|
`EmitCoverageRows(...)` converts the accumulated fixed-point state into row spans.
|
|
|
|
For each touched row, the emitter:
|
|
|
|
1. starts from the seeded `startCover`
|
|
2. walks the row's touched columns using the bit vectors
|
|
3. updates the running cover from `deltaCover`
|
|
4. combines running cover and `deltaArea` into signed area
|
|
5. converts signed area into normalized coverage using the selected fill rule
|
|
6. coalesces equal-coverage spans
|
|
7. writes only non-zero spans into the reusable scanline buffer
|
|
8. invokes the row callback
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
A[Touched row] --> B[Walk set bits]
|
|
B --> C[Reconstruct cover and area]
|
|
C --> D[Apply fill rule]
|
|
D --> E[Coalesce equal coverage]
|
|
E --> F[Write compact scanline spans]
|
|
F --> G[Invoke row handler]
|
|
```
|
|
|
|
The rasterizer therefore emits only rows that actually received contributions and only the non-zero spans within those rows.
|
|
|
|
## Fill Rules
|
|
|
|
The rasterizer supports both `NonZero` and `EvenOdd`.
|
|
|
|
### NonZero
|
|
|
|
The accumulated signed area is treated as winding magnitude. Coverage is the clamped absolute value of that area.
|
|
|
|
### EvenOdd
|
|
|
|
The accumulated area is wrapped into the even-odd domain before coverage is produced. This gives parity-based behavior without changing the earlier scan-conversion logic.
|
|
|
|
The fill rule is therefore an emission-time decision, not a geometry-preprocessing decision.
|
|
|
|
## Antialiased And Aliased Modes
|
|
|
|
The rasterizer can emit either continuous or thresholded coverage.
|
|
|
|
- `Antialiased` mode keeps the continuous coverage produced by the area-and-cover math
|
|
- `Aliased` mode thresholds that continuous coverage using `AntialiasThreshold`
|
|
|
|
The scan-conversion core stays the same in both modes. Only the final conversion from area to emitted coverage changes.
|
|
|
|
## Why Self-Intersections Work
|
|
|
|
The rasterizer can handle self-intersections because it does not require geometric boolean normalization before rasterization. It accumulates signed contributions and then applies the selected fill rule during emission.
|
|
|
|
That means overlapping or self-crossing contours are resolved by:
|
|
|
|
- area-and-cover integration
|
|
- winding or parity mapping
|
|
|
|
instead of by an earlier polygon-boolean pass.
|
|
|
|
## How The Rasterizer Stays Separate From The Backend
|
|
|
|
The rasterizer and the backend solve different problems.
|
|
|
|
The rasterizer decides:
|
|
|
|
- how geometry contributes coverage
|
|
- which rows and columns within a band are touched
|
|
- how much coverage each emitted span has
|
|
|
|
The backend decides:
|
|
|
|
- which scene items execute
|
|
- which retained band is being scanned
|
|
- which destination slice receives the coverage
|
|
- which brush renderer consumes the emitted spans
|
|
|
|
That separation is one of the main architectural advantages of the current CPU path.
|
|
|
|
## Reading Guide
|
|
|
|
If you are new to this part of the library, read the rasterizer in this order:
|
|
|
|
1. `DrawingCanvas.cs`
|
|
2. `DrawingCanvas{TPixel}.cs`
|
|
3. `DrawingCanvasBatcher{TPixel}.cs`
|
|
4. `DefaultDrawingBackend.cs`
|
|
5. `FlushScene.cs`
|
|
6. `CreateRasterizableGeometry(...)` in `DefaultRasterizer.cs`
|
|
7. `Linearizer<TL>` and the concrete linearizers in `DefaultRasterizer.Linearizer.cs`
|
|
8. retained line types in `DefaultRasterizer.RetainedTypes.cs`
|
|
9. `ExecuteRasterizableBand(...)` in `DefaultRasterizer.cs`
|
|
10. `Context` in `DefaultRasterizer.cs`
|
|
|
|
That order mirrors the data lifecycle:
|
|
|
|
canvas intent -> prepared geometry -> retained storage -> band execution -> coverage emission
|
|
|
|
## The Mental Model To Keep
|
|
|
|
The easiest way to reason about `DefaultRasterizer` is this:
|
|
|
|
it is a retained fixed-point polygon scanner that transforms prepared geometry into compact band-local line payloads, then turns those payloads into row coverage spans.
|
|
|
|
If that model stays clear, the rest of the code becomes easier to read:
|
|
|
|
- the canvas and backend docs explain how execution reaches the CPU path
|
|
- the linearizer explains where retained line data comes from
|
|
- `RasterizableGeometry` explains what is stored
|
|
- the `Context` explains how retained data becomes coverage
|
|
- the backend explains how coverage becomes pixels
|