Profile and optimize retained rendering
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
<Compile Include="..\..\godot\PageLocatorState.cs" Link="PageLocatorState.cs" />
|
||||
<Compile Include="..\..\godot\GodotTimelineLog.cs" Link="GodotTimelineLog.cs" />
|
||||
<Compile Include="..\..\godot\GodotTraceSink.cs" Link="GodotTraceSink.cs" />
|
||||
<Compile Include="..\..\godot\PerformanceFrameLog.cs" Link="PerformanceFrameLog.cs" />
|
||||
<Compile Include="..\..\godot\MovieSurfaceRegistry.cs" Link="MovieSurfaceRegistry.cs" />
|
||||
<Compile Include="..\..\godot\RiffWaveSanitizer.cs" Link="RiffWaveSanitizer.cs" />
|
||||
<Compile Include="..\..\tools\movie-corpus-gate\MovieCorpusGate.cs" Link="MovieCorpusGate.cs" />
|
||||
|
||||
@@ -25,6 +25,25 @@ public class GfxAnimationTests
|
||||
Assert.True(o.TranslationEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CallerOwnedVisibleSnapshot_ReusesStorageAndPreservesHandleOrder()
|
||||
{
|
||||
var g = new GfxState();
|
||||
for (int i = 999; i >= 0; i--)
|
||||
g.BindDraw(0x1000 + i, 1, 0, 0, 1, 1, i, 0);
|
||||
var snapshot = new List<RenderObject>();
|
||||
g.SnapshotVisibleObjects(0, snapshot); // Grow and warm the buffer outside the measured interval.
|
||||
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int i = 0; i < 10; i++) g.SnapshotVisibleObjects(i, snapshot);
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
Assert.Equal(0, allocated);
|
||||
Assert.Equal(1000, snapshot.Count);
|
||||
Assert.Equal(0x1000, snapshot[0].Handle);
|
||||
Assert.Equal(0x1000 + 999, snapshot[^1].Handle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CurrentScaleSetter_ExpandsGlowAroundItsAnchor()
|
||||
{
|
||||
@@ -94,18 +113,22 @@ public class GfxAnimationTests
|
||||
|
||||
var unchanged = VisibleObject();
|
||||
Assert.False(unchanged.HasActiveVisualPresentation(1000));
|
||||
Assert.False(unchanged.SnapshotVisibleObjects(1000).Single().TimeVarying);
|
||||
|
||||
var spritesheet = VisibleObject();
|
||||
spritesheet.SetSrcRect(7, 4, 1, 0, 800);
|
||||
Assert.True(spritesheet.HasActiveVisualPresentation(1000));
|
||||
Assert.True(spritesheet.SnapshotVisibleObjects(1000).Single().TimeVarying);
|
||||
|
||||
var color = VisibleObject();
|
||||
color.SetColorAnim(7, 1000, GfxState.PackColor(0x80, 0xff0000));
|
||||
Assert.True(color.HasActiveVisualPresentation(1000));
|
||||
Assert.True(color.SnapshotVisibleObjects(1000).Single().TimeVarying);
|
||||
|
||||
var rotation = VisibleObject();
|
||||
rotation.SetRotationCycle(7, 1000, (0, 0, 1));
|
||||
Assert.True(rotation.HasActiveVisualPresentation(1000));
|
||||
Assert.True(rotation.SnapshotVisibleObjects(1000).Single().TimeVarying);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -129,6 +152,108 @@ public class GfxAnimationTests
|
||||
Assert.Equal((0, 0, 200, 200), (wrapped.SrcX, wrapped.SrcY, wrapped.W, wrapped.H));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PresentationReasons_PublishMutationOnce_AndSpritesheetOnlyAtCellBoundaries()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.SetSurface(4, 0x37, -1);
|
||||
g.BindDraw(7, 4, 0, 0, 16, 16, 0, 0);
|
||||
g.SetSrcRect(7, frameCount: 4, columns: 2, cell: 0, period: 200);
|
||||
|
||||
Assert.Equal(GfxPresentationReason.RetainedMutation, g.ConsumePresentationReasons(1000));
|
||||
Assert.Equal(GfxPresentationReason.None, g.ConsumePresentationReasons(1199));
|
||||
Assert.Equal(GfxPresentationReason.DiscreteSourceCell, g.ConsumePresentationReasons(1200));
|
||||
Assert.Equal(GfxPresentationReason.None, g.ConsumePresentationReasons(1399));
|
||||
Assert.Equal(GfxPresentationReason.DiscreteSourceCell, g.ConsumePresentationReasons(1400));
|
||||
Assert.Equal(GfxPresentationReason.None, g.ConsumePresentationReasons(1599));
|
||||
Assert.Equal(GfxPresentationReason.DiscreteSourceCell, g.ConsumePresentationReasons(1600));
|
||||
Assert.Equal(GfxPresentationReason.None, g.ConsumePresentationReasons(1799));
|
||||
Assert.Equal(GfxPresentationReason.DiscreteSourceCell, g.ConsumePresentationReasons(1800));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PresentationReasons_CloneBeforeFirstSampleSharesPhase_AndCloneAfterKeepsIt()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.SetSurface(4, 0x37, -1);
|
||||
g.BindDraw(10, 4, 0, 0, 16, 16, 0, 0);
|
||||
g.SetSrcRect(10, frameCount: 4, columns: 2, cell: 0, period: 200);
|
||||
Assert.True(g.CloneObject(10, 11));
|
||||
|
||||
Assert.Equal(GfxPresentationReason.RetainedMutation, g.ConsumePresentationReasons(1000));
|
||||
Assert.Equal(1000, g.TryGet(10)!.SrcStart);
|
||||
Assert.Equal(1000, g.TryGet(11)!.SrcStart);
|
||||
Assert.Equal(GfxPresentationReason.DiscreteSourceCell, g.ConsumePresentationReasons(1200));
|
||||
Assert.Equal(g.SnapshotVisibleObjects(1200).Single(x => x.Handle == 10).SrcX,
|
||||
g.SnapshotVisibleObjects(1200).Single(x => x.Handle == 11).SrcX);
|
||||
|
||||
Assert.True(g.CloneObject(10, 12));
|
||||
Assert.Equal(GfxPresentationReason.RetainedMutation, g.ConsumePresentationReasons(1250));
|
||||
Assert.Equal(1000, g.TryGet(12)!.SrcStart);
|
||||
Assert.Equal(GfxPresentationReason.DiscreteSourceCell, g.ConsumePresentationReasons(1400));
|
||||
var cells = g.SnapshotVisibleObjects(1400).Select(x => x.SrcX).Distinct().ToArray();
|
||||
Assert.Single(cells);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PresentationReasons_UsesObjectLocalPeriods_AndReconfigurationRestartsAtSharedSample()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.SetSurface(4, 0x37, -1);
|
||||
g.BindDraw(10, 4, 0, 0, 16, 16, 0, 0);
|
||||
g.BindDraw(11, 4, 0, 0, 16, 16, 0, 0);
|
||||
g.SetSrcRect(10, frameCount: 4, columns: 2, cell: 0, period: 100);
|
||||
g.SetSrcRect(11, frameCount: 4, columns: 2, cell: 0, period: 250);
|
||||
g.ConsumePresentationReasons(1000);
|
||||
|
||||
Assert.Equal(GfxPresentationReason.None, g.ConsumePresentationReasons(1099));
|
||||
Assert.Equal(GfxPresentationReason.DiscreteSourceCell, g.ConsumePresentationReasons(1100));
|
||||
Assert.Equal(GfxPresentationReason.None, g.ConsumePresentationReasons(1199));
|
||||
Assert.Equal(GfxPresentationReason.DiscreteSourceCell, g.ConsumePresentationReasons(1200));
|
||||
Assert.Equal(GfxPresentationReason.None, g.ConsumePresentationReasons(1249));
|
||||
Assert.Equal(GfxPresentationReason.DiscreteSourceCell, g.ConsumePresentationReasons(1250));
|
||||
|
||||
g.SetSrcRect(10, frameCount: 4, columns: 2, cell: 0, period: 400);
|
||||
Assert.Equal(GfxPresentationReason.RetainedMutation, g.ConsumePresentationReasons(1250));
|
||||
Assert.Equal(1250, g.TryGet(10)!.SrcStart);
|
||||
Assert.Equal(GfxPresentationReason.None, g.ConsumePresentationReasons(1499));
|
||||
// The second object's 250 ms boundary and the reconfigured object's 400 ms boundary coincide here.
|
||||
Assert.Equal(GfxPresentationReason.DiscreteSourceCell, g.ConsumePresentationReasons(1650));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PresentationReasons_ContinuousChannelRemainsFrameDriven()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.SetSurface(1, 5, -1);
|
||||
g.BindDraw(7, 1, 0, 0, 64, 64, 0, 0);
|
||||
g.SetColorAnim(7, 1000, GfxState.PackColor(0x80, 0xff0000));
|
||||
|
||||
var first = g.ConsumePresentationReasons(1000);
|
||||
Assert.True((first & GfxPresentationReason.RetainedMutation) != 0);
|
||||
Assert.True((first & GfxPresentationReason.ContinuousChannel) != 0);
|
||||
Assert.Equal(GfxPresentationReason.ContinuousChannel, g.ConsumePresentationReasons(1016));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PresentationReasons_PhaseLockedUnitFamilyPublishesFiveCellChangesPerSecond()
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.SetSurface(4, 0x37, -1);
|
||||
g.BindDraw(100, 4, 0, 0, 16, 16, 0, 0);
|
||||
g.SetSrcRect(100, frameCount: 4, columns: 2, cell: 0, period: 200);
|
||||
for (long handle = 101; handle < 151; handle++) Assert.True(g.CloneObject(100, handle));
|
||||
g.ConsumePresentationReasons(1000);
|
||||
|
||||
int cellChanges = 0;
|
||||
for (long now = 1001; now <= 2000; now++)
|
||||
if ((g.ConsumePresentationReasons(now) & GfxPresentationReason.DiscreteSourceCell) != 0)
|
||||
cellChanges++;
|
||||
|
||||
Assert.Equal(5, cellChanges);
|
||||
Assert.Single(g.Objects.Select(pair => g.TryGet(pair.Handle)!.SrcStart).Distinct());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OneShotRotation_SharesMatrixClockAndMatchesNativeSample()
|
||||
{
|
||||
|
||||
@@ -60,6 +60,33 @@ public class MovieSurfaceRegistryTests
|
||||
Assert.False(registry.IsKnownMovieResource(0x2af5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResourceLookup_SelectsNewestPublishedPlaybackWithoutAllocating()
|
||||
{
|
||||
var registry = new MovieSurfaceRegistry();
|
||||
MovieSurfaceBinding older = registry.Begin(7, 0x2b42, out _);
|
||||
MovieSurfaceBinding unrelated = registry.Begin(8, 0x2bad, out _);
|
||||
MovieSurfaceBinding newer = registry.Begin(9, 0x2b42, out _);
|
||||
Assert.True(registry.PublishFrame(older.PlaybackId, Frame(1), "OLDER.AGF", 1));
|
||||
Assert.True(registry.PublishFrame(unrelated.PlaybackId, Frame(2), "OTHER.AGF", 2));
|
||||
Assert.True(registry.PublishFrame(newer.PlaybackId, Frame(3), "NEWER.AGF", 3));
|
||||
Assert.True(registry.TryResolveResource(0x2b42, out MovieSurfaceFrame? warm));
|
||||
Assert.Equal("NEWER.AGF", warm!.Name);
|
||||
Assert.False(registry.TryResolveResource(0x2af5, out _));
|
||||
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
bool resultsCorrect = true;
|
||||
for (int i = 0; i < 10_000; i++)
|
||||
{
|
||||
resultsCorrect &= registry.TryResolveResource(0x2b42, out _);
|
||||
resultsCorrect &= !registry.TryResolveResource(0x2af5, out _);
|
||||
}
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
Assert.True(resultsCorrect);
|
||||
Assert.Equal(0, allocated);
|
||||
}
|
||||
|
||||
private static RgbaImage Frame(byte value)
|
||||
=> new(1, 1, new[] { value, value, value, (byte)255 });
|
||||
}
|
||||
|
||||
97
engine/Age.Engine.Tests/PerformanceFrameLogTests.cs
Normal file
97
engine/Age.Engine.Tests/PerformanceFrameLogTests.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using Age.Engine.Model;
|
||||
|
||||
public class PerformanceFrameLogTests
|
||||
{
|
||||
[Fact]
|
||||
public void WritesStableCsvSchemaAndFrameWorkload()
|
||||
{
|
||||
string path = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
|
||||
$"age-perf-{Guid.NewGuid():N}.csv");
|
||||
try
|
||||
{
|
||||
using (var log = new PerformanceFrameLog(path))
|
||||
{
|
||||
log.BeginFrame(17, 1234, 1.0 / 60.0, "SC0000.BIN", 0x2a91, 0x20c);
|
||||
log.BeginRecomposite(false);
|
||||
log.RecordPresentationReasons(1 | 4 | 16);
|
||||
log.RecordRecomposeAllocation(4096);
|
||||
log.RecordSnapshotAllocation(512);
|
||||
log.RecordCompositeAllocation(2048);
|
||||
log.RecordSourcePrepAllocation(128);
|
||||
log.RecordSetDataAllocation(1024);
|
||||
log.RecordUiAllocation(64);
|
||||
log.RecordPresentationCoordinate("SC0000.BIN", 0x2aaa, 0x21c);
|
||||
log.RecordObject(timeVarying: true);
|
||||
log.RecordRaster(800, 600, new Affine2D(1, 0, 0, 1, 0, 0),
|
||||
800, 600, dynamic: false, BlendKind.Alpha, ticks: 10);
|
||||
log.RecordFillLayer();
|
||||
log.RecordSkippedLayer();
|
||||
log.EndRecomposite();
|
||||
log.EndFrame();
|
||||
Assert.Equal(1, log.FrameCount);
|
||||
Assert.Equal(1, log.RecompositeCount);
|
||||
}
|
||||
|
||||
string[] lines = File.ReadAllLines(path);
|
||||
Assert.Equal(2, lines.Length);
|
||||
string[] header = lines[0].Split(',');
|
||||
string[] row = lines[1].Split(',');
|
||||
Assert.Equal(header.Length, row.Length);
|
||||
Assert.Equal("frame", header[0]);
|
||||
Assert.Equal("17", row[0]);
|
||||
Assert.Equal("1", row[Array.IndexOf(header, "recomposited")]);
|
||||
Assert.Equal("1", row[Array.IndexOf(header, "present_host_request")]);
|
||||
Assert.Equal("1", row[Array.IndexOf(header, "present_retained_mutation")]);
|
||||
Assert.Equal("1", row[Array.IndexOf(header, "present_discrete_cell")]);
|
||||
Assert.Equal("0", row[Array.IndexOf(header, "present_continuous_channel")]);
|
||||
Assert.Equal("4096", row[Array.IndexOf(header, "recompose_allocated_bytes")]);
|
||||
Assert.Equal("512", row[Array.IndexOf(header, "snapshot_allocated_bytes")]);
|
||||
Assert.Equal("2048", row[Array.IndexOf(header, "composite_allocated_bytes")]);
|
||||
Assert.Equal("128", row[Array.IndexOf(header, "source_prep_allocated_bytes")]);
|
||||
Assert.Equal("1024", row[Array.IndexOf(header, "set_data_allocated_bytes")]);
|
||||
Assert.Equal("64", row[Array.IndexOf(header, "ui_allocated_bytes")]);
|
||||
Assert.Equal("1", row[Array.IndexOf(header, "time_varying_objects")]);
|
||||
Assert.Equal("480000", row[Array.IndexOf(header, "candidate_pixels")]);
|
||||
Assert.Equal("SC0000.BIN", row[Array.IndexOf(header, "script")].Trim('"'));
|
||||
Assert.Equal("10897", row[Array.IndexOf(header, "offset")]);
|
||||
Assert.Equal("524", row[Array.IndexOf(header, "opcode")]);
|
||||
Assert.Equal("10922", row[Array.IndexOf(header, "present_offset")]);
|
||||
Assert.Equal("540", row[Array.IndexOf(header, "present_opcode")]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 0, 10, 10, 100)]
|
||||
[InlineData(-5, -5, 10, 10, 25)]
|
||||
[InlineData(95, 95, 10, 10, 25)]
|
||||
[InlineData(200, 200, 10, 10, 0)]
|
||||
public void CandidatePixelsClipsIntegerTranslations(int x, int y, int width, int height, long expected)
|
||||
{
|
||||
var transform = new Affine2D(1, 0, 0, 1, x, y);
|
||||
Assert.True(PerformanceFrameLog.IsIntegerTranslation(transform));
|
||||
Assert.Equal(expected,
|
||||
PerformanceFrameLog.EstimateCandidatePixels(transform, width, height, 100, 100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CandidatePixelsUsesAffineBoundingBoxAndDetectsNonIntegerPath()
|
||||
{
|
||||
var scaled = new Affine2D(2, 0, 0, 3, 1.5, 2.5);
|
||||
Assert.False(PerformanceFrameLog.IsIntegerTranslation(scaled));
|
||||
Assert.Equal(651,
|
||||
PerformanceFrameLog.EstimateCandidatePixels(scaled, 10, 10, 100, 100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RasterClassificationSeparatesTranslationScaleAndGeneralAffine()
|
||||
{
|
||||
Assert.True(PerformanceFrameLog.IsTranslation(new Affine2D(1, 0, 0, 1, 0.25, -0.5)));
|
||||
Assert.False(PerformanceFrameLog.IsIntegerTranslation(new Affine2D(1, 0, 0, 1, 0.25, -0.5)));
|
||||
Assert.True(PerformanceFrameLog.IsAxisAligned(new Affine2D(2, 0, 0, 3, 0, 0)));
|
||||
Assert.False(PerformanceFrameLog.IsAxisAligned(new Affine2D(1, 0.25, 0, 1, 0, 0)));
|
||||
}
|
||||
}
|
||||
@@ -115,10 +115,110 @@ public class SoftwareAffineRasterizerTests
|
||||
Assert.Equal(new byte[] { 43, 57, 109, 255 }, background);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BlitRgba_AxisAlignedScaleFastPath_MatchesGeneralAffineAcrossRandomizedCases()
|
||||
{
|
||||
var random = new Random(0x231);
|
||||
double[] scales = { -2.0, -1.25, -0.5, 0.25, 0.5, 0.75, 1.25, 2.0, 3.0 };
|
||||
double[] translations = { -5.5, -2.25, -0.5, 0.0, 0.5, 1.25, 4.5, 8.0 };
|
||||
BlendKind[] blends = { BlendKind.Opaque, BlendKind.Alpha, BlendKind.Additive };
|
||||
for (int iteration = 0; iteration < 250; iteration++)
|
||||
{
|
||||
int srcW = random.Next(2, 12), srcH = random.Next(2, 10);
|
||||
byte[] src = new byte[srcW * srcH * 4];
|
||||
random.NextBytes(src);
|
||||
int srcX = random.Next(0, srcW), srcY = random.Next(0, srcH);
|
||||
int width = random.Next(1, srcW - srcX + 1), height = random.Next(1, srcH - srcY + 1);
|
||||
int dstW = random.Next(3, 15), dstH = random.Next(3, 13);
|
||||
byte[] expected = new byte[dstW * dstH * 4];
|
||||
random.NextBytes(expected);
|
||||
byte[] actual = (byte[])expected.Clone();
|
||||
var transform = new Affine2D(
|
||||
scales[random.Next(scales.Length)], 0, 0, scales[random.Next(scales.Length)],
|
||||
translations[random.Next(translations.Length)], translations[random.Next(translations.Length)]);
|
||||
long tint = random.Next(0x1000000);
|
||||
float tintStrength = random.NextSingle();
|
||||
float opacity = random.NextSingle();
|
||||
bool multiplyTint = random.Next(2) != 0;
|
||||
BlendKind blend = blends[random.Next(blends.Length)];
|
||||
|
||||
ReferenceBlit(expected, dstW, dstH, src, srcW, srcX, srcY, width, height,
|
||||
transform, tint, tintStrength, opacity, multiplyTint, blend);
|
||||
SoftwareAffineRasterizer.BlitRgba(actual, dstW, dstH, src, srcW, srcH,
|
||||
srcX, srcY, width, height, transform,
|
||||
tint, tintStrength, opacity, multiplyTint, blend);
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FillRgba_AxisAlignedScaleFastPath_MatchesGeneralAffineAcrossRandomizedCases()
|
||||
{
|
||||
var random = new Random(0x22d);
|
||||
double[] scales = { -2.0, -1.0, -0.5, 0.25, 0.5, 0.75, 1.5, 2.0, 3.0 };
|
||||
double[] translations = { -5.5, -1.25, -0.5, 0.0, 0.5, 2.25, 7.0 };
|
||||
for (int iteration = 0; iteration < 250; iteration++)
|
||||
{
|
||||
int width = random.Next(1, 10), height = random.Next(1, 9);
|
||||
int dstW = random.Next(3, 15), dstH = random.Next(3, 13);
|
||||
byte[] expected = new byte[dstW * dstH * 4];
|
||||
random.NextBytes(expected);
|
||||
byte[] actual = (byte[])expected.Clone();
|
||||
var transform = new Affine2D(
|
||||
scales[random.Next(scales.Length)], 0, 0, scales[random.Next(scales.Length)],
|
||||
translations[random.Next(translations.Length)], translations[random.Next(translations.Length)]);
|
||||
long color = random.Next(0x1000000);
|
||||
float opacity = random.NextSingle();
|
||||
|
||||
ReferenceFill(expected, dstW, dstH, width, height, transform, color, opacity);
|
||||
SoftwareAffineRasterizer.FillRgba(actual, dstW, dstH, width, height,
|
||||
transform, color, opacity);
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BlitRgba_UnmodulatedSourceOverFastPaths_MatchGeneralAffineAcrossAlphaEdges()
|
||||
{
|
||||
var random = new Random(0x2e0);
|
||||
Affine2D[] transforms =
|
||||
{
|
||||
new(1, 0, 0, 1, -2, 1),
|
||||
new(1, 0, 0, 1, 3, -1),
|
||||
new(0.5, 0, 0, 1.5, -1.25, 0.5),
|
||||
new(-1.25, 0, 0, 0.75, 8.5, -0.5),
|
||||
};
|
||||
foreach (var transform in transforms)
|
||||
foreach (BlendKind blend in new[] { BlendKind.Opaque, BlendKind.Alpha })
|
||||
for (int iteration = 0; iteration < 50; iteration++)
|
||||
{
|
||||
int srcW = random.Next(3, 12), srcH = random.Next(3, 10);
|
||||
byte[] src = new byte[srcW * srcH * 4];
|
||||
random.NextBytes(src);
|
||||
for (int pixel = 0; pixel < srcW * srcH; pixel++)
|
||||
src[pixel * 4 + 3] = (byte)(pixel % 4 switch { 0 => 0, 1 => 1, 2 => 254, _ => 255 });
|
||||
int dstW = random.Next(5, 16), dstH = random.Next(5, 14);
|
||||
byte[] expected = new byte[dstW * dstH * 4];
|
||||
random.NextBytes(expected);
|
||||
byte[] actual = (byte[])expected.Clone();
|
||||
|
||||
ReferenceBlit(expected, dstW, dstH, src, srcW, 1, 1, srcW - 1, srcH - 1,
|
||||
transform, 0x6a4c2e, 0, 1, false, blend);
|
||||
SoftwareAffineRasterizer.BlitRgba(actual, dstW, dstH, src, srcW, srcH,
|
||||
1, 1, srcW - 1, srcH - 1, transform,
|
||||
0x6a4c2e, 0, 1, false, blend);
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
long tint, float tintStrength, float opacity, bool multiplyTint,
|
||||
BlendKind blend = BlendKind.Alpha)
|
||||
{
|
||||
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);
|
||||
@@ -136,7 +236,7 @@ public class SoftwareAffineRasterizerTests
|
||||
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);
|
||||
ReferenceBlend(dst, di, sr, sg, sb, sa, blend);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,8 +265,17 @@ public class SoftwareAffineRasterizerTests
|
||||
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)
|
||||
private static void ReferenceBlend(byte[] dst, int i, int r, int g, int b, int a,
|
||||
BlendKind blend = BlendKind.Alpha)
|
||||
{
|
||||
if (blend == BlendKind.Additive)
|
||||
{
|
||||
dst[i] = (byte)System.Math.Min(255, dst[i] + r * a / 255);
|
||||
dst[i + 1] = (byte)System.Math.Min(255, dst[i + 1] + g * a / 255);
|
||||
dst[i + 2] = (byte)System.Math.Min(255, dst[i + 2] + b * a / 255);
|
||||
dst[i + 3] = (byte)System.Math.Min(255, dst[i + 3] + a);
|
||||
return;
|
||||
}
|
||||
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);
|
||||
|
||||
95
engine/Age.Engine.Tests/Transform2DMathTests.cs
Normal file
95
engine/Age.Engine.Tests/Transform2DMathTests.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using Age.Engine.Model;
|
||||
using Xunit;
|
||||
|
||||
public class Transform2DMathTests
|
||||
{
|
||||
[Fact]
|
||||
public void ValueMatrixBuild_MatchesFormerArrayCompositionExactly()
|
||||
{
|
||||
var random = new Random(0x240);
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
var transform = new TransformState(
|
||||
Next(random, -3, 3), Next(random, -3, 3), Next(random, -3, 3),
|
||||
Next(random, -500, 500), Next(random, -500, 500), Next(random, -500, 500),
|
||||
Next(random, -1000, 1000), Next(random, -1000, 1000), Next(random, -1000, 1000),
|
||||
Next(random, -1, 1), Next(random, -1, 1), Next(random, -1, 1),
|
||||
Next(random, -360, 360));
|
||||
var cycle = new RotationCycleState(
|
||||
random.Next(2) != 0, random.Next(1, 20000),
|
||||
Next(random, -1, 1), Next(random, -1, 1), Next(random, -1, 1),
|
||||
Next(random, -360, 360));
|
||||
|
||||
var expected = ArrayBuild(transform, cycle);
|
||||
var actual = Transform2DMath.Build(transform, cycle);
|
||||
|
||||
Assert.Equal(BitConverter.DoubleToInt64Bits(expected.XX), BitConverter.DoubleToInt64Bits(actual.XX));
|
||||
Assert.Equal(BitConverter.DoubleToInt64Bits(expected.XY), BitConverter.DoubleToInt64Bits(actual.XY));
|
||||
Assert.Equal(BitConverter.DoubleToInt64Bits(expected.YX), BitConverter.DoubleToInt64Bits(actual.YX));
|
||||
Assert.Equal(BitConverter.DoubleToInt64Bits(expected.YY), BitConverter.DoubleToInt64Bits(actual.YY));
|
||||
Assert.Equal(BitConverter.DoubleToInt64Bits(expected.TX), BitConverter.DoubleToInt64Bits(actual.TX));
|
||||
Assert.Equal(BitConverter.DoubleToInt64Bits(expected.TY), BitConverter.DoubleToInt64Bits(actual.TY));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueMatrixBuild_DoesNotAllocatePerObject()
|
||||
{
|
||||
var transform = new TransformState(1.25, 0.75, 1.1, 42, -17, 5, 400, 300, 9, 0, 0, 1, 37);
|
||||
var cycle = new RotationCycleState(true, 9000, 0, 0, -1, 123);
|
||||
_ = Transform2DMath.Build(transform, cycle); // JIT/warm-up outside the measured interval.
|
||||
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
Affine2D result = default;
|
||||
for (int i = 0; i < 10_000; i++) result = Transform2DMath.Build(transform, cycle);
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
GC.KeepAlive(result);
|
||||
|
||||
Assert.Equal(0, allocated);
|
||||
}
|
||||
|
||||
private static double Next(Random random, double minimum, double maximum)
|
||||
=> minimum + random.NextDouble() * (maximum - minimum);
|
||||
|
||||
// The former heap-array implementation, retained only as a differential oracle.
|
||||
private static Affine2D ArrayBuild(TransformState t, RotationCycleState cycle)
|
||||
{
|
||||
double[] m = Identity();
|
||||
m = Mul(m, Translation(-t.AnchorX, -t.AnchorY, -t.AnchorZ));
|
||||
m = Mul(m, Scale(t.ScaleX, t.ScaleY, t.ScaleZ));
|
||||
m = Mul(m, AxisAngle(t.RotationAxisX, t.RotationAxisY, t.RotationAxisZ, t.RotationAngleDegrees));
|
||||
m = Mul(m, Translation(t.TranslateX, t.TranslateY, t.TranslateZ));
|
||||
if (cycle.Enabled) m = Mul(m, AxisAngle(cycle.AxisX, cycle.AxisY, cycle.AxisZ, cycle.AngleDegrees));
|
||||
m = Mul(m, Translation(t.AnchorX, t.AnchorY, t.AnchorZ));
|
||||
return new(m[0], m[1], m[4], m[5], m[12], m[13]);
|
||||
}
|
||||
|
||||
private static double[] Identity() => new double[] { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };
|
||||
private static double[] Scale(double x, double y, double z)
|
||||
=> new double[] { x,0,0,0, 0,y,0,0, 0,0,z,0, 0,0,0,1 };
|
||||
private static double[] Translation(double x, double y, double z)
|
||||
=> new double[] { 1,0,0,0, 0,1,0,0, 0,0,1,0, x,y,z,1 };
|
||||
|
||||
private static double[] AxisAngle(double x, double y, double z, double degrees)
|
||||
{
|
||||
double len = Math.Sqrt(x*x + y*y + z*z);
|
||||
if (len < 1e-12 || Math.Abs(degrees) < 1e-12) return Identity();
|
||||
x /= len; y /= len; z /= len;
|
||||
double r = degrees * Math.PI / 180.0, c = Math.Cos(r), s = Math.Sin(r), q = 1-c;
|
||||
return new double[] {
|
||||
x*x*q+c, x*y*q+z*s, x*z*q-y*s, 0,
|
||||
x*y*q-z*s, y*y*q+c, y*z*q+x*s, 0,
|
||||
x*z*q+y*s, y*z*q-x*s, z*z*q+c, 0,
|
||||
0,0,0,1
|
||||
};
|
||||
}
|
||||
|
||||
private static double[] Mul(double[] a, double[] b)
|
||||
{
|
||||
var output = new double[16];
|
||||
for (int row = 0; row < 4; row++)
|
||||
for (int column = 0; column < 4; column++)
|
||||
for (int k = 0; k < 4; k++) output[row * 4 + column] += a[row * 4 + k] * b[k * 4 + column];
|
||||
return output;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,15 @@ public readonly record struct RotationCycleState(bool Enabled, long PeriodMs,
|
||||
double AxisX, double AxisY, double AxisZ,
|
||||
double AngleDegrees = 0);
|
||||
|
||||
[System.Flags]
|
||||
public enum GfxPresentationReason
|
||||
{
|
||||
None = 0,
|
||||
RetainedMutation = 1,
|
||||
ContinuousChannel = 2,
|
||||
DiscreteSourceCell = 4,
|
||||
}
|
||||
|
||||
/// <summary>Sampled op-0x223 type-0 surface transition. Range A is already present in normal z-order;
|
||||
/// the compositor draws range B over it with <paramref name="Progress"/> to form the native crossfade.</summary>
|
||||
public readonly record struct SurfaceTransitionState(long CommandKey, int TargetSlot,
|
||||
@@ -62,7 +71,8 @@ public readonly record struct RenderObject(long Handle, long SurfaceResId, long
|
||||
bool MultiplyTint,
|
||||
SurfaceTransitionState? SurfaceTransition = null,
|
||||
ColorTransitionState? ColorTransition = null,
|
||||
Affine2D? RangeTransform = null);
|
||||
Affine2D? RangeTransform = null,
|
||||
bool TimeVarying = false);
|
||||
|
||||
/// <summary>Host-agnostic model of the AGE native gfx command-buffer (reversed in
|
||||
/// docs/engine-re.md, gfx op-contract table). One registry maps an object handle to a GfxObject — the
|
||||
@@ -142,6 +152,9 @@ public sealed class GfxState
|
||||
// Populated lazily by the geometry SET ops and draw-texture. Op 0x215 queries this same native map and
|
||||
// returns the object's live source slot (obj+4), or -1 when the handle has not been drawn/bound yet.
|
||||
private readonly Dictionary<long, GfxObject> _objects = new();
|
||||
// Native composition is handle-ascending z order. Mutations maintain this small index so snapshots do
|
||||
// not rebuild/sort a dictionary-sized LINQ buffer, while hot handle lookup remains O(1).
|
||||
private readonly List<long> _orderedObjectHandles = new();
|
||||
private readonly NumericGlyphStyle[] _numericGlyphStyles = new NumericGlyphStyle[11];
|
||||
|
||||
// Ops 0x229-0x22e address one embedded gfx-object record outside the ordinary object map. Its sampled
|
||||
@@ -164,6 +177,12 @@ public sealed class GfxState
|
||||
public long AnimationServiceFlags { get; private set; }
|
||||
public uint PreviousFrameTimeMilliseconds { get; private set; }
|
||||
public uint CurrentFrameTimeMilliseconds { get; private set; }
|
||||
private long _previousFrameTimeMs;
|
||||
private long _currentFrameTimeMs;
|
||||
private long _mutationGeneration;
|
||||
private long _publishedMutationGeneration;
|
||||
|
||||
private void MarkRetainedMutation() => _mutationGeneration++;
|
||||
|
||||
/// <summary>Live geometry objects and the surface slot they draw from — for the CLI gfx oracle.</summary>
|
||||
public IEnumerable<(long Handle, int Slot)> Objects
|
||||
@@ -178,8 +197,14 @@ public sealed class GfxState
|
||||
// (Monitor) so the callers that already hold it are fine.
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_objects.TryGetValue(handle, out var o)) { o = new GfxObject(); _objects[handle] = o; }
|
||||
if (!_objects.TryGetValue(handle, out var o))
|
||||
{
|
||||
o = new GfxObject();
|
||||
_objects[handle] = o;
|
||||
InsertOrderedHandle(handle);
|
||||
}
|
||||
CurrentObject = handle;
|
||||
MarkRetainedMutation();
|
||||
return o;
|
||||
}
|
||||
}
|
||||
@@ -189,6 +214,31 @@ public sealed class GfxState
|
||||
lock (_lock) DefaultObjectSlot = slot;
|
||||
}
|
||||
|
||||
public void SetObjectAnchor(long handle, (long X, long Y, long Z) anchor)
|
||||
{
|
||||
lock (_lock) GetOrCreate(handle).V18 = anchor;
|
||||
}
|
||||
|
||||
public void SetObjectPosition(long handle, (long X, long Y, long Z) position)
|
||||
{
|
||||
lock (_lock) GetOrCreate(handle).V24 = position;
|
||||
}
|
||||
|
||||
public void SetObjectField64(long handle, long value)
|
||||
{
|
||||
lock (_lock) GetOrCreate(handle).Field64 = value;
|
||||
}
|
||||
|
||||
public void SetObjectFields68And6c(long handle, long value68, long value6c)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var o = GetOrCreate(handle);
|
||||
o.Field68 = value68;
|
||||
o.Field6c = value6c;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x229: reset the embedded range transform, select [first, first+count), and set its
|
||||
/// anchor/pivot. This does not create or mutate an ordinary retained object.</summary>
|
||||
public void SetRangeTransform(long first, long count, (long X, long Y, long Z) anchor)
|
||||
@@ -198,6 +248,7 @@ public sealed class GfxState
|
||||
_rangeTransformFirst = first;
|
||||
_rangeTransformCount = System.Math.Max(0, count);
|
||||
_rangeTransform = new GfxObject { V18 = anchor };
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,13 +256,20 @@ public sealed class GfxState
|
||||
public void SetRangeScaleCurrent((long X, long Y, long Z) percent)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_rangeTransform.ScaleCurrent = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x22c: immediately replace the embedded range transform's current translation.</summary>
|
||||
public void SetRangeTranslationCurrent((long X, long Y, long Z) translation)
|
||||
{
|
||||
lock (_lock) _rangeTransform.TranslationCurrent = translation;
|
||||
lock (_lock)
|
||||
{
|
||||
_rangeTransform.TranslationCurrent = translation;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x22d: arm the range transform's delayed one-shot scale target.</summary>
|
||||
@@ -224,6 +282,7 @@ public sealed class GfxState
|
||||
_rangeTransform.ScaleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
|
||||
_rangeTransform.ScaleEnabled = durationMs > 0;
|
||||
_rangeTransform.OneShotStartMs = -1;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +292,7 @@ public sealed class GfxState
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_objects.TryGetValue(sourceHandle, out var s)) return false;
|
||||
bool destinationIsNew = !_objects.ContainsKey(destinationHandle);
|
||||
_objects[destinationHandle] = new GfxObject
|
||||
{
|
||||
V18 = s.V18, V24 = s.V24, V16c = s.V16c,
|
||||
@@ -258,7 +318,9 @@ public sealed class GfxState
|
||||
RotationPeriodMs = s.RotationPeriodMs, RotationAxis = s.RotationAxis,
|
||||
RotationEnabled = s.RotationEnabled, RotationStartMs = s.RotationStartMs,
|
||||
};
|
||||
if (destinationIsNew) InsertOrderedHandle(destinationHandle);
|
||||
CurrentObject = destinationHandle;
|
||||
MarkRetainedMutation();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -280,7 +342,10 @@ public sealed class GfxState
|
||||
{
|
||||
lock (_lock)
|
||||
if (_objects.TryGetValue(handle, out var obj) && obj.SourceSlot == fromSlot)
|
||||
{
|
||||
obj.SourceSlot = toSlot;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
public long QueryField(long idx) => _fieldTable.TryGetValue(idx, out var v) ? v : 0;
|
||||
|
||||
@@ -288,7 +353,12 @@ public sealed class GfxState
|
||||
{
|
||||
lock (_lock) // re-entrant: EraseRange already holds _lock
|
||||
{
|
||||
_objects.Remove(handle);
|
||||
if (_objects.Remove(handle))
|
||||
{
|
||||
int index = _orderedObjectHandles.BinarySearch(handle);
|
||||
if (index >= 0) _orderedObjectHandles.RemoveAt(index);
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +382,9 @@ public sealed class GfxState
|
||||
lock (_lock)
|
||||
{
|
||||
_objects.Clear();
|
||||
_orderedObjectHandles.Clear();
|
||||
CurrentObject = 0;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,6 +396,7 @@ public sealed class GfxState
|
||||
lock (_lock)
|
||||
{
|
||||
_objects.Clear();
|
||||
_orderedObjectHandles.Clear();
|
||||
_fieldTable.Clear();
|
||||
_surfaces.Clear();
|
||||
_createdSurfaces.Clear();
|
||||
@@ -339,6 +412,9 @@ public sealed class GfxState
|
||||
AnimationServiceFlags = 0;
|
||||
PreviousFrameTimeMilliseconds = 0;
|
||||
CurrentFrameTimeMilliseconds = 0;
|
||||
_previousFrameTimeMs = 0;
|
||||
_currentFrameTimeMs = 0;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +436,7 @@ public sealed class GfxState
|
||||
_surfaces[slot] = (resId, colorKey);
|
||||
_createdSurfaces.Remove(slot);
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,6 +476,7 @@ public sealed class GfxState
|
||||
}
|
||||
if (CurrentRenderTargetSlot >= firstSlot && CurrentRenderTargetSlot < end)
|
||||
CurrentRenderTargetSlot = -1;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,6 +576,7 @@ public sealed class GfxState
|
||||
_surfaces[slot] = (0, -1); // create-texture: real mutable pixels, no asset id or color key
|
||||
_createdSurfaces.Add(slot);
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,6 +588,7 @@ public sealed class GfxState
|
||||
_createdSurfaces.Remove(slot);
|
||||
_movieStopTimesMs.Remove(slot);
|
||||
_surfaceTransitions.Remove(slot);
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,6 +606,7 @@ public sealed class GfxState
|
||||
RangeBStart = rangeBStart, RangeBCount = System.Math.Max(0, rangeBCount),
|
||||
DelayMs = System.Math.Max(0, delayMs), DurationMs = System.Math.Max(0, durationMs),
|
||||
};
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,6 +765,7 @@ public sealed class GfxState
|
||||
int completed = 0;
|
||||
foreach (var t in _surfaceTransitions.Values)
|
||||
if (!t.Forced && TransitionProgress(t, nowMs) < 1.0) { t.Forced = true; completed++; }
|
||||
if (completed > 0) MarkRetainedMutation();
|
||||
return completed;
|
||||
}
|
||||
}
|
||||
@@ -883,7 +965,12 @@ public sealed class GfxState
|
||||
/// <summary>Op 0x238: set its separate global animation-service duration and reset marker.</summary>
|
||||
public void SetAnimClock(long durationTicks)
|
||||
{
|
||||
lock (_lock) { AnimClockDurationTicks = durationTicks; AnimClockGeneration++; }
|
||||
lock (_lock)
|
||||
{
|
||||
AnimClockDurationTicks = durationTicks;
|
||||
AnimClockGeneration++;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAnimationServiceFlags(long flags)
|
||||
@@ -895,6 +982,8 @@ public sealed class GfxState
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_previousFrameTimeMs = _currentFrameTimeMs;
|
||||
_currentFrameTimeMs = nowMilliseconds;
|
||||
PreviousFrameTimeMilliseconds = CurrentFrameTimeMilliseconds;
|
||||
CurrentFrameTimeMilliseconds = unchecked((uint)nowMilliseconds);
|
||||
}
|
||||
@@ -910,9 +999,65 @@ public sealed class GfxState
|
||||
ForceCompleteOneShotChannels();
|
||||
AnimClockDurationTicks = 0;
|
||||
AnimClockGeneration++;
|
||||
MarkRetainedMutation();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sample the shared native frame clock and report why the retained scene needs publishing.
|
||||
/// Continuous channels remain frame-driven; op-0x231 spritesheets become dirty only when the shared
|
||||
/// previous/current samples select different cells. Retained VM writes are published exactly once.</summary>
|
||||
public GfxPresentationReason ConsumePresentationReasons(long nowMs)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_previousFrameTimeMs = _currentFrameTimeMs;
|
||||
_currentFrameTimeMs = nowMs;
|
||||
PreviousFrameTimeMilliseconds = unchecked((uint)_previousFrameTimeMs);
|
||||
CurrentFrameTimeMilliseconds = unchecked((uint)_currentFrameTimeMs);
|
||||
|
||||
GfxPresentationReason reasons = GfxPresentationReason.None;
|
||||
if (_publishedMutationGeneration != _mutationGeneration)
|
||||
{
|
||||
_publishedMutationGeneration = _mutationGeneration;
|
||||
reasons |= GfxPresentationReason.RetainedMutation;
|
||||
}
|
||||
|
||||
if (_surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0) ||
|
||||
_rangeTransform.ScaleEnabled || _rangeTransform.RotationChannelEnabled ||
|
||||
_rangeTransform.TranslationEnabled ||
|
||||
_objects.Values.Any(o => o.Visible &&
|
||||
(o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled ||
|
||||
o.TranslationEnabled ||
|
||||
(o.ColorAnim && o.ColorPeriod > 0) ||
|
||||
(o.RotationEnabled && o.RotationPeriodMs > 0))))
|
||||
reasons |= GfxPresentationReason.ContinuousChannel;
|
||||
|
||||
foreach (var o in _objects.Values)
|
||||
{
|
||||
if (!o.Visible || !o.SrcAnim || o.SrcPeriod <= 0 || o.SrcFrameCount < 1) continue;
|
||||
if (o.SrcStart < 0)
|
||||
{
|
||||
// Prototype clones made before first publication all enter here in the same shared sample,
|
||||
// reproducing FIELD's native phase lock.
|
||||
o.SrcStart = nowMs;
|
||||
continue;
|
||||
}
|
||||
if (SourceCellAt(o, _previousFrameTimeMs) != SourceCellAt(o, _currentFrameTimeMs))
|
||||
{
|
||||
reasons |= GfxPresentationReason.DiscreteSourceCell;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return reasons;
|
||||
}
|
||||
}
|
||||
|
||||
private static long SourceCellAt(GfxObject o, long nowMs)
|
||||
{
|
||||
long elapsed = System.Math.Max(0, nowMs - o.SrcStart);
|
||||
return elapsed / o.SrcPeriod % o.SrcFrameCount;
|
||||
}
|
||||
|
||||
/// <summary>Back-compat: snapshot with no animation clock (nowMs = 0) — deterministic, for headless
|
||||
/// callers and existing tests.</summary>
|
||||
public IReadOnlyList<RenderObject> SnapshotVisibleObjects() => SnapshotVisibleObjects(0);
|
||||
@@ -925,10 +1070,22 @@ public sealed class GfxState
|
||||
/// alpha/tint. Channel Start fields seed to nowMs on first sight.</summary>
|
||||
public IReadOnlyList<RenderObject> SnapshotVisibleObjects(long nowMs)
|
||||
{
|
||||
var list = new List<RenderObject>();
|
||||
SnapshotVisibleObjects(nowMs, list);
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>Fill a caller-owned snapshot buffer. The Godot compositor reuses one list so its backing
|
||||
/// array survives across frames; callers that need an independently retained snapshot should use the
|
||||
/// returning overload.</summary>
|
||||
public void SnapshotVisibleObjects(long nowMs, List<RenderObject> list)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
lock (_lock)
|
||||
{
|
||||
var list = new List<RenderObject>();
|
||||
list.Clear();
|
||||
Affine2D? rangeAffine = null;
|
||||
bool rangeTimeVarying = false;
|
||||
if (_rangeTransformCount > 0)
|
||||
{
|
||||
var r = _rangeTransform;
|
||||
@@ -944,15 +1101,16 @@ public sealed class GfxState
|
||||
ref r.TranslationEnabled, nowMs);
|
||||
if (!r.ScaleEnabled && !r.RotationChannelEnabled && !r.TranslationEnabled)
|
||||
r.OneShotStartMs = -1;
|
||||
rangeTimeVarying = r.ScaleEnabled || r.RotationChannelEnabled || r.TranslationEnabled;
|
||||
rangeAffine = Transform2DMath.Build(new TransformState(
|
||||
rangeScale.X, rangeScale.Y, rangeScale.Z,
|
||||
rangeTranslation.X, rangeTranslation.Y, rangeTranslation.Z,
|
||||
r.V18.X, r.V18.Y, r.V18.Z,
|
||||
rangeRotation.X, rangeRotation.Y, rangeRotation.Z, rangeRotation.Angle));
|
||||
}
|
||||
foreach (var kv in _objects.OrderBy(k => k.Key))
|
||||
foreach (long handle in _orderedObjectHandles)
|
||||
{
|
||||
var o = kv.Value;
|
||||
var o = _objects[handle];
|
||||
if (!o.Visible) continue;
|
||||
bool hadOneShot = o.OneShotColorEnabled || o.ScaleEnabled ||
|
||||
o.RotationChannelEnabled || o.TranslationEnabled;
|
||||
@@ -1064,9 +1222,17 @@ public sealed class GfxState
|
||||
SurfaceTransitionState? transition = _surfaceTransitions.TryGetValue(o.SourceSlot, out var st)
|
||||
? SampleTransition(st, nowMs) : null;
|
||||
Affine2D? objectRangeTransform = rangeAffine is { } ra &&
|
||||
kv.Key >= _rangeTransformFirst && kv.Key - _rangeTransformFirst < _rangeTransformCount
|
||||
handle >= _rangeTransformFirst && handle - _rangeTransformFirst < _rangeTransformCount
|
||||
? ra : null;
|
||||
list.Add(new RenderObject(kv.Key, resId, ck, srcX, srcY, w, h,
|
||||
bool timeVarying =
|
||||
o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled ||
|
||||
o.TranslationEnabled ||
|
||||
(o.SrcAnim && o.SrcPeriod > 0) ||
|
||||
(o.ColorAnim && o.ColorPeriod > 0) ||
|
||||
(o.RotationEnabled && o.RotationPeriodMs > 0) ||
|
||||
transition is { Progress: < 1.0 } ||
|
||||
(objectRangeTransform != null && rangeTimeVarying);
|
||||
list.Add(new RenderObject(handle, resId, ck, srcX, srcY, w, h,
|
||||
(int)o.V24.X, (int)o.V24.Y,
|
||||
new TransformState(scale.X, scale.Y, scale.Z,
|
||||
translation.X, translation.Y, translation.Z,
|
||||
@@ -1076,12 +1242,17 @@ public sealed class GfxState
|
||||
o.RotationAxis.X, o.RotationAxis.Y,
|
||||
o.RotationAxis.Z, cycleAngle),
|
||||
alpha, tint, strength, blend, multiplyTint, transition,
|
||||
colorTransition, objectRangeTransform));
|
||||
colorTransition, objectRangeTransform, timeVarying));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
private void InsertOrderedHandle(long handle)
|
||||
{
|
||||
int index = _orderedObjectHandles.BinarySearch(handle);
|
||||
if (index < 0) _orderedObjectHandles.Insert(~index, handle);
|
||||
}
|
||||
|
||||
private static (long Packed, ColorTransitionState State) SampleOneShotColor(GfxObject o, long nowMs)
|
||||
{
|
||||
long current = o.Color & 0xffffffff;
|
||||
|
||||
@@ -13,14 +13,23 @@ public static class SoftwareAffineRasterizer
|
||||
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);
|
||||
bool unmodulatedSourceOver = ia == 255 && istr == 0 && !multiplyTint && blend != BlendKind.Additive;
|
||||
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, blend);
|
||||
tx, ty, tr, tg, tb, istr, ia, multiplyTint, blend, unmodulatedSourceOver);
|
||||
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);
|
||||
if (x1 <= x0 || y1 <= y0) return;
|
||||
if (localToDest.XY == 0 && localToDest.YX == 0 && x1 - x0 <= 4096)
|
||||
{
|
||||
BlitAxisAligned(dst, dstW, src, srcW, srcX, srcY, width, height, inv,
|
||||
x0, y0, x1, y1, tr, tg, tb, istr, ia, multiplyTint, blend,
|
||||
unmodulatedSourceOver);
|
||||
return;
|
||||
}
|
||||
for (int y=y0; y<y1; y++) for (int x=x0; x<x1; x++)
|
||||
{
|
||||
var p = inv.Apply(x + 0.5, y + 0.5);
|
||||
@@ -48,6 +57,12 @@ public static class SoftwareAffineRasterizer
|
||||
}
|
||||
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);
|
||||
if (x1 <= x0 || y1 <= y0) return;
|
||||
if (localToDest.XY == 0 && localToDest.YX == 0 && x1 - x0 <= 4096)
|
||||
{
|
||||
FillAxisAligned(dst, dstW, width, height, inv, x0, y0, x1, y1, r, g, b, a);
|
||||
return;
|
||||
}
|
||||
for(int y=y0;y<y1;y++)for(int x=x0;x<x1;x++){
|
||||
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);
|
||||
@@ -69,11 +84,16 @@ public static class SoftwareAffineRasterizer
|
||||
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,
|
||||
BlendKind blend)
|
||||
BlendKind blend, bool unmodulatedSourceOver)
|
||||
{
|
||||
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);
|
||||
if (unmodulatedSourceOver)
|
||||
{
|
||||
BlitTranslatedUnmodulated(dst, dstW, src, srcW, srcX, srcY, tx, ty, x0, y0, x1, y1);
|
||||
return;
|
||||
}
|
||||
for (int y = y0; y < y1; y++)
|
||||
{
|
||||
int v = y - ty;
|
||||
@@ -91,6 +111,20 @@ public static class SoftwareAffineRasterizer
|
||||
}
|
||||
}
|
||||
|
||||
private static void BlitTranslatedUnmodulated(byte[] dst, int dstW, byte[] src, int srcW,
|
||||
int srcX, int srcY, int tx, int ty,
|
||||
int x0, int y0, int x1, int y1)
|
||||
{
|
||||
for (int y = y0; y < y1; y++)
|
||||
{
|
||||
int sourceRow = (srcY + y - ty) * srcW + srcX - tx;
|
||||
int destinationRow = y * dstW;
|
||||
for (int x = x0; x < x1; x++)
|
||||
BlendUnmodulatedSourceOver(dst, (destinationRow + x) * 4,
|
||||
src, (sourceRow + x) * 4);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -102,6 +136,102 @@ public static class SoftwareAffineRasterizer
|
||||
Blend(dst, (y * dstW + x) * 4, r, g, b, a);
|
||||
}
|
||||
|
||||
// Axis-aligned scale is FIELD's dominant non-integer path. Its inverse source column is independent
|
||||
// of destination Y, and its source row is independent of destination X. Cache the former once per
|
||||
// layer and compute the latter once per scanline while preserving the general path's exact center-
|
||||
// sample/floor arithmetic. Pooled lookup storage avoids both per-layer garbage and repeated-stackalloc
|
||||
// growth when the JIT inlines this hot path; the 4096-column caller gate keeps the rented bucket small.
|
||||
private static void BlitAxisAligned(byte[] dst, int dstW, byte[] src, int srcW,
|
||||
int srcX, int srcY, int width, int height, Affine2D inv,
|
||||
int x0, int y0, int x1, int y1,
|
||||
int tr, int tg, int tb, int istr, int ia,
|
||||
bool multiplyTint, BlendKind blend, bool unmodulatedSourceOver)
|
||||
{
|
||||
int count = x1 - x0;
|
||||
int[] sourceColumns = System.Buffers.ArrayPool<int>.Shared.Rent(count);
|
||||
try
|
||||
{
|
||||
for (int x = x0; x < x1; x++)
|
||||
{
|
||||
int u = (int)System.Math.Floor(inv.Apply(x + 0.5, y0 + 0.5).X);
|
||||
sourceColumns[x - x0] = (uint)u < (uint)width ? u : -1;
|
||||
}
|
||||
for (int y = y0; y < y1; y++)
|
||||
{
|
||||
int v = (int)System.Math.Floor(inv.Apply(x0 + 0.5, y + 0.5).Y);
|
||||
if ((uint)v >= (uint)height) continue;
|
||||
int sourceRow = (srcY + v) * srcW;
|
||||
int destinationRow = y * dstW;
|
||||
for (int x = x0; x < x1; x++)
|
||||
{
|
||||
int u = sourceColumns[x - x0];
|
||||
if (u < 0) continue;
|
||||
int si = (sourceRow + srcX + u) * 4;
|
||||
int di = (destinationRow + x) * 4;
|
||||
if (unmodulatedSourceOver)
|
||||
{
|
||||
BlendUnmodulatedSourceOver(dst, di, src, si);
|
||||
continue;
|
||||
}
|
||||
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, blend);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally { System.Buffers.ArrayPool<int>.Shared.Return(sourceColumns); }
|
||||
}
|
||||
|
||||
// Most FIELD layers have full object opacity and no tint. Preserve color-key transparency and partially
|
||||
// transparent edge texels, but make the overwhelmingly common alpha-255 case a four-byte copy instead of
|
||||
// performing tint and source-over multiply/divide work whose result is exactly the source texel.
|
||||
private static void BlendUnmodulatedSourceOver(byte[] dst, int di, byte[] src, int si)
|
||||
{
|
||||
int a = src[si + 3];
|
||||
if (a == 0) return;
|
||||
if (a == 255)
|
||||
{
|
||||
dst[di] = src[si];
|
||||
dst[di + 1] = src[si + 1];
|
||||
dst[di + 2] = src[si + 2];
|
||||
dst[di + 3] = 255;
|
||||
return;
|
||||
}
|
||||
int inverse = 255 - a;
|
||||
dst[di] = (byte)((src[si] * a + dst[di] * inverse) / 255);
|
||||
dst[di + 1] = (byte)((src[si + 1] * a + dst[di + 1] * inverse) / 255);
|
||||
dst[di + 2] = (byte)((src[si + 2] * a + dst[di + 2] * inverse) / 255);
|
||||
dst[di + 3] = (byte)System.Math.Min(255, dst[di + 3] + a);
|
||||
}
|
||||
|
||||
private static void FillAxisAligned(byte[] dst, int dstW, int width, int height, Affine2D inv,
|
||||
int x0, int y0, int x1, int y1,
|
||||
int r, int g, int b, int a)
|
||||
{
|
||||
int count = x1 - x0;
|
||||
bool[] includedColumns = System.Buffers.ArrayPool<bool>.Shared.Rent(count);
|
||||
try
|
||||
{
|
||||
for (int x = x0; x < x1; x++)
|
||||
{
|
||||
double u = inv.Apply(x + 0.5, y0 + 0.5).X;
|
||||
includedColumns[x - x0] = u >= 0 && u < width;
|
||||
}
|
||||
for (int y = y0; y < y1; y++)
|
||||
{
|
||||
double v = inv.Apply(x0 + 0.5, y + 0.5).Y;
|
||||
if (v < 0 || v >= height) continue;
|
||||
int destinationRow = y * dstW;
|
||||
for (int x = x0; x < x1; x++)
|
||||
if (includedColumns[x - x0]) Blend(dst, (destinationRow + x) * 4, r, g, b, a);
|
||||
}
|
||||
}
|
||||
finally { System.Buffers.ArrayPool<bool>.Shared.Return(includedColumns); }
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -40,46 +40,74 @@ public static class Transform2DMath
|
||||
{
|
||||
public static Affine2D Build(TransformState t, RotationCycleState cycle = default)
|
||||
{
|
||||
double[] m = Identity();
|
||||
Matrix3D m = Identity();
|
||||
m = Mul(m, Translation(-t.AnchorX, -t.AnchorY, -t.AnchorZ));
|
||||
m = Mul(m, Scale(t.ScaleX, t.ScaleY, t.ScaleZ));
|
||||
m = Mul(m, AxisAngle(t.RotationAxisX, t.RotationAxisY, t.RotationAxisZ, t.RotationAngleDegrees));
|
||||
m = Mul(m, Translation(t.TranslateX, t.TranslateY, t.TranslateZ));
|
||||
if (cycle.Enabled) m = Mul(m, AxisAngle(cycle.AxisX, cycle.AxisY, cycle.AxisZ, cycle.AngleDegrees));
|
||||
m = Mul(m, Translation(t.AnchorX, t.AnchorY, t.AnchorZ));
|
||||
return new(m[0], m[1], m[4], m[5], m[12], m[13]);
|
||||
return new(m.M11, m.M12, m.M21, m.M22, m.TX, m.TY);
|
||||
}
|
||||
|
||||
public static (double X, double Y) Apply(double x, double y, TransformState transform,
|
||||
RotationCycleState cycle = default)
|
||||
=> Build(transform, cycle).Apply(x, y);
|
||||
|
||||
private static double[] Identity() => new double[] { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };
|
||||
private static double[] Scale(double x, double y, double z)
|
||||
=> new double[] { x,0,0,0, 0,y,0,0, 0,0,z,0, 0,0,0,1 };
|
||||
private static double[] Translation(double x, double y, double z)
|
||||
=> new double[] { 1,0,0,0, 0,1,0,0, 0,0,1,0, x,y,z,1 };
|
||||
// AGE composes affine 4x4 row-vector matrices, whose last column is always (0,0,0,1). Carry only
|
||||
// the 3x3 linear part and translation row as a value type: the old double[16] implementation allocated
|
||||
// about eleven arrays per rendered object, or roughly 2.2 MB on every DEBUGMAP composition.
|
||||
private readonly record struct Matrix3D(
|
||||
double M11, double M12, double M13,
|
||||
double M21, double M22, double M23,
|
||||
double M31, double M32, double M33,
|
||||
double TX, double TY, double TZ);
|
||||
|
||||
private static double[] AxisAngle(double x, double y, double z, double degrees)
|
||||
private static Matrix3D Identity() => new(
|
||||
1,0,0, 0,1,0, 0,0,1, 0,0,0);
|
||||
|
||||
private static Matrix3D Scale(double x, double y, double z) => new(
|
||||
x,0,0, 0,y,0, 0,0,z, 0,0,0);
|
||||
|
||||
private static Matrix3D Translation(double x, double y, double z) => new(
|
||||
1,0,0, 0,1,0, 0,0,1, x,y,z);
|
||||
|
||||
private static Matrix3D AxisAngle(double x, double y, double z, double degrees)
|
||||
{
|
||||
double len = System.Math.Sqrt(x*x + y*y + z*z);
|
||||
if (len < 1e-12 || System.Math.Abs(degrees) < 1e-12) return Identity();
|
||||
x /= len; y /= len; z /= len;
|
||||
double r = degrees * System.Math.PI / 180.0, c = System.Math.Cos(r), s = System.Math.Sin(r), q = 1-c;
|
||||
return new double[] {
|
||||
x*x*q+c, x*y*q+z*s, x*z*q-y*s, 0,
|
||||
x*y*q-z*s, y*y*q+c, y*z*q+x*s, 0,
|
||||
x*z*q+y*s, y*z*q-x*s, z*z*q+c, 0,
|
||||
0,0,0,1
|
||||
};
|
||||
return new(
|
||||
x*x*q+c, x*y*q+z*s, x*z*q-y*s,
|
||||
x*y*q-z*s, y*y*q+c, y*z*q+x*s,
|
||||
x*z*q+y*s, y*z*q-x*s, z*z*q+c,
|
||||
0,0,0);
|
||||
}
|
||||
|
||||
private static double[] Mul(double[] a, double[] b)
|
||||
private static Matrix3D Mul(Matrix3D a, Matrix3D b) => new(
|
||||
Sum4(a.M11*b.M11, a.M12*b.M21, a.M13*b.M31, 0),
|
||||
Sum4(a.M11*b.M12, a.M12*b.M22, a.M13*b.M32, 0),
|
||||
Sum4(a.M11*b.M13, a.M12*b.M23, a.M13*b.M33, 0),
|
||||
Sum4(a.M21*b.M11, a.M22*b.M21, a.M23*b.M31, 0),
|
||||
Sum4(a.M21*b.M12, a.M22*b.M22, a.M23*b.M32, 0),
|
||||
Sum4(a.M21*b.M13, a.M22*b.M23, a.M23*b.M33, 0),
|
||||
Sum4(a.M31*b.M11, a.M32*b.M21, a.M33*b.M31, 0),
|
||||
Sum4(a.M31*b.M12, a.M32*b.M22, a.M33*b.M32, 0),
|
||||
Sum4(a.M31*b.M13, a.M32*b.M23, a.M33*b.M33, 0),
|
||||
Sum4(a.TX*b.M11, a.TY*b.M21, a.TZ*b.M31, b.TX),
|
||||
Sum4(a.TX*b.M12, a.TY*b.M22, a.TZ*b.M32, b.TY),
|
||||
Sum4(a.TX*b.M13, a.TY*b.M23, a.TZ*b.M33, b.TZ));
|
||||
|
||||
// Accumulate in the same order as the former 4x4 loop so boundary-sensitive nearest-neighbour
|
||||
// projection retains its floating-point behavior while avoiding an intermediate array.
|
||||
private static double Sum4(double a, double b, double c, double d)
|
||||
{
|
||||
var o = new double[16];
|
||||
for (int row=0; row<4; row++)
|
||||
for (int col=0; col<4; col++)
|
||||
for (int k=0; k<4; k++) o[row*4+col] += a[row*4+k] * b[k*4+col];
|
||||
return o;
|
||||
double result = 0;
|
||||
result += a;
|
||||
result += b;
|
||||
result += c;
|
||||
result += d;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1712,16 +1712,16 @@ public sealed class VirtualMachine
|
||||
Write(a[1], v.X); Write(a[2], v.Y); Write(a[3], v.Z); return pc + 1;
|
||||
}
|
||||
case "set-gfx-geom3": // 0x217 (handle)(a)(b)(c) -> V18
|
||||
Gfx.GetOrCreate(Read(a[0])).V18 = (Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1;
|
||||
Gfx.SetObjectAnchor(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
|
||||
case "set-gfx-geom3-b": // 0x219 (handle)(a)(b)(c) -> V24
|
||||
Gfx.GetOrCreate(Read(a[0])).V24 = (Read(a[1]), Read(a[2]), Read(a[3])); return pc + 1;
|
||||
Gfx.SetObjectPosition(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
|
||||
case "u0041AF00": // 0x80: default object slot substituted by native op 0x1d9
|
||||
case "set-default-gfx-object-slot":
|
||||
Gfx.SetDefaultObjectSlot((int)Read(a[0])); return pc + 1;
|
||||
|
||||
// ---- SC0000 anim/transform/spritesheet cluster (docs/engine-re.md §"SC0000 anim ... cluster") ----
|
||||
case "u00421DD0": // 0x22f set-position: (handle)(op2)(x)(y)(z) -> base position (direct set)
|
||||
Gfx.GetOrCreate(Read(a[0])).V24 = (Read(a[2]), Read(a[3]), Read(a[4])); return pc + 1;
|
||||
Gfx.SetObjectPosition(Read(a[0]), (Read(a[2]), Read(a[3]), Read(a[4]))); return pc + 1;
|
||||
case "u004219E0": // pre-reference compatibility
|
||||
case "set-gfx-range-transform": // 0x229 (first)(count)(anchor x/y/z)
|
||||
Gfx.SetRangeTransform(Read(a[0]), Read(a[1]), (Read(a[2]), Read(a[3]), Read(a[4])));
|
||||
@@ -1782,10 +1782,10 @@ public sealed class VirtualMachine
|
||||
case "gfx-set-scale-current": // 0x1fd (handle)(sx%)(sy%)(sz%) -> current scale matrix
|
||||
Gfx.SetCurrentScale(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
|
||||
case "set-gfx-field64": // 0x212 (idx)(val)
|
||||
Gfx.GetOrCreate(Read(a[0])).Field64 = Read(a[1]); return pc + 1;
|
||||
Gfx.SetObjectField64(Read(a[0]), Read(a[1])); return pc + 1;
|
||||
case "set-gfx-xy": // 0x213 (idx)(x)(y)
|
||||
{
|
||||
var o = Gfx.GetOrCreate(Read(a[0])); o.Field68 = Read(a[1]); o.Field6c = Read(a[2]); return pc + 1;
|
||||
Gfx.SetObjectFields68And6c(Read(a[0]), Read(a[1]), Read(a[2])); return pc + 1;
|
||||
}
|
||||
case "gfx-elem-erase": // 0x1f7 (handle)(count) — erase retained-object range
|
||||
Gfx.EraseRange(Read(a[0]), Read(a[1])); return pc + 1;
|
||||
|
||||
Reference in New Issue
Block a user