Optimize identity raster paths

This commit is contained in:
gamer147
2026-07-11 15:31:56 -04:00
parent 0cb72e0d5e
commit 1f18a378d3
3 changed files with 190 additions and 4 deletions

View File

@@ -1337,3 +1337,28 @@ The normal-speed FPS samples were effectively unchanged from quick win 2 (54/26/
backbuffer landed. The cleanup removes needless Godot objects/copies and helps future layer-heavy scenes, backbuffer landed. The cleanup removes needless Godot objects/copies and helps future layer-heavy scenes,
but the measured next target is the general inverse-affine per-pixel path—especially identity-transform, but the measured next target is the general inverse-affine per-pixel path—especially identity-transform,
opaque-copy, and axis-aligned fill fast paths. opaque-copy, and axis-aligned fill fast paths.
**Quick win 4 initial raster fast path implemented (2026-07-11).** `SoftwareAffineRasterizer` now detects
exact identity matrices with integral translation and bypasses inverse-matrix construction, floating-point
coordinate application, `Floor`, and per-pixel geometric rejection. Both `BlitRgba` and `FillRgba` use
direct clipped integer indexing for that case while retaining the exact existing tint, multiplicative tint,
source alpha, object opacity, destination blend, and alpha-accumulation arithmetic. Scale, rotation,
fractional translation, singular transforms, and all other geometry retain the original affine fallback.
Zero-opacity draws also return before scanning pixels.
Six new differential cases compare the fast paths byte-for-byte against a retained copy of the pre-fast-path
algorithm across positive/negative clipping, existing destination pixels, ordinary and multiplicative tint,
partial source/object alpha, and solid fills. The full engine suite is **141/141**; Godot builds with zero
warnings and threaded `SELFTEST OK`. The differential SC0000 page image retains SHA-256
`E669355772D4D9118BE80AC93595F78BB467B088659DEA4CC2D2B7D0F05B62BE`, subject to the automated-shot
presentation-state caveat above.
Because the console SC0000 state does not match the user's ordinary interactive CG presentation, no
whole-scene FPS claim is made for this slice. An isolated Release-mode 800x600 tinted/alpha identity-blit
kernel comparison measured **2.737 ms** for the new path versus **4.217 ms** for the retained general loop,
or **1.54x** for that layer type. Manual-path fade responsiveness is validated below. A later bounded step
may add a fully opaque/unmodulated row-copy path as optional incremental work.
**Manual validation:** the user tested the ordinary interactive path—the path whose CG state and fade
cadence differ from the console capture—and reported a **massive improvement**. This confirms the identity
blit/fill fast path materially improves real fade responsiveness, not merely the isolated kernel benchmark.

View File

@@ -3,6 +3,48 @@ using Xunit;
public class SoftwareAffineRasterizerTests public class SoftwareAffineRasterizerTests
{ {
[Theory]
[InlineData(-1, 2, false)]
[InlineData(4, -1, false)]
[InlineData(1, 1, true)]
public void BlitRgba_IntegerTranslationFastPath_MatchesGeneralAffine(int tx, int ty, bool multiplyTint)
{
byte[] src = new byte[5 * 4 * 4];
for (int i = 0; i < src.Length; i += 4)
{
src[i] = (byte)(20 + i % 211); src[i + 1] = (byte)(70 + i % 173);
src[i + 2] = (byte)(130 + i % 107); src[i + 3] = (byte)(40 + i % 216);
}
byte[] expected = new byte[6 * 5 * 4];
for (int i = 0; i < expected.Length; i++) expected[i] = (byte)(i * 17 + 3);
byte[] actual = (byte[])expected.Clone();
var translation = new Affine2D(1, 0, 0, 1, tx, ty);
ReferenceBlit(expected, 6, 5, src, 5, 1, 1, 3, 2, translation,
0x4080c0, 0.35f, 0.6f, multiplyTint);
SoftwareAffineRasterizer.BlitRgba(actual, 6, 5, src, 5, 4, 1, 1, 3, 2, translation,
0x4080c0, 0.35f, 0.6f, multiplyTint);
Assert.Equal(expected, actual);
}
[Theory]
[InlineData(-2, 1)]
[InlineData(4, -1)]
[InlineData(1, 2)]
public void FillRgba_IntegerTranslationFastPath_MatchesGeneralAffine(int tx, int ty)
{
byte[] expected = new byte[6 * 5 * 4];
for (int i = 0; i < expected.Length; i++) expected[i] = (byte)(i * 11 + 5);
byte[] actual = (byte[])expected.Clone();
var translation = new Affine2D(1, 0, 0, 1, tx, ty);
ReferenceFill(expected, 6, 5, 4, 3, translation, 0x9a4f21, 0.42f);
SoftwareAffineRasterizer.FillRgba(actual, 6, 5, 4, 3, translation, 0x9a4f21, 0.42f);
Assert.Equal(expected, actual);
}
[Fact] [Fact]
public void BlitRgba_RotatesTwoPixelsClockwiseWithNearestSampling() public void BlitRgba_RotatesTwoPixelsClockwiseWithNearestSampling()
{ {
@@ -56,4 +98,62 @@ public class SoftwareAffineRasterizerTests
identity, 0xffffff, 0, 0, multiplyTint: true); identity, 0xffffff, 0, 0, multiplyTint: true);
Assert.Equal(new byte[4], hidden); Assert.Equal(new byte[4], hidden);
} }
// Pre-fast-path affine algorithm retained here as an independent differential oracle.
private static void ReferenceBlit(byte[] dst, int dstW, int dstH, byte[] src, int srcW,
int srcX, int srcY, int width, int height, Affine2D transform,
long tint, float tintStrength, float opacity, bool multiplyTint)
{
if (!transform.TryInverse(out var inv)) return;
ReferenceBounds(transform, width, height, dstW, dstH, out int x0, out int y0, out int x1, out int y1);
int istr = (int)(System.Math.Clamp(tintStrength, 0f, 1f) * 255);
int ia = (int)(System.Math.Clamp(opacity, 0f, 1f) * 255);
int tr = (int)(tint >> 16 & 255), tg = (int)(tint >> 8 & 255), tb = (int)(tint & 255);
for (int y = y0; y < y1; y++) for (int x = x0; x < x1; x++)
{
var p = inv.Apply(x + 0.5, y + 0.5);
int u = (int)System.Math.Floor(p.X), v = (int)System.Math.Floor(p.Y);
if ((uint)u >= (uint)width || (uint)v >= (uint)height) continue;
int si = ((srcY + v) * srcW + (srcX + u)) * 4, di = (y * dstW + x) * 4;
int sa = src[si + 3] * ia / 255;
if (sa == 0) continue;
int sr = multiplyTint ? src[si] * tr / 255 : (src[si] * (255 - istr) + tr * istr) / 255;
int sg = multiplyTint ? src[si + 1] * tg / 255 : (src[si + 1] * (255 - istr) + tg * istr) / 255;
int sb = multiplyTint ? src[si + 2] * tb / 255 : (src[si + 2] * (255 - istr) + tb * istr) / 255;
ReferenceBlend(dst, di, sr, sg, sb, sa);
}
}
private static void ReferenceFill(byte[] dst, int dstW, int dstH, int width, int height,
Affine2D transform, long color, float opacity)
{
if (!transform.TryInverse(out var inv)) return;
ReferenceBounds(transform, width, height, dstW, dstH, out int x0, out int y0, out int x1, out int y1);
int a = (int)(System.Math.Clamp(opacity, 0f, 1f) * 255);
int r = (int)(color >> 16 & 255), g = (int)(color >> 8 & 255), b = (int)(color & 255);
for (int y = y0; y < y1; y++) for (int x = x0; x < x1; x++)
{
var p = inv.Apply(x + 0.5, y + 0.5);
if (p.X >= 0 && p.X < width && p.Y >= 0 && p.Y < height)
ReferenceBlend(dst, (y * dstW + x) * 4, r, g, b, a);
}
}
private static void ReferenceBounds(Affine2D m, int w, int h, int dw, int dh,
out int x0, out int y0, out int x1, out int y1)
{
var a = m.Apply(0, 0); var b = m.Apply(w, 0); var c = m.Apply(0, h); var d = m.Apply(w, h);
x0 = System.Math.Max(0, (int)System.Math.Floor(System.Math.Min(System.Math.Min(a.X, b.X), System.Math.Min(c.X, d.X))));
y0 = System.Math.Max(0, (int)System.Math.Floor(System.Math.Min(System.Math.Min(a.Y, b.Y), System.Math.Min(c.Y, d.Y))));
x1 = System.Math.Min(dw, (int)System.Math.Ceiling(System.Math.Max(System.Math.Max(a.X, b.X), System.Math.Max(c.X, d.X))));
y1 = System.Math.Min(dh, (int)System.Math.Ceiling(System.Math.Max(System.Math.Max(a.Y, b.Y), System.Math.Max(c.Y, d.Y))));
}
private static void ReferenceBlend(byte[] dst, int i, int r, int g, int b, int a)
{
dst[i] = (byte)((r * a + dst[i] * (255 - a)) / 255);
dst[i + 1] = (byte)((g * a + dst[i + 1] * (255 - a)) / 255);
dst[i + 2] = (byte)((b * a + dst[i + 2] * (255 - a)) / 255);
dst[i + 3] = (byte)System.Math.Min(255, dst[i + 3] + a);
}
} }

View File

@@ -7,11 +7,19 @@ public static class SoftwareAffineRasterizer
int srcX, int srcY, int width, int height, Affine2D localToDest, int srcX, int srcY, int width, int height, Affine2D localToDest,
long tint, float tintStrength, float opacity, bool multiplyTint = false) long tint, float tintStrength, float opacity, bool multiplyTint = false)
{ {
if (width <= 0 || height <= 0 || !localToDest.TryInverse(out var inv)) return; if (width <= 0 || height <= 0) return;
Bounds(localToDest, width, height, dstW, dstH, out int x0, out int y0, out int x1, out int y1);
int istr = (int)(System.Math.Clamp(tintStrength, 0f, 1f) * 255); int istr = (int)(System.Math.Clamp(tintStrength, 0f, 1f) * 255);
int ia = (int)(System.Math.Clamp(opacity, 0f, 1f) * 255); int ia = (int)(System.Math.Clamp(opacity, 0f, 1f) * 255);
if (ia == 0) return;
int tr=(int)(tint>>16&255), tg=(int)(tint>>8&255), tb=(int)(tint&255); int tr=(int)(tint>>16&255), tg=(int)(tint>>8&255), tb=(int)(tint&255);
if (TryIntegerTranslation(localToDest, out int tx, out int ty))
{
BlitTranslated(dst, dstW, dstH, src, srcW, srcX, srcY, width, height,
tx, ty, tr, tg, tb, istr, ia, multiplyTint);
return;
}
if (!localToDest.TryInverse(out var inv)) return;
Bounds(localToDest, width, height, dstW, dstH, out int x0, out int y0, out int x1, out int y1);
for (int y=y0; y<y1; y++) for (int x=x0; x<x1; x++) for (int y=y0; y<y1; y++) for (int x=x0; x<x1; x++)
{ {
var p = inv.Apply(x + 0.5, y + 0.5); var p = inv.Apply(x + 0.5, y + 0.5);
@@ -29,16 +37,69 @@ public static class SoftwareAffineRasterizer
public static void FillRgba(byte[] dst, int dstW, int dstH, int width, int height, public static void FillRgba(byte[] dst, int dstW, int dstH, int width, int height,
Affine2D localToDest, long color, float opacity) Affine2D localToDest, long color, float opacity)
{ {
if (width <= 0 || height <= 0 || !localToDest.TryInverse(out var inv)) return; if (width <= 0 || height <= 0) return;
Bounds(localToDest,width,height,dstW,dstH,out int x0,out int y0,out int x1,out int y1);
int a=(int)(System.Math.Clamp(opacity,0f,1f)*255); if(a==0)return; int a=(int)(System.Math.Clamp(opacity,0f,1f)*255); if(a==0)return;
int r=(int)(color>>16&255),g=(int)(color>>8&255),b=(int)(color&255); int r=(int)(color>>16&255),g=(int)(color>>8&255),b=(int)(color&255);
if (TryIntegerTranslation(localToDest, out int tx, out int ty))
{
FillTranslated(dst, dstW, dstH, width, height, tx, ty, r, g, b, a);
return;
}
if (!localToDest.TryInverse(out var inv)) return;
Bounds(localToDest,width,height,dstW,dstH,out int x0,out int y0,out int x1,out int y1);
for(int y=y0;y<y1;y++)for(int x=x0;x<x1;x++){ for(int y=y0;y<y1;y++)for(int x=x0;x<x1;x++){
var p=inv.Apply(x+.5,y+.5); var p=inv.Apply(x+.5,y+.5);
if(p.X>=0&&p.X<width&&p.Y>=0&&p.Y<height) Blend(dst,(y*dstW+x)*4,r,g,b,a); if(p.X>=0&&p.X<width&&p.Y>=0&&p.Y<height) Blend(dst,(y*dstW+x)*4,r,g,b,a);
} }
} }
private static bool TryIntegerTranslation(Affine2D m, out int tx, out int ty)
{
tx = ty = 0;
if (m.XX != 1 || m.XY != 0 || m.YX != 0 || m.YY != 1 ||
m.TX != System.Math.Truncate(m.TX) || m.TY != System.Math.Truncate(m.TY) ||
m.TX < int.MinValue || m.TX > int.MaxValue || m.TY < int.MinValue || m.TY > int.MaxValue)
return false;
tx = (int)m.TX;
ty = (int)m.TY;
return true;
}
private static void BlitTranslated(byte[] dst, int dstW, int dstH, byte[] src, int srcW,
int srcX, int srcY, int width, int height, int tx, int ty,
int tr, int tg, int tb, int istr, int ia, bool multiplyTint)
{
int x0 = System.Math.Max(0, tx), y0 = System.Math.Max(0, ty);
int x1 = (int)System.Math.Min(dstW, (long)tx + width);
int y1 = (int)System.Math.Min(dstH, (long)ty + height);
for (int y = y0; y < y1; y++)
{
int v = y - ty;
for (int x = x0; x < x1; x++)
{
int u = x - tx;
int si = ((srcY + v) * srcW + (srcX + u)) * 4, di = (y * dstW + x) * 4;
int sa = src[si + 3] * ia / 255;
if (sa == 0) continue;
int sr = multiplyTint ? src[si] * tr / 255 : (src[si] * (255 - istr) + tr * istr) / 255;
int sg = multiplyTint ? src[si + 1] * tg / 255 : (src[si + 1] * (255 - istr) + tg * istr) / 255;
int sb = multiplyTint ? src[si + 2] * tb / 255 : (src[si + 2] * (255 - istr) + tb * istr) / 255;
Blend(dst, di, sr, sg, sb, sa);
}
}
}
private static void FillTranslated(byte[] dst, int dstW, int dstH, int width, int height,
int tx, int ty, int r, int g, int b, int a)
{
int x0 = System.Math.Max(0, tx), y0 = System.Math.Max(0, ty);
int x1 = (int)System.Math.Min(dstW, (long)tx + width);
int y1 = (int)System.Math.Min(dstH, (long)ty + height);
for (int y = y0; y < y1; y++)
for (int x = x0; x < x1; x++)
Blend(dst, (y * dstW + x) * 4, r, g, b, a);
}
private static void Bounds(Affine2D m,int w,int h,int dw,int dh,out int x0,out int y0,out int x1,out int y1) private static void Bounds(Affine2D m,int w,int h,int dw,int dh,out int x0,out int y0,out int x1,out int y1)
{ {
var a=m.Apply(0,0);var b=m.Apply(w,0);var c=m.Apply(0,h);var d=m.Apply(w,h); var a=m.Apply(0,0);var b=m.Apply(w,0);var c=m.Apply(0,h);var d=m.Apply(w,h);