Optimize identity raster paths
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user