|
| 1 | +# Polygon Scanning In `DefaultRasterizer` |
| 2 | + |
| 3 | +`DefaultRasterizer` is the CPU polygon scanner used by the retained fill backend in ImageSharp.Drawing. Its job is not to understand brushes, layers, or destination frames. Its job is narrower and more important: take already-prepared geometry, convert it into fixed-point edge contributions, and emit row coverage spans that the backend can compose into pixels. |
| 4 | + |
| 5 | +This article explains how that scanner works today, why it is structured around retained row bands, and how the fixed-point coverage engine turns line segments into stable fill coverage. |
| 6 | + |
| 7 | +The current implementation lives in: |
| 8 | + |
| 9 | +- `src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs` |
| 10 | +- `src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.cs` |
| 11 | +- `src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.Outputs.cs` |
| 12 | +- `src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RetainedTypes.cs` |
| 13 | +- `src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RasterizableGeometry.cs` |
| 14 | + |
| 15 | +## Why This Scanner Exists |
| 16 | + |
| 17 | +The retained fill backend does not rasterize directly from raw `IPath` commands during row execution. Doing that would force the executor to repeatedly rediscover the same geometry facts for every touched row band. Instead, the backend asks the rasterizer to do two separate kinds of work: |
| 18 | + |
| 19 | +1. Build retained rasterizable geometry once. |
| 20 | +2. Execute one prepared rasterizable band many times, cheaply. |
| 21 | + |
| 22 | +That split is the core design choice. It keeps expensive geometry work out of the hottest row-composition loop and lets the execution phase focus on scan conversion and coverage emission. |
| 23 | + |
| 24 | +```mermaid |
| 25 | +flowchart LR |
| 26 | + A[Prepared geometry] --> B[Linearize into retained line blocks] |
| 27 | + B --> C[Store start-cover seeds and per-band line lists] |
| 28 | + C --> D[Build RasterizableBand view] |
| 29 | + D --> E[Execute fixed-point scan conversion] |
| 30 | + E --> F[Emit coverage rows] |
| 31 | +``` |
| 32 | + |
| 33 | +## The Scanner's Input And Output |
| 34 | + |
| 35 | +The scanner sits in the middle of the backend pipeline. |
| 36 | + |
| 37 | +- Upstream, `FlushScene` and the geometry preparer decide which commands are visible and which geometry they contribute. |
| 38 | +- Downstream, `DefaultDrawingBackend` uses emitted coverage rows to call `BrushRenderer<TPixel>.Apply(...)`. |
| 39 | + |
| 40 | +The scanner therefore consumes geometry-centric data and emits coverage-centric data. |
| 41 | + |
| 42 | +```mermaid |
| 43 | +flowchart TD |
| 44 | + A[Composition command] --> B[Prepared geometry] |
| 45 | + B --> C[DefaultRasterizer.CreateRasterizableGeometry] |
| 46 | + C --> D[RasterizableGeometry] |
| 47 | + D --> E[TryBuildRasterizableBand / ExecuteRasterizableBand] |
| 48 | + E --> F[Coverage rows] |
| 49 | + F --> G[Brush renderer] |
| 50 | +``` |
| 51 | + |
| 52 | +The important consequence is that the scanner has no knowledge of color sources. It never decides what a pixel should look like. It only decides how much geometric coverage each pixel receives. |
| 53 | + |
| 54 | +## Coordinate System And Fixed-Point Precision |
| 55 | + |
| 56 | +The scanner works in 24.8 fixed-point coordinates. Eight fractional bits give subpixel precision while keeping the inner loops integer-based. |
| 57 | + |
| 58 | +- `1` pixel = `256` fixed-point units |
| 59 | +- `FixedShift = 8` |
| 60 | +- `FixedOne = 256` |
| 61 | + |
| 62 | +This lets the scanner track fractional edge coverage without drifting into floating-point arithmetic in the hot path. Geometry may begin as floating-point path data, but once a retained line enters the scan-conversion core it is treated as integer fixed-point state. |
| 63 | + |
| 64 | +Coverage is later converted into normalized `float` values in the range `[0, 1]` only at the emission boundary. |
| 65 | + |
| 66 | +## The Two Major Scanner Phases |
| 67 | + |
| 68 | +The retained fill path uses two scanner phases with very different responsibilities. |
| 69 | + |
| 70 | +### Phase 1: Retained Geometry Building |
| 71 | + |
| 72 | +`CreateRasterizableGeometry(...)` converts prepared geometry into a retained representation that is cheap to execute later. This phase uses the linearizers in `DefaultRasterizer.Linearizer.cs`. |
| 73 | + |
| 74 | +The linearizer: |
| 75 | + |
| 76 | +- walks the prepared contours |
| 77 | +- converts them into fixed-point line segments |
| 78 | +- splits or clips them as required by row-band boundaries |
| 79 | +- records left-of-band winding influence into start-cover tables |
| 80 | +- stores visible line segments into retained line blocks |
| 81 | + |
| 82 | +The output is a `RasterizableGeometry` containing: |
| 83 | + |
| 84 | +- clipped tile bounds |
| 85 | +- per-band retained line arrays |
| 86 | +- optional per-band start-cover seeds |
| 87 | + |
| 88 | +### Phase 2: Band Execution |
| 89 | + |
| 90 | +`ExecuteRasterizableBand(...)` is the hot execution entry point. |
| 91 | + |
| 92 | +It does not rediscover geometry. Instead it receives a `RasterizableBand` view over already-retained data and performs the minimum work needed to emit coverage rows for that band. |
| 93 | + |
| 94 | +```mermaid |
| 95 | +sequenceDiagram |
| 96 | + participant Exec as ExecuteRasterizableBand |
| 97 | + participant Ctx as Context |
| 98 | + participant Emit as Coverage Row Handler |
| 99 | +
|
| 100 | + Exec->>Ctx: Reconfigure(...) |
| 101 | + Exec->>Ctx: SeedStartCovers(...) |
| 102 | + Exec->>Ctx: RasterizePreparedLines(...) |
| 103 | + Exec->>Ctx: EmitCoverageRows(...) |
| 104 | + Ctx-->>Emit: row coverage spans |
| 105 | + Exec->>Ctx: ResetTouchedRows() |
| 106 | +``` |
| 107 | + |
| 108 | +That separation is why the scanner performs well on larger retained-fill workloads. The expensive topology work happens once; execution only consumes compact band-local data. |
| 109 | + |
| 110 | +## Retained Geometry: What Gets Stored |
| 111 | + |
| 112 | +The retained geometry model is built around per-band line storage. |
| 113 | + |
| 114 | +`RasterizableGeometry` stores: |
| 115 | + |
| 116 | +- the local bounds of the prepared shape |
| 117 | +- band count and band-local metadata |
| 118 | +- retained line arrays for each band |
| 119 | +- optional start-cover arrays for bands that need left-of-band carry-in |
| 120 | + |
| 121 | +The line arrays are specialized for two storage formats: |
| 122 | + |
| 123 | +- `LineArrayX16Y16` |
| 124 | +- `LineArrayX32Y16` |
| 125 | + |
| 126 | +Those types are storage-oriented. They exist to retain compact fixed-point line segments so the runtime scan-conversion phase does not need to revisit the original contour data. |
| 127 | + |
| 128 | +The line blocks currently use `NativeMemory` intentionally. This is a targeted exception in the retained-fill path, not a general policy for the rest of the backend. The retained line blocks are tiny and numerous, and larger retained-fill workloads benchmarked better when those blocks avoided the normal allocator path. |
| 129 | + |
| 130 | +## Why Row Bands Exist |
| 131 | + |
| 132 | +The scanner does not retain a single giant scene-wide edge table. It retains geometry in vertical row bands. |
| 133 | + |
| 134 | +That choice matters because it bounds per-band work and makes downstream execution naturally row-oriented. |
| 135 | + |
| 136 | +Each band represents a small vertical slice of the shape. When a segment crosses multiple bands, the linearizer splits it so each retained band contains only the line contributions relevant to that band. |
| 137 | + |
| 138 | +This gives the backend several advantages: |
| 139 | + |
| 140 | +- row execution only touches the band it is currently composing |
| 141 | +- left-of-band winding can be precomputed into start-cover seeds |
| 142 | +- scratch storage can be reused because band dimensions are bounded |
| 143 | +- parallel row execution can consume compact band-local payloads |
| 144 | + |
| 145 | +```mermaid |
| 146 | +flowchart TD |
| 147 | + A[Contour segment] --> B{Touches one band?} |
| 148 | + B -- Yes --> C[Store visible line in that band] |
| 149 | + B -- No --> D[Split across band boundaries] |
| 150 | + D --> E[Store band-local visible pieces] |
| 151 | + D --> F[Accumulate start-cover seeds where needed] |
| 152 | +``` |
| 153 | + |
| 154 | +## The Linearizer |
| 155 | + |
| 156 | +The linearizer is the retained-geometry builder. It is generic over the line-array storage type but the conceptual work is the same for both concrete variants. |
| 157 | + |
| 158 | +Its responsibilities are: |
| 159 | + |
| 160 | +- traverse prepared geometry contours |
| 161 | +- clip work to the geometry's retained bounds |
| 162 | +- convert coordinates into fixed-point |
| 163 | +- decide whether a segment is fully contained or needs splitting |
| 164 | +- store visible line pieces |
| 165 | +- accumulate start covers for the parts of a segment that lie to the left of the visible band |
| 166 | + |
| 167 | +For a new reader, 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. |
| 168 | + |
| 169 | +### Contained Lines |
| 170 | + |
| 171 | +A contained line is one whose fixed-point endpoints already fit the assumptions of the current retained geometry/band representation. Those lines can be pushed directly into retained line storage after the necessary fixed-point and band-boundary handling. |
| 172 | + |
| 173 | +### Split Lines |
| 174 | + |
| 175 | +When a line crosses band boundaries, the linearizer uses split logic so that each band receives only the portion it must scan. This is how the scanner avoids carrying giant scene-wide edge sets into execution. |
| 176 | + |
| 177 | +### Start-Cover Seeding |
| 178 | + |
| 179 | +When a line contributes winding to pixels 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 keeping an off-screen line around forever. |
| 180 | + |
| 181 | +That is one of the most important ideas in the retained design: |
| 182 | + |
| 183 | +- visible geometry becomes retained lines |
| 184 | +- invisible left-of-band winding becomes retained start-cover seeds |
| 185 | + |
| 186 | +## The `Context` Ref Struct |
| 187 | + |
| 188 | +`DefaultRasterizer.Context` is the mutable fixed-point scanner state used during band execution. It is a `ref struct` so its spans stay tied to worker-owned scratch and cannot accidentally escape the execution scope. |
| 189 | + |
| 190 | +It owns the per-band mutable raster state: |
| 191 | + |
| 192 | +- `bitVectors` |
| 193 | +- `coverArea` |
| 194 | +- `startCover` |
| 195 | +- `rowMinTouchedColumn` |
| 196 | +- `rowMaxTouchedColumn` |
| 197 | +- `rowHasBits` |
| 198 | +- `rowTouched` |
| 199 | +- `touchedRows` |
| 200 | + |
| 201 | +This state is reused across bands by reconfiguration, not by reallocation. |
| 202 | + |
| 203 | +```mermaid |
| 204 | +flowchart LR |
| 205 | + A[WorkerScratch] --> B[Context] |
| 206 | + B --> C[Rasterize prepared lines] |
| 207 | + C --> D[Mutate coverArea and bitVectors] |
| 208 | + D --> E[Emit coverage rows] |
| 209 | + E --> F[Reset touched rows] |
| 210 | +``` |
| 211 | + |
| 212 | +The `Context` has four public execution-stage responsibilities: |
| 213 | + |
| 214 | +- `Reconfigure(...)` |
| 215 | +- `SeedStartCovers(...)` |
| 216 | +- `RasterizeLineSegment(...)` and line iteration helpers |
| 217 | +- `EmitCoverageRows(...)` |
| 218 | + |
| 219 | +Together they bridge retained geometry and per-row coverage emission. |
| 220 | + |
| 221 | +## How The Scanner Accumulates Coverage |
| 222 | + |
| 223 | +The scanner uses the classic area/cover formulation. |
| 224 | + |
| 225 | +When a fixed-point line is rasterized, it is broken into cell contributions. Those contributions end up in `AddCell(...)`, which updates two pieces of per-cell state: |
| 226 | + |
| 227 | +- delta cover |
| 228 | +- delta area |
| 229 | + |
| 230 | +Rows also track sparse touched-column information through bit vectors so the emitter can avoid scanning the full width of empty rows. |
| 231 | + |
| 232 | +```mermaid |
| 233 | +flowchart TD |
| 234 | + A[Rasterize fixed-point line] --> B[Decompose into touched cells] |
| 235 | + B --> C["AddCell(row, column, deltaCover, deltaArea)"] |
| 236 | + C --> D[Update coverArea] |
| 237 | + C --> E[Mark bitVectors] |
| 238 | + C --> F[Track touched rows and bounds] |
| 239 | + C --> G{column < 0?} |
| 240 | + G -- Yes --> H[Fold into startCover] |
| 241 | + G -- No --> I[Keep visible cell contribution] |
| 242 | +``` |
| 243 | + |
| 244 | +This is the core reason the scanner can honor fill rules after the fact. It does not need pre-normalized polygon topology. It integrates winding/area contributions first and applies the fill rule during emission. |
| 245 | + |
| 246 | +## Coverage Emission |
| 247 | + |
| 248 | +`EmitCoverageRows(...)` converts the accumulated integer coverage state into row spans. |
| 249 | + |
| 250 | +For each touched row, the emitter: |
| 251 | + |
| 252 | +1. starts from the seeded `startCover` |
| 253 | +2. walks the row's touched columns using the bit vectors |
| 254 | +3. updates the running cover from `deltaCover` |
| 255 | +4. combines running cover and `deltaArea` into signed area |
| 256 | +5. converts signed area into normalized coverage using the fill rule |
| 257 | +6. coalesces equal-coverage runs |
| 258 | +7. materializes only non-zero spans into the reusable `scanline` |
| 259 | +8. invokes the row callback |
| 260 | + |
| 261 | +```mermaid |
| 262 | +flowchart LR |
| 263 | + A[Touched row] --> B[Walk set bits] |
| 264 | + B --> C[Reconstruct cover and area] |
| 265 | + C --> D[Apply fill rule] |
| 266 | + D --> E[Coalesce equal coverage] |
| 267 | + E --> F[Write compact scanline spans] |
| 268 | + F --> G[Invoke row handler] |
| 269 | +``` |
| 270 | + |
| 271 | +The scanner therefore emits only rows that actually received contributions and only the non-zero runs within those rows. |
| 272 | + |
| 273 | +## Fill Rules |
| 274 | + |
| 275 | +The scanner supports both `NonZero` and `EvenOdd`. |
| 276 | + |
| 277 | +### `NonZero` |
| 278 | + |
| 279 | +The accumulated signed area is treated as winding magnitude. Coverage is the clamped absolute value of that area. |
| 280 | + |
| 281 | +### `EvenOdd` |
| 282 | + |
| 283 | +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. |
| 284 | + |
| 285 | +The fill rule is therefore an emission-time decision, not a geometry-preprocessing decision. |
| 286 | + |
| 287 | +## Antialiased And Aliased Modes |
| 288 | + |
| 289 | +The scanner can emit either continuous or thresholded coverage. |
| 290 | + |
| 291 | +- `Antialiased` mode keeps the continuous coverage generated by the area/cover math. |
| 292 | +- `Aliased` mode thresholds that continuous coverage using `AntialiasThreshold`. |
| 293 | + |
| 294 | +This is important because the scan-conversion core stays the same in both modes. Only the final conversion from area to emitted coverage changes. |
| 295 | + |
| 296 | +## Why Self-Intersections Work |
| 297 | + |
| 298 | +The scanner 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. |
| 299 | + |
| 300 | +That means overlapping or self-crossing contours are resolved by: |
| 301 | + |
| 302 | +- area/cover integration |
| 303 | +- winding or parity mapping |
| 304 | + |
| 305 | +instead of by an earlier polygon-boolean pass. |
| 306 | + |
| 307 | +## Practical Performance Characteristics |
| 308 | + |
| 309 | +The current scanner favors retained preparation and cheap repeated execution. |
| 310 | + |
| 311 | +The major performance ideas are: |
| 312 | + |
| 313 | +- build retained geometry once |
| 314 | +- execute compact band-local line lists |
| 315 | +- reuse worker-owned scratch |
| 316 | +- skip untouched rows through explicit tracking |
| 317 | +- walk only touched columns via bit vectors |
| 318 | +- fold off-screen left-of-band influence into start-cover seeds |
| 319 | + |
| 320 | +For larger retained-fill workloads, the current retained line-block storage and row-band model substantially reduce execution work compared with rediscovering geometry during row composition. |
| 321 | + |
| 322 | +## How To Read The Code |
| 323 | + |
| 324 | +If you are new to this part of the library, read the scanner in this order: |
| 325 | + |
| 326 | +1. `CreateRasterizableGeometry(...)` in `DefaultRasterizer.cs` |
| 327 | +2. `Linearizer<TL>` and the concrete linearizers in `DefaultRasterizer.Linearizer.cs` |
| 328 | +3. retained line types in `DefaultRasterizer.RetainedTypes.cs` |
| 329 | +4. `ExecuteRasterizableBand(...)` in `DefaultRasterizer.cs` |
| 330 | +5. `Context` in `DefaultRasterizer.cs` |
| 331 | + |
| 332 | +That order mirrors the real lifecycle of the data: |
| 333 | + |
| 334 | +- geometry preparation |
| 335 | +- retained storage |
| 336 | +- band execution |
| 337 | +- coverage emission |
| 338 | + |
| 339 | +## Closing Mental Model |
| 340 | + |
| 341 | +The easiest way to reason about `DefaultRasterizer` is this: |
| 342 | + |
| 343 | +> 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 |
| 344 | +
|
| 345 | +If that model stays clear, the rest of the code becomes easier to read: |
| 346 | + |
| 347 | +- the linearizer explains where retained line data comes from |
| 348 | +- the retained line arrays explain how it is stored |
| 349 | +- the `Context` explains how it becomes coverage |
| 350 | +- the backend explains how that coverage becomes pixels |
0 commit comments