// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Buffers; using System.Numerics; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing.Backends { internal static partial class DefaultRasterizer { /// /// Base class that lowers translated geometry into retained per-row line storage. /// /// The mutable per-row line collector type. private abstract class Linearizer where TL : class { private bool hasAnyCoverage; protected Linearizer( LinearGeometry geometry, Matrix4x4 residual, int translateX, int translateY, int minX, int minY, int width, int height, int firstBandIndex, int rowBandCount, float samplingOffsetX, float samplingOffsetY, MemoryAllocator allocator) { this.Geometry = geometry; this.Residual = residual; this.HasResidual = !residual.IsIdentity; this.TranslateX = translateX; this.TranslateY = translateY; this.MinX = minX; this.MinY = minY; this.Width = width; this.Height = height; this.FirstBandIndex = firstBandIndex; this.RowBandCount = rowBandCount; this.SamplingOffsetX = samplingOffsetX; this.SamplingOffsetY = samplingOffsetY; this.Allocator = allocator; this.BandTopStart = (firstBandIndex * PreferredRowHeight) - minY; this.FirstBlockLineCounts = new int[rowBandCount]; this.LineCounts = new int[rowBandCount]; this.StartCoverTable = new IMemoryOwner?[rowBandCount]; this.LineArrays = new TL?[rowBandCount]; } /// /// Gets the source geometry being lowered. /// protected LinearGeometry Geometry { get; } /// /// Gets the residual transform applied to each source point during emission. /// protected Matrix4x4 Residual { get; } /// /// Gets a value indicating whether is non-identity. /// protected bool HasResidual { get; } /// /// Gets the translated X offset applied to the geometry. /// protected int TranslateX { get; } /// /// Gets the translated Y offset applied to the geometry. /// protected int TranslateY { get; } /// /// Gets the minimum destination X bound after clipping. /// protected int MinX { get; } /// /// Gets the minimum destination Y bound after clipping. /// protected int MinY { get; } /// /// Gets the visible destination width in pixels. /// protected int Width { get; } /// /// Gets the visible destination height in pixels. /// protected int Height { get; } /// /// Gets the first retained row-band index touched by the geometry. /// protected int FirstBandIndex { get; } /// /// Gets the number of retained row bands owned by the geometry. /// protected int RowBandCount { get; } /// /// Gets the horizontal sampling offset applied before fixed-point conversion. /// protected float SamplingOffsetX { get; } /// /// Gets the vertical sampling offset applied before fixed-point conversion. /// protected float SamplingOffsetY { get; } /// /// Gets the allocator used for retained start-cover storage. /// protected MemoryAllocator Allocator { get; } /// /// Gets the top offset, in whole pixels, of the first retained row band. /// protected int BandTopStart { get; } /// /// Gets the mutable per-row line collectors used during lowering. /// protected TL?[] LineArrays { get; } /// /// Gets the valid front-block line count for each retained row band. /// protected int[] FirstBlockLineCounts { get; } /// /// Gets the total retained line count for each row band. /// protected int[] LineCounts { get; } /// /// Gets the retained start-cover storage for each row band. /// protected IMemoryOwner?[] StartCoverTable { get; } /// /// Gets a value indicating whether any retained payload was produced. /// protected ref bool HasAnyCoverage => ref this.hasAnyCoverage; /// /// Executes the linearization pass and finalizes the retained row payloads. /// /// when any retained coverage was produced; otherwise . protected virtual bool ProcessCore() { RectangleF translatedBounds = this.HasResidual ? RectangleF.Transform(this.Geometry.Info.Bounds, this.Residual) : this.Geometry.Info.Bounds; translatedBounds.Offset(this.TranslateX + this.SamplingOffsetX - this.MinX, this.TranslateY + this.SamplingOffsetY - this.MinY); bool contains = translatedBounds.Left >= 0F && translatedBounds.Top >= 0F && translatedBounds.Right <= this.Width && translatedBounds.Bottom <= this.Height; // Contained geometry can skip clipping and go straight to the fixed-point band splitter. if (contains) { this.ProcessContained(); } else { // Geometry that touches the interest edges needs clipping so start covers and line // segments still match the destination bounds seen by the rasterizer. this.ProcessUncontained(); } if (!this.hasAnyCoverage) { return false; } this.FinalizeLines(); return true; } /// /// Linearizes geometry that is fully contained inside the destination interest. /// protected void ProcessContained() { SegmentEnumerator enumerator = this.Geometry.GetSegments(); Matrix4x4 residual = this.Residual; bool hasResidual = this.HasResidual; while (enumerator.MoveNext()) { LinearSegment segment = enumerator.Current; PointF p0 = segment.Start; PointF p1 = segment.End; if (hasResidual) { p0 = PointF.Transform(p0, residual); p1 = PointF.Transform(p1, residual); } this.AddContainedLineF24Dot8( FloatToFixed24Dot8(((p0.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX), FloatToFixed24Dot8(((p0.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY), FloatToFixed24Dot8(((p1.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX), FloatToFixed24Dot8(((p1.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY)); } } /// /// Linearizes geometry that intersects the destination interest bounds and requires clipping. /// protected void ProcessUncontained() { SegmentEnumerator enumerator = this.Geometry.GetSegments(); Matrix4x4 residual = this.Residual; bool hasResidual = this.HasResidual; while (enumerator.MoveNext()) { LinearSegment segment = enumerator.Current; PointF p0 = segment.Start; PointF p1 = segment.End; if (hasResidual) { p0 = PointF.Transform(p0, residual); p1 = PointF.Transform(p1, residual); } this.AddUncontainedLine( ((p0.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX, ((p0.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY, ((p1.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX, ((p1.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY); } } /// /// Clips one geometry line against the destination interest and adds the retained result. /// /// The starting X coordinate in translated float space. /// The starting Y coordinate in translated float space. /// The ending X coordinate in translated float space. /// The ending Y coordinate in translated float space. protected void AddUncontainedLine(float x0, float y0, float x1, float y1) { if (y0 == y1) { return; } if (y0 <= 0F && y1 <= 0F) { return; } if (y0 >= this.Height && y1 >= this.Height) { return; } if (x0 >= this.Width && x1 >= this.Width) { return; } if (x0 == x1) { int x0c = Math.Clamp(FloatToFixed24Dot8(x0), 0, this.Width * FixedOne); int p0y = Math.Clamp(FloatToFixed24Dot8(y0), 0, this.Height * FixedOne); int p1y = Math.Clamp(FloatToFixed24Dot8(y1), 0, this.Height * FixedOne); if (x0c == 0) { // Segments clipped fully to the left edge do not produce a visible line, but they // still change winding for rows they cross. Retain that effect as start covers. this.UpdateStartCoversClipped(p0y, p1y); this.hasAnyCoverage = true; } else { this.AddContainedLineF24Dot8(x0c, p0y, x0c, p1y); } return; } double deltayV = Math.Abs(y1 - y0); double deltaxV = x1 - x0; double rx0 = x0; double ry0 = y0; double rx1 = x1; double ry1 = y1; if (y1 > y0) { if (y0 < 0F) { double t = -y0 / deltayV; rx0 = x0 + (deltaxV * t); ry0 = 0D; } if (y1 > this.Height) { double t = (this.Height - y0) / deltayV; rx1 = x0 + (deltaxV * t); ry1 = this.Height; } } else { if (y0 > this.Height) { double t = (y0 - this.Height) / deltayV; rx0 = x0 + (deltaxV * t); ry0 = this.Height; } if (y1 < 0F) { double t = y0 / deltayV; rx1 = x0 + (deltaxV * t); ry1 = 0D; } } if (rx0 >= this.Width && rx1 >= this.Width) { return; } if (rx0 > 0D && rx1 > 0D && rx0 < this.Width && rx1 < this.Width) { this.AddContainedLineF24Dot8( Math.Clamp(FloatToFixed24Dot8((float)rx0), 0, this.Width * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)rx1), 0, this.Width * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne)); return; } if (rx0 <= 0D && rx1 <= 0D) { // A segment that stays left of the visible band contributes winding only. this.UpdateStartCoversClipped( Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne)); this.hasAnyCoverage = true; return; } double deltayH = ry1 - ry0; double deltaxH = Math.Abs(rx1 - rx0); if (rx1 > rx0) { double bx1 = rx1; double by1 = ry1; if (rx1 > this.Width) { double t = (this.Width - rx0) / deltaxH; by1 = ry0 + (deltayH * t); bx1 = this.Width; } if (rx0 < 0D) { double t = -rx0 / deltaxH; int a = Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne); int by = Math.Clamp(FloatToFixed24Dot8((float)(ry0 + (deltayH * t))), 0, this.Height * FixedOne); int cx = Math.Clamp(FloatToFixed24Dot8((float)bx1), 0, this.Width * FixedOne); int cy = Math.Clamp(FloatToFixed24Dot8((float)by1), 0, this.Height * FixedOne); this.UpdateStartCoversClipped(a, by); this.hasAnyCoverage = true; // The visible portion begins exactly at x == 0 after the left-edge clip. this.AddContainedLineF24Dot8(0, by, cx, cy); } else { this.AddContainedLineF24Dot8( Math.Clamp(FloatToFixed24Dot8((float)rx0), 0, this.Width * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)bx1), 0, this.Width * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)by1), 0, this.Height * FixedOne)); } } else { double bx0 = rx0; double by0 = ry0; if (rx0 > this.Width) { double t = (rx0 - this.Width) / deltaxH; by0 = ry0 + (deltayH * t); bx0 = this.Width; } if (rx1 < 0D) { double t = rx0 / deltaxH; int ax = Math.Clamp(FloatToFixed24Dot8((float)bx0), 0, this.Width * FixedOne); int ay = Math.Clamp(FloatToFixed24Dot8((float)by0), 0, this.Height * FixedOne); int by = Math.Clamp(FloatToFixed24Dot8((float)(ry0 + (deltayH * t))), 0, this.Height * FixedOne); int c = Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne); // The right-to-left case mirrors the left-edge handling above: emit the // visible portion first, then retain the winding-only tail as start covers. this.AddContainedLineF24Dot8(ax, ay, 0, by); this.UpdateStartCoversClipped(by, c); this.hasAnyCoverage = true; } else { this.AddContainedLineF24Dot8( Math.Clamp(FloatToFixed24Dot8((float)bx0), 0, this.Width * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)by0), 0, this.Height * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)rx1), 0, this.Width * FixedOne), Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne)); } } } /// /// Adds one fully-contained line segment in 24.8 fixed-point coordinates. /// /// The starting X coordinate. /// The starting Y coordinate. /// The ending X coordinate. /// The ending Y coordinate. protected void AddContainedLineF24Dot8(int x0, int y0, int x1, int y1) { if (y0 == y1) { return; } if (x0 == x1) { if (y0 < y1) { this.VerticalDown(x0, y0, y1); } else { this.VerticalUp(x0, y0, y1); } return; } int dx = Math.Abs(x1 - x0); int dy = Math.Abs(y1 - y0); if (dx > MaximumDelta || dy > MaximumDelta) { int mx = (x0 + x1) >> 1; int my = (y0 + y1) >> 1; this.AddContainedLineF24Dot8(x0, y0, mx, my); this.AddContainedLineF24Dot8(mx, my, x1, y1); return; } int rowIndex0; int rowIndex1; int bandTopStart = this.BandTopStart * FixedOne; int bandHeight = PreferredRowHeight * FixedOne; if (y0 < y1) { rowIndex0 = (y0 - bandTopStart) / bandHeight; rowIndex1 = ((y1 - 1) - bandTopStart) / bandHeight; } else { rowIndex0 = ((y0 - 1) - bandTopStart) / bandHeight; rowIndex1 = (y1 - bandTopStart) / bandHeight; } if ((uint)rowIndex0 >= (uint)this.RowBandCount || (uint)rowIndex1 >= (uint)this.RowBandCount) { return; } if (rowIndex0 == rowIndex1) { int rowTop = bandTopStart + (rowIndex0 * bandHeight); this.AppendLine(rowIndex0, x0, y0 - rowTop, x1, y1 - rowTop); this.LineCounts[rowIndex0]++; this.hasAnyCoverage = true; return; } this.SplitAcrossBands(x0, y0, x1, y1); } /// /// Creates the mutable line collector used for one row band. /// /// The mutable line collector. protected abstract TL CreateLineArray(); /// /// Appends one line segment into the retained row-band collector. /// /// The local row-band index. /// The starting X coordinate relative to the row band. /// The starting Y coordinate relative to the row band. /// The ending X coordinate relative to the row band. /// The ending Y coordinate relative to the row band. protected abstract void AppendLine(int rowIndex, int x0, int y0, int x1, int y1); /// /// Finalizes the mutable collectors into the retained line-block representation. /// protected abstract void FinalizeLines(); /// /// Gets the mutable line collector for a row band, creating it on first use. /// /// The local row-band index. /// The mutable line collector. protected TL GetOrCreateLineArray(int rowIndex) { TL? lineArray = this.LineArrays[rowIndex]; if (lineArray is not null) { return lineArray; } lineArray = this.CreateLineArray(); this.LineArrays[rowIndex] = lineArray; return lineArray; } /// /// Adds a downward vertical segment by delegating to the shared band-splitting path. /// /// The fixed-point X coordinate. /// The starting fixed-point Y coordinate. /// The ending fixed-point Y coordinate. private void VerticalDown(int x, int y0, int y1) => this.SplitAcrossBands(x, y0, x, y1); /// /// Adds an upward vertical segment by delegating to the shared band-splitting path. /// /// The fixed-point X coordinate. /// The starting fixed-point Y coordinate. /// The ending fixed-point Y coordinate. private void VerticalUp(int x, int y0, int y1) => this.SplitAcrossBands(x, y0, x, y1); /// /// Splits a contained line segment at row-band boundaries and appends each retained piece. /// /// The starting X coordinate. /// The starting Y coordinate. /// The ending X coordinate. /// The ending Y coordinate. private void SplitAcrossBands(int x0, int y0, int x1, int y1) { int dy = y1 - y0; int dx = x1 - x0; int bandTopStart = this.BandTopStart * FixedOne; int bandHeight = PreferredRowHeight * FixedOne; int startBand = dy > 0 ? (y0 - bandTopStart) / bandHeight : ((y0 - 1) - bandTopStart) / bandHeight; int endBand = dy > 0 ? ((y1 - 1) - bandTopStart) / bandHeight : (y1 - bandTopStart) / bandHeight; int step = dy > 0 ? 1 : -1; int currentBand = startBand; int currentX = x0; int currentY = y0; while (currentBand != endBand) { int bandBoundaryY = dy > 0 ? bandTopStart + ((currentBand + 1) * bandHeight) : bandTopStart + (currentBand * bandHeight); int deltaY = bandBoundaryY - currentY; int nextX = currentX + (int)(((long)dx * deltaY) / dy); int rowTop = bandTopStart + (currentBand * bandHeight); // Each retained segment is stored in the local coordinate space of its owning band. this.AppendLine(currentBand, currentX, currentY - rowTop, nextX, bandBoundaryY - rowTop); this.LineCounts[currentBand]++; this.hasAnyCoverage = true; currentX = nextX; currentY = bandBoundaryY; currentBand += step; if ((uint)currentBand >= (uint)this.RowBandCount) { return; } } int finalRowTop = bandTopStart + (endBand * bandHeight); this.AppendLine(endBand, currentX, currentY - finalRowTop, x1, y1 - finalRowTop); this.LineCounts[endBand]++; this.hasAnyCoverage = true; } /// /// Updates retained start-cover rows for a line that has been clipped against the visible band. /// /// The clipped starting Y coordinate. /// The clipped ending Y coordinate. private void UpdateStartCoversClipped(int y0, int y1) { if (y0 == y1) { return; } if (y0 < y1) { int bandTopStart = this.BandTopStart * FixedOne; int bandHeight = PreferredRowHeight * FixedOne; int rowIndex0 = (y0 - bandTopStart) / bandHeight; int rowIndex1 = ((y1 - 1) - bandTopStart) / bandHeight; rowIndex0 = Math.Clamp(rowIndex0, 0, this.RowBandCount - 1); rowIndex1 = Math.Clamp(rowIndex1, 0, this.RowBandCount - 1); int fy0 = y0 - (bandTopStart + (rowIndex0 * bandHeight)); int fy1 = y1 - (bandTopStart + (rowIndex1 * bandHeight)); this.UpdateStartCovers(rowIndex0, fy0, rowIndex0 == rowIndex1 ? fy1 : bandHeight); for (int i = rowIndex0 + 1; i < rowIndex1; i++) { // Full interior bands receive a constant winding contribution. this.FillStartCovers(i, -FixedOne); } if (rowIndex0 != rowIndex1) { this.UpdateStartCovers(rowIndex1, 0, fy1); } } else { int bandTopStart = this.BandTopStart * FixedOne; int bandHeight = PreferredRowHeight * FixedOne; int rowIndex0 = ((y0 - 1) - bandTopStart) / bandHeight; int rowIndex1 = (y1 - bandTopStart) / bandHeight; rowIndex0 = Math.Clamp(rowIndex0, 0, this.RowBandCount - 1); rowIndex1 = Math.Clamp(rowIndex1, 0, this.RowBandCount - 1); int fy0 = y0 - (bandTopStart + (rowIndex0 * bandHeight)); int fy1 = y1 - (bandTopStart + (rowIndex1 * bandHeight)); this.UpdateStartCovers(rowIndex0, fy0, rowIndex0 == rowIndex1 ? fy1 : 0); for (int i = rowIndex0 - 1; i > rowIndex1; i--) { // Full interior bands receive a constant winding contribution. this.FillStartCovers(i, FixedOne); } if (rowIndex0 != rowIndex1) { this.UpdateStartCovers(rowIndex1, bandHeight, fy1); } } } /// /// Fills an entire retained start-cover row with a constant winding value. /// /// The local row-band index. /// The constant winding value to add. private void FillStartCovers(int localBandIndex, int value) { IMemoryOwner? owner = this.StartCoverTable[localBandIndex]; if (owner is null) { owner = this.Allocator.Allocate(PreferredRowHeight, AllocationOptions.Clean); this.StartCoverTable[localBandIndex] = owner; owner.Memory.Span[..PreferredRowHeight].Fill(value); return; } Span covers = owner.Memory.Span[..PreferredRowHeight]; for (int i = 0; i < PreferredRowHeight; i++) { covers[i] += value; } } /// /// Updates a retained start-cover row for one clipped vertical interval. /// /// The local row-band index. /// The starting Y coordinate relative to the row band. /// The ending Y coordinate relative to the row band. private void UpdateStartCovers(int localBandIndex, int y0, int y1) { IMemoryOwner? owner = this.StartCoverTable[localBandIndex]; if (owner is null) { owner = this.Allocator.Allocate(PreferredRowHeight, AllocationOptions.Clean); this.StartCoverTable[localBandIndex] = owner; } Span covers = owner.Memory.Span[..PreferredRowHeight]; if (y0 < y1) { UpdateCoverTableDown(covers, y0, y1); } else { UpdateCoverTableUp(covers, y0, y1); } } /// /// Applies a downward winding contribution to one retained start-cover table. /// /// The retained start-cover rows. /// The starting Y coordinate relative to the row band. /// The ending Y coordinate relative to the row band. private static void UpdateCoverTableDown(Span covers, int y0, int y1) { int rowIndex0 = y0 >> FixedShift; int rowIndex1 = (y1 - 1) >> FixedShift; int fy0 = y0 - (rowIndex0 << FixedShift); int fy1 = y1 - (rowIndex1 << FixedShift); if (rowIndex0 == rowIndex1) { covers[rowIndex0] -= fy1 - fy0; return; } covers[rowIndex0] -= FixedOne - fy0; for (int i = rowIndex0 + 1; i < rowIndex1; i++) { covers[i] -= FixedOne; } covers[rowIndex1] -= fy1; } /// /// Applies an upward winding contribution to one retained start-cover table. /// /// The retained start-cover rows. /// The starting Y coordinate relative to the row band. /// The ending Y coordinate relative to the row band. private static void UpdateCoverTableUp(Span covers, int y0, int y1) { int rowIndex0 = (y0 - 1) >> FixedShift; int rowIndex1 = y1 >> FixedShift; int fy0 = y0 - (rowIndex0 << FixedShift); int fy1 = y1 - (rowIndex1 << FixedShift); if (rowIndex0 == rowIndex1) { covers[rowIndex0] += fy0 - fy1; return; } covers[rowIndex0] += fy0; for (int i = rowIndex0 - 1; i > rowIndex1; i--) { covers[i] += FixedOne; } covers[rowIndex1] += FixedOne - fy1; } } /// /// Linearizer that finalizes retained lines into the 32-bit-X encoding. /// private sealed class LinearizerX32Y16 : Linearizer { /// /// Initializes a new instance of the class. /// public LinearizerX32Y16( LinearGeometry geometry, Matrix4x4 residual, int translateX, int translateY, int minX, int minY, int width, int height, int firstBandIndex, int rowBandCount, float samplingOffsetX, float samplingOffsetY, MemoryAllocator allocator) : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) => this.FinalLines = new LineArrayX32Y16Block?[rowBandCount]; /// /// Gets the finalized retained line blocks for each row band. /// public LineArrayX32Y16Block?[] FinalLines { get; } /// protected override LineArrayX32Y16 CreateLineArray() => new(); /// protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1) => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1); /// protected override void FinalizeLines() { for (int i = 0; i < this.RowBandCount; i++) { LineArrayX32Y16? lineArray = this.LineArrays[i]; this.FinalLines[i] = lineArray?.GetFrontBlock(); this.FirstBlockLineCounts[i] = lineArray?.GetFrontBlockLineCount() ?? 0; } } /// /// Executes the 32-bit-X linearization pass and returns the retained result. /// /// The finalized retained raster data. /// when retained coverage was produced; otherwise . internal bool TryProcess(out LinearizedRasterData result) { if (!this.ProcessCore()) { result = null!; return false; } result = new LinearizedRasterData( this.Geometry, new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount), this.FinalLines, this.FirstBlockLineCounts, this.StartCoverTable); return true; } } /// /// Linearizer that finalizes retained lines into the packed 16-bit-X encoding. /// private sealed class LinearizerX16Y16 : Linearizer { /// /// Initializes a new instance of the class. /// public LinearizerX16Y16( LinearGeometry geometry, Matrix4x4 residual, int translateX, int translateY, int minX, int minY, int width, int height, int firstBandIndex, int rowBandCount, float samplingOffsetX, float samplingOffsetY, MemoryAllocator allocator) : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) => this.FinalLines = new LineArrayX16Y16Block?[rowBandCount]; /// /// Gets the finalized retained line blocks for each row band. /// public LineArrayX16Y16Block?[] FinalLines { get; } /// protected override LineArrayX16Y16 CreateLineArray() => new(); /// protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1) => this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1); /// protected override void FinalizeLines() { for (int i = 0; i < this.RowBandCount; i++) { LineArrayX16Y16? lineArray = this.LineArrays[i]; this.FinalLines[i] = lineArray?.GetFrontBlock(); this.FirstBlockLineCounts[i] = lineArray?.GetFrontBlockLineCount() ?? 0; } } /// /// Executes the 16-bit-X linearization pass and returns the retained result. /// /// The finalized retained raster data. /// when retained coverage was produced; otherwise . internal bool TryProcess(out LinearizedRasterData result) { if (!this.ProcessCore()) { result = null!; return false; } result = new LinearizedRasterData( this.Geometry, new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount), this.FinalLines, this.FirstBlockLineCounts, this.StartCoverTable); return true; } } } }