Profile and optimize retained rendering
This commit is contained in:
@@ -7,6 +7,17 @@ using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
|
||||
[Flags]
|
||||
public enum HostPresentationReason
|
||||
{
|
||||
None = 0,
|
||||
HostRequest = 1,
|
||||
ScreenTransition = 2,
|
||||
RetainedMutation = 4,
|
||||
ContinuousChannel = 8,
|
||||
DiscreteSourceCell = 16,
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed class GodotAdvHost : IHost
|
||||
{
|
||||
@@ -185,6 +196,16 @@ public sealed class GodotAdvHost : IHost
|
||||
return _surfaceText.TryGetValue(surfaceSlot, out var draws) ? draws.ToArray() : Array.Empty<SurfaceTextDraw>();
|
||||
}
|
||||
|
||||
public void SnapshotSurfaceText(int surfaceSlot, List<SurfaceTextDraw> snapshot)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
lock (_textLock)
|
||||
{
|
||||
snapshot.Clear();
|
||||
if (_surfaceText.TryGetValue(surfaceSlot, out var draws)) snapshot.AddRange(draws);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearRenderedAdvTextLayout(int layoutSlot)
|
||||
{
|
||||
lock (_textLock) _historyText.Remove(layoutSlot == 0 ? _currentAdvLayout : layoutSlot);
|
||||
@@ -481,7 +502,12 @@ public sealed class GodotAdvHost : IHost
|
||||
}
|
||||
|
||||
public void InputCallbackCompleted(GfxState gfx)
|
||||
=> Interlocked.Exchange(ref _presentRequested, 1);
|
||||
{
|
||||
// Callback completion itself is not native graphics dirtiness. Any retained writes made by the
|
||||
// callback are published through GfxState's mutation generation; host-owned surface writes set
|
||||
// _presentRequested at their actual mutation sites. FIELD services a 50 ms hover callback even
|
||||
// while the pointer is idle, so an unconditional request here recreates its sleep-poll overdraw.
|
||||
}
|
||||
|
||||
public long InputClockMilliseconds => _clock.NowMs;
|
||||
|
||||
@@ -639,12 +665,22 @@ public sealed class GodotAdvHost : IHost
|
||||
// Native retained-object writes are not front-buffer writes. Publish explicit/service-boundary dirtiness
|
||||
// once, then continue only while the sampled retained scene can actually change. Text reveal is a separate
|
||||
// Godot Label; waiting/sleeping alone do not alter background pixels.
|
||||
public bool ShouldRecomposite(GfxState gfx)
|
||||
public HostPresentationReason ConsumePresentationReasons(GfxState gfx)
|
||||
{
|
||||
bool screenTransitionActive;
|
||||
lock (_screenTransitionLock) screenTransitionActive = _screenTransition != null;
|
||||
return System.Threading.Interlocked.Exchange(ref _presentRequested, 0) != 0 ||
|
||||
screenTransitionActive || gfx.HasActiveVisualPresentation(_clock.NowMs);
|
||||
var reasons = HostPresentationReason.None;
|
||||
if (System.Threading.Interlocked.Exchange(ref _presentRequested, 0) != 0)
|
||||
reasons |= HostPresentationReason.HostRequest;
|
||||
if (screenTransitionActive) reasons |= HostPresentationReason.ScreenTransition;
|
||||
GfxPresentationReason gfxReasons = gfx.ConsumePresentationReasons(_clock.NowMs);
|
||||
if ((gfxReasons & GfxPresentationReason.RetainedMutation) != 0)
|
||||
reasons |= HostPresentationReason.RetainedMutation;
|
||||
if ((gfxReasons & GfxPresentationReason.ContinuousChannel) != 0)
|
||||
reasons |= HostPresentationReason.ContinuousChannel;
|
||||
if ((gfxReasons & GfxPresentationReason.DiscreteSourceCell) != 0)
|
||||
reasons |= HostPresentationReason.DiscreteSourceCell;
|
||||
return reasons;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
@@ -760,9 +796,6 @@ public sealed class GodotAdvHost : IHost
|
||||
long ms = NormalizeSleepMilliseconds(duration, SleepScale);
|
||||
long deadline = _clock.NowMs + ms;
|
||||
_timeline?.State("sleep", new() { ["duration_ms"] = ms, ["deadline_ms"] = deadline });
|
||||
// A sleep is a service boundary: make preceding retained writes visible once even when no animation
|
||||
// channel is active during the hold.
|
||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
||||
IsSleeping = true;
|
||||
while (_clock.NowMs < deadline)
|
||||
{
|
||||
|
||||
@@ -97,6 +97,12 @@ public sealed class GodotTraceSink : ITraceSink
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Allocation-free current coordinate for once-per-frame diagnostics.</summary>
|
||||
public GodotTraceStepSnapshot? LatestStep
|
||||
{
|
||||
get { lock (_snapshotLock) return _latestStep; }
|
||||
}
|
||||
|
||||
private string[] CurrentCallStackLocked()
|
||||
{
|
||||
var stack = _scripts.ToArray();
|
||||
|
||||
262
godot/Main.cs
262
godot/Main.cs
@@ -86,6 +86,8 @@ public partial class Main : Godot.Control
|
||||
private string? _timelineLogPath; // --timeline-log <jsonl>: synchronized VM/host/compositor evidence
|
||||
private GodotTimelineLog? _timeline;
|
||||
private int _timelineFrame;
|
||||
private string? _perfLogPath; // --perf-log <csv>: low-overhead frame/compositor timings + work
|
||||
private PerformanceFrameLog? _perf;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
@@ -184,6 +186,7 @@ public partial class Main : Godot.Control
|
||||
if (userArgs[i] == "--shot-sequence" && i + 1 < userArgs.Length) _seqDir = userArgs[i + 1];
|
||||
if (userArgs[i] == "--gfx-log" && i + 1 < userArgs.Length) _gfxLogPath = userArgs[i + 1];
|
||||
if (userArgs[i] == "--timeline-log" && i + 1 < userArgs.Length) _timelineLogPath = userArgs[i + 1];
|
||||
if (userArgs[i] == "--perf-log" && i + 1 < userArgs.Length) _perfLogPath = userArgs[i + 1];
|
||||
if (userArgs[i] == "--frames" && i + 1 < userArgs.Length) int.TryParse(userArgs[i + 1], out _seqFrames);
|
||||
if (userArgs[i] == "--sleep-scale" && i + 1 < userArgs.Length) double.TryParse(userArgs[i + 1], out sleepScale);
|
||||
if (userArgs[i] == "--speed" && i + 1 < userArgs.Length) double.TryParse(userArgs[i + 1], out speed);
|
||||
@@ -225,6 +228,7 @@ public partial class Main : Godot.Control
|
||||
var resources = scripts != null ? new ResourceMap(scripts.Catalog) : ResourceMap.Load();
|
||||
_host = new GodotAdvHost(this, resources, scene, _clock, _locator, _timeline) { SleepScale = sleepScale, TraceOps = _gfxLogPath != null };
|
||||
_trace = new GodotTraceSink(_locator, _timeline);
|
||||
if (_perfLogPath != null) _perf = new PerformanceFrameLog(_perfLogPath);
|
||||
// --trace-histogram: aggregate op/call-site execution counts of the REAL Godot run (headless flow
|
||||
// diverges — wait-for-input is a no-op there — so this is the only way to profile the live path).
|
||||
_table = table;
|
||||
@@ -322,51 +326,80 @@ public partial class Main : Godot.Control
|
||||
_clock.Advance(delta);
|
||||
_timelineFrame++;
|
||||
_timeline?.SetFrame(_timelineFrame, _clock.NowMs);
|
||||
_host?.PulseFrame();
|
||||
UpdateVoicePlaybackState();
|
||||
AdoptPendingMovies();
|
||||
UpdateMovieFrames();
|
||||
if (!_selftest && _vm != null && _host != null && _host.ShouldRecomposite(_vm.Gfx))
|
||||
Recomposite(); // native publishes retained mutations only at present/service boundaries
|
||||
if (!_selftest && _host != null) UpdateAdvTextPresentation();
|
||||
if (!_selftest && _host != null) UpdateAdvWaitIndicatorPresentation();
|
||||
if (!_selftest && _host != null) UpdateHistoryTextPresentation();
|
||||
// --shot-sequence: dump one PNG per frame across the opening so a time-based (paced) effect can be
|
||||
// verified as distinct frames, not just the final state. Captures after Recomposite; quits when full.
|
||||
if (_seqDir != null && _seqIdx < _seqFrames && !_done)
|
||||
var perf = _perf;
|
||||
var step = perf != null ? _trace.LatestStep : null;
|
||||
perf?.BeginFrame(_timelineFrame, _clock.NowMs, delta,
|
||||
step?.Script ?? "<startup>", step?.Offset ?? -1, step?.Opcode ?? -1);
|
||||
try
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(_seqDir);
|
||||
// Headless has no rendered viewport texture (GetImage() is null). Still advance/count/quit so the
|
||||
// real-run trace-histogram can profile the live path without a display; only the PNG grab is skipped.
|
||||
var fimg = GetViewport().GetTexture()?.GetImage();
|
||||
fimg?.SavePng($"{_seqDir}/frame_{_seqIdx:0000}.png");
|
||||
_seqIdx++;
|
||||
if (_seqIdx >= _seqFrames) { GD.Print($"SEQ saved {_seqIdx} frames -> {_seqDir}"); GetTree().Quit(0); }
|
||||
return;
|
||||
}
|
||||
// --shot: once the target page is composed and parked at wait-for-input, settle a few frames then grab it.
|
||||
if (_shotPath != null && !_shotDone && _host != null && (_host.Pages >= _shotPage && _host.IsWaiting || _done))
|
||||
{
|
||||
if (++_shotSettle >= _shotSettleTarget)
|
||||
long phase = perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
_host?.PulseFrame();
|
||||
perf?.RecordPulse(PerformanceFrameLog.Timestamp() - phase);
|
||||
|
||||
phase = perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
UpdateVoicePlaybackState();
|
||||
AdoptPendingMovies();
|
||||
UpdateMovieFrames();
|
||||
perf?.RecordMovies(PerformanceFrameLog.Timestamp() - phase);
|
||||
|
||||
phase = perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
HostPresentationReason presentationReasons = !_selftest && _vm != null && _host != null
|
||||
? _host.ConsumePresentationReasons(_vm.Gfx)
|
||||
: HostPresentationReason.None;
|
||||
bool shouldRecomposite = presentationReasons != HostPresentationReason.None;
|
||||
perf?.RecordPresentationReasons((int)presentationReasons);
|
||||
perf?.RecordShouldRecomposite(PerformanceFrameLog.Timestamp() - phase);
|
||||
long allocationPhase = perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||
if (shouldRecomposite)
|
||||
Recomposite(); // native publishes retained mutations only at present/service boundaries
|
||||
perf?.RecordRecomposeAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||
|
||||
phase = perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
allocationPhase = perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||
if (!_selftest && _host != null) UpdateAdvTextPresentation();
|
||||
if (!_selftest && _host != null) UpdateAdvWaitIndicatorPresentation();
|
||||
if (!_selftest && _host != null) UpdateHistoryTextPresentation();
|
||||
perf?.RecordUiAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||
perf?.RecordUi(PerformanceFrameLog.Timestamp() - phase);
|
||||
|
||||
// --shot-sequence: dump one PNG per frame across the opening so a time-based (paced) effect can be
|
||||
// verified as distinct frames, not just the final state. Captures after Recomposite; quits when full.
|
||||
if (_seqDir != null && _seqIdx < _seqFrames && !_done)
|
||||
{
|
||||
_shotDone = true;
|
||||
var img = GetViewport().GetTexture().GetImage();
|
||||
img.SavePng(_shotPath);
|
||||
GD.Print($"SHOT saved page {_pageCount} -> {_shotPath}");
|
||||
ReportSubroutines();
|
||||
GetTree().Quit(0);
|
||||
System.IO.Directory.CreateDirectory(_seqDir);
|
||||
// Headless has no rendered viewport texture (GetImage() is null). Still advance/count/quit so the
|
||||
// real-run trace-histogram can profile the live path without a display; only the PNG grab is skipped.
|
||||
var fimg = GetViewport().GetTexture()?.GetImage();
|
||||
fimg?.SavePng($"{_seqDir}/frame_{_seqIdx:0000}.png");
|
||||
_seqIdx++;
|
||||
if (_seqIdx >= _seqFrames) { GD.Print($"SEQ saved {_seqIdx} frames -> {_seqDir}"); GetTree().Quit(0); }
|
||||
return;
|
||||
}
|
||||
// --shot: once the target page is composed and parked at wait-for-input, settle a few frames then grab it.
|
||||
if (_shotPath != null && !_shotDone && _host != null && (_host.Pages >= _shotPage && _host.IsWaiting || _done))
|
||||
{
|
||||
if (++_shotSettle >= _shotSettleTarget)
|
||||
{
|
||||
_shotDone = true;
|
||||
var img = GetViewport().GetTexture().GetImage();
|
||||
img.SavePng(_shotPath);
|
||||
GD.Print($"SHOT saved page {_pageCount} -> {_shotPath}");
|
||||
ReportSubroutines();
|
||||
GetTree().Quit(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_done && !_ended)
|
||||
{
|
||||
_ended = true;
|
||||
DumpHistogram();
|
||||
GD.Print($"[vm] ended: {_vm!.HaltReason ?? "unknown"} after {_vm.Steps} steps");
|
||||
ReportSubroutines();
|
||||
ShowEnd();
|
||||
if (_selftest) RunSelfTest();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_done && !_ended)
|
||||
{
|
||||
_ended = true;
|
||||
DumpHistogram();
|
||||
GD.Print($"[vm] ended: {_vm!.HaltReason ?? "unknown"} after {_vm.Steps} steps");
|
||||
ReportSubroutines();
|
||||
ShowEnd();
|
||||
if (_selftest) RunSelfTest();
|
||||
}
|
||||
finally { perf?.EndFrame(); }
|
||||
}
|
||||
|
||||
// _Input (not _UnhandledInput): the root Control consumes mouse clicks as GUI input before they
|
||||
@@ -688,6 +721,12 @@ public partial class Main : Godot.Control
|
||||
public override void _ExitTree()
|
||||
{
|
||||
DumpHistogram(); _host?.Stop(); _timeline?.Dispose(); _locator?.Dispose();
|
||||
if (_perf != null)
|
||||
{
|
||||
_perf.Dispose();
|
||||
GD.Print($"[perf-log] wrote {_perf.FrameCount} frames / {_perf.RecompositeCount} recomposites -> {_perf.Path}");
|
||||
_perf = null;
|
||||
}
|
||||
foreach (var movie in _pendingMovies.Values) movie.Decoder.Dispose();
|
||||
_pendingMovies.Clear();
|
||||
foreach (var movie in _movies.Values) movie.Decoder.Dispose();
|
||||
@@ -718,30 +757,62 @@ public partial class Main : Godot.Control
|
||||
// GfxState and applied here; object opacity comes only from the actual blend/color path.
|
||||
private sealed record CachedPixels(int Width, int Height, byte[] Rgba);
|
||||
private readonly System.Collections.Generic.Dictionary<(int AssetId, long Key), CachedPixels> _pixelCache = new();
|
||||
private readonly System.Collections.Generic.List<RenderObject> _visibleSnapshot = new(1024);
|
||||
private readonly System.Collections.Generic.List<SurfaceTextDraw> _surfaceTextSnapshot = new();
|
||||
|
||||
private void Recomposite()
|
||||
{
|
||||
if (_perf != null)
|
||||
{
|
||||
var presentStep = _trace.LatestStep;
|
||||
_perf.RecordPresentationCoordinate(presentStep?.Script ?? "<startup>",
|
||||
presentStep?.Offset ?? -1, presentStep?.Opcode ?? -1);
|
||||
}
|
||||
long phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
long allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||
bool hasScreenTransition = _host.TrySnapshotScreenTransition(out var transition);
|
||||
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
|
||||
_perf?.BeginRecomposite(hasScreenTransition);
|
||||
|
||||
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
System.Array.Clear(_screenPixels);
|
||||
foreach (var label in _surfaceTextLabels) label.Visible = false;
|
||||
_perf?.RecordClear(PerformanceFrameLog.Timestamp() - phase);
|
||||
int surfaceTextLabelIndex = 0;
|
||||
System.Collections.Generic.Dictionary<long, string>? decisions = _gfxLogPath != null || _timeline != null ? new() : null;
|
||||
if (_host.TrySnapshotScreenTransition(out var transition))
|
||||
if (hasScreenTransition)
|
||||
{
|
||||
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||
// Native mode 4 keeps the captured source opaque and alpha-composites the complete target
|
||||
// surface over it. Each offscreen target has an opaque-black clear beneath its objects.
|
||||
CompositeVisibleObjects(transition.Source, 1f, ref surfaceTextLabelIndex, decisions, false);
|
||||
FillQuad(0, 0, ScreenWidth, ScreenHeight, 0, (float)transition.Progress);
|
||||
CompositeVisibleObjects(transition.Target, (float)transition.Progress,
|
||||
ref surfaceTextLabelIndex, decisions, false);
|
||||
_perf?.RecordCompositeAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||
}
|
||||
else
|
||||
{
|
||||
var visible = _vm.Gfx.SnapshotVisibleObjects(_clock.NowMs); // synchronized objects + ranges
|
||||
CompositeVisibleObjects(visible, 1f, ref surfaceTextLabelIndex, decisions, true);
|
||||
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||
_vm.Gfx.SnapshotVisibleObjects(_clock.NowMs, _visibleSnapshot); // synchronized objects + ranges
|
||||
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
|
||||
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||
CompositeVisibleObjects(_visibleSnapshot, 1f, ref surfaceTextLabelIndex, decisions, true);
|
||||
_perf?.RecordCompositeAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||
}
|
||||
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||
_screen.SetData(ScreenWidth, ScreenHeight, false, Image.Format.Rgba8, _screenPixels);
|
||||
_perf?.RecordSetDataAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||
_perf?.RecordSetData(PerformanceFrameLog.Timestamp() - phase);
|
||||
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
_screenTex.Update(_screen);
|
||||
_perf?.RecordTextureUpdate(PerformanceFrameLog.Timestamp() - phase);
|
||||
if (decisions != null) LogGfxDecisionChanges(decisions);
|
||||
_perf?.EndRecomposite();
|
||||
}
|
||||
|
||||
private void CompositeVisibleObjects(IReadOnlyList<RenderObject> visible, float globalOpacity,
|
||||
@@ -752,6 +823,7 @@ public partial class Main : Godot.Control
|
||||
int z = 0;
|
||||
foreach (var v in visible) // interpolate at the retained-presentation clock
|
||||
{
|
||||
_perf?.RecordObject(v.TimeVarying);
|
||||
var t = v.Transform;
|
||||
var affine = Age.Engine.Model.Transform2DMath.Build(t, v.Rotation);
|
||||
var localToDest = affine.FromLocalOrigin(v.DstX, v.DstY);
|
||||
@@ -763,16 +835,23 @@ public partial class Main : Godot.Control
|
||||
float opacity = v.Alpha / 255f * globalOpacity; // transform Z is never opacity
|
||||
float strength = v.TintStrength / 255f; // tint-blend / fill strength
|
||||
var rawObject = _vm.Gfx.TryGet(v.Handle);
|
||||
long resolveStarted = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
var surfaceTexture = rawObject != null
|
||||
? _host.ResolveSurfaceTexture(rawObject.SourceSlot, v.SurfaceResId)
|
||||
: null;
|
||||
_perf?.RecordResolve(PerformanceFrameLog.Timestamp() - resolveStarted);
|
||||
bool movieSurfaceBound = rawObject != null && _host.IsMovieSurfaceBound(rawObject.SourceSlot);
|
||||
string outcome;
|
||||
// These strings exist only for --gfx-log/timeline diagnostics. DEBUGMAP visits roughly one
|
||||
// thousand retained objects per composition, so formatting them unconditionally creates
|
||||
// several megabytes of short-lived garbage even in an ordinary run.
|
||||
string? outcome = null;
|
||||
if (v.SurfaceTransition is { } transition)
|
||||
{
|
||||
_perf?.RecordTransitionLayer();
|
||||
int layers = DrawTransitionRange(visible, transition);
|
||||
outcome = $"TRANSITION slot={transition.TargetSlot} key=0x{transition.CommandKey:x} " +
|
||||
$"progress={transition.Progress:0.000} forced={transition.Forced} layers={layers}";
|
||||
if (decisions != null)
|
||||
outcome = $"TRANSITION slot={transition.TargetSlot} key=0x{transition.CommandKey:x} " +
|
||||
$"progress={transition.Progress:0.000} forced={transition.Forced} layers={layers}";
|
||||
}
|
||||
else if (v.SurfaceResId == 0 && surfaceTexture == null)
|
||||
{
|
||||
@@ -785,37 +864,54 @@ public partial class Main : Godot.Control
|
||||
// One-shot/mode-1 packed color supplies opacity directly. Static mode-0 fills retain
|
||||
// the tint-strength convention used by the existing effect objects.
|
||||
float fillA = v.MultiplyTint ? opacity : opacity * strength;
|
||||
_perf?.RecordFillLayer();
|
||||
FillAffineQuad(baseW, baseH, localToDest, v.Tint, fillA);
|
||||
outcome = $"FILL tint=0x{v.Tint:x6} a={fillA:0.00} {baseW}x{baseH}@({dstX},{dstY}) " +
|
||||
$"base=({v.DstX},{v.DstY}) anchor=({t.AnchorX:0.0},{t.AnchorY:0.0}) " +
|
||||
$"scale=({t.ScaleX:0.00},{t.ScaleY:0.00}) " +
|
||||
$"trans=({t.TranslateX:0.0},{t.TranslateY:0.0}) rot={v.Rotation.AngleDegrees:0.0}" +
|
||||
ColorTimeline(v.ColorTransition);
|
||||
if (decisions != null)
|
||||
outcome = $"FILL tint=0x{v.Tint:x6} a={fillA:0.00} {baseW}x{baseH}@({dstX},{dstY}) " +
|
||||
$"base=({v.DstX},{v.DstY}) anchor=({t.AnchorX:0.0},{t.AnchorY:0.0}) " +
|
||||
$"scale=({t.ScaleX:0.00},{t.ScaleY:0.00}) " +
|
||||
$"trans=({t.TranslateX:0.0},{t.TranslateY:0.0}) rot={v.Rotation.AngleDegrees:0.0}" +
|
||||
ColorTimeline(v.ColorTransition);
|
||||
}
|
||||
else
|
||||
{
|
||||
_perf?.RecordSkippedLayer();
|
||||
if (decisions != null) outcome = "SKIP(no-resId, opaque render-target)";
|
||||
}
|
||||
else outcome = "SKIP(no-resId, opaque render-target)";
|
||||
}
|
||||
else
|
||||
{
|
||||
var texture = surfaceTexture
|
||||
?? (movieSurfaceBound ? null : _host.ResolveResIdTexture(v.SurfaceResId));
|
||||
if (texture == null) outcome = $"SKIP(resId=0x{v.SurfaceResId:x} UNRESOLVED)";
|
||||
var texture = surfaceTexture;
|
||||
if (texture == null && !movieSurfaceBound)
|
||||
{
|
||||
resolveStarted = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
texture = _host.ResolveResIdTexture(v.SurfaceResId);
|
||||
_perf?.RecordResolve(PerformanceFrameLog.Timestamp() - resolveStarted);
|
||||
}
|
||||
if (texture == null)
|
||||
{
|
||||
_perf?.RecordSkippedLayer();
|
||||
if (decisions != null) outcome = $"SKIP(resId=0x{v.SurfaceResId:x} UNRESOLVED)";
|
||||
}
|
||||
else
|
||||
{
|
||||
BlitLayer(texture.Value.Image, texture.Value.AssetId, v.ColorKey, v.Tint, strength, v.SrcX, v.SrcY, v.W, v.H,
|
||||
localToDest, opacity, v.MultiplyTint, texture.Value.IsDynamic, v.Blend);
|
||||
outcome = $"slot={rawObject?.SourceSlot} DRAWN resId=0x{v.SurfaceResId:x} {texture.Value.Name} " +
|
||||
$"src=({v.SrcX},{v.SrcY} {v.W}x{v.H}) base=({v.DstX},{v.DstY}) " +
|
||||
$"anchor=({t.AnchorX:0.0},{t.AnchorY:0.0}) dst=({dstX},{dstY}) " +
|
||||
$"scale=({t.ScaleX:0.00},{t.ScaleY:0.00}) trans=({t.TranslateX:0.0},{t.TranslateY:0.0}) " +
|
||||
$"rot=({t.RotationAngleDegrees:0.0}+{v.Rotation.AngleDegrees:0.0}) " +
|
||||
$"mode={rawObject?.StaticColorMode} op={opacity:0.00} tintStr={strength:0.00}" +
|
||||
ColorTimeline(v.ColorTransition);
|
||||
if (decisions != null)
|
||||
outcome = $"slot={rawObject?.SourceSlot} DRAWN resId=0x{v.SurfaceResId:x} {texture.Value.Name} " +
|
||||
$"src=({v.SrcX},{v.SrcY} {v.W}x{v.H}) base=({v.DstX},{v.DstY}) " +
|
||||
$"anchor=({t.AnchorX:0.0},{t.AnchorY:0.0}) dst=({dstX},{dstY}) " +
|
||||
$"scale=({t.ScaleX:0.00},{t.ScaleY:0.00}) trans=({t.TranslateX:0.0},{t.TranslateY:0.0}) " +
|
||||
$"rot=({t.RotationAngleDegrees:0.0}+{v.Rotation.AngleDegrees:0.0}) " +
|
||||
$"mode={rawObject?.StaticColorMode} op={opacity:0.00} tintStr={strength:0.00}" +
|
||||
ColorTimeline(v.ColorTransition);
|
||||
}
|
||||
}
|
||||
if (decisions != null) decisions[v.Handle] = $"z{z} {outcome}";
|
||||
if (includeSurfaceText && rawObject != null)
|
||||
{
|
||||
foreach (var surfaceText in _host.SnapshotSurfaceText(rawObject.SourceSlot))
|
||||
_host.SnapshotSurfaceText(rawObject.SourceSlot, _surfaceTextSnapshot);
|
||||
foreach (var surfaceText in _surfaceTextSnapshot)
|
||||
{
|
||||
if (surfaceText.X < v.SrcX || surfaceText.X >= v.SrcX + v.W ||
|
||||
surfaceText.Y < v.SrcY || surfaceText.Y >= v.SrcY + v.H) continue;
|
||||
@@ -969,25 +1065,42 @@ public partial class Main : Godot.Control
|
||||
{
|
||||
if (source.Handle < transition.RangeBStart || source.Handle >= end || source.SurfaceTransition != null)
|
||||
continue;
|
||||
_perf?.RecordObject(source.TimeVarying);
|
||||
var affine = Transform2DMath.Build(source.Transform, source.Rotation).FromLocalOrigin(source.DstX, source.DstY);
|
||||
if (source.RangeTransform is { } rangeTransform)
|
||||
affine = affine.Then(rangeTransform);
|
||||
float opacity = source.Alpha / 255f * (float)transition.Progress;
|
||||
var rawObject = _vm.Gfx.TryGet(source.Handle);
|
||||
long resolveStarted = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
var texture = rawObject != null
|
||||
? _host.ResolveSurfaceTexture(rawObject.SourceSlot, source.SurfaceResId)
|
||||
: null;
|
||||
_perf?.RecordResolve(PerformanceFrameLog.Timestamp() - resolveStarted);
|
||||
bool movieSurfaceBound = rawObject != null && _host.IsMovieSurfaceBound(rawObject.SourceSlot);
|
||||
if (source.SurfaceResId == 0 && texture == null)
|
||||
{
|
||||
if (source.Blend == BlendKind.Opaque) continue;
|
||||
if (source.Blend == BlendKind.Opaque)
|
||||
{
|
||||
_perf?.RecordSkippedLayer();
|
||||
continue;
|
||||
}
|
||||
int w = source.W > 0 ? source.W : 800, h = source.H > 0 ? source.H : 600;
|
||||
_perf?.RecordFillLayer();
|
||||
FillAffineQuad(w, h, affine, source.Tint, opacity * source.TintStrength / 255f);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!movieSurfaceBound) texture ??= _host.ResolveResIdTexture(source.SurfaceResId);
|
||||
if (texture == null) continue;
|
||||
if (!movieSurfaceBound && texture == null)
|
||||
{
|
||||
resolveStarted = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
texture = _host.ResolveResIdTexture(source.SurfaceResId);
|
||||
_perf?.RecordResolve(PerformanceFrameLog.Timestamp() - resolveStarted);
|
||||
}
|
||||
if (texture == null)
|
||||
{
|
||||
_perf?.RecordSkippedLayer();
|
||||
continue;
|
||||
}
|
||||
BlitLayer(texture.Value.Image, texture.Value.AssetId, source.ColorKey, source.Tint, source.TintStrength / 255f,
|
||||
source.SrcX, source.SrcY, source.W, source.H, affine, opacity, source.MultiplyTint,
|
||||
texture.Value.IsDynamic, source.Blend);
|
||||
@@ -1039,6 +1152,8 @@ public partial class Main : Godot.Control
|
||||
// right<=left or bottom<=top. FIELD deliberately creates zero-area prototype objects from SO005;
|
||||
// expanding those dimensions to the full texture leaks the entire spritesheet onto the map.
|
||||
if (w <= 0 || h <= 0) return;
|
||||
long sourcePrepStarted = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
long sourcePrepAllocated = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||
var cacheKey = (assetId, colorKey);
|
||||
int sourceWidth, sourceHeight;
|
||||
byte[] sourcePixels;
|
||||
@@ -1077,16 +1192,24 @@ public partial class Main : Godot.Control
|
||||
int sh = h;
|
||||
sw = System.Math.Min(sw, sourceWidth - srcX);
|
||||
sh = System.Math.Min(sh, sourceHeight - srcY);
|
||||
_perf?.RecordSourcePrep(PerformanceFrameLog.Timestamp() - sourcePrepStarted);
|
||||
_perf?.RecordSourcePrepAllocation(PerformanceFrameLog.AllocatedBytes() - sourcePrepAllocated);
|
||||
if (sw <= 0 || sh <= 0) return;
|
||||
long rasterStarted = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
Age.Engine.Model.SoftwareAffineRasterizer.BlitRgba(
|
||||
_screenPixels, ScreenWidth, ScreenHeight, sourcePixels, sourceWidth, sourceHeight,
|
||||
srcX, srcY, sw, sh, localToDest, tint, tintStrength, alpha, multiplyTint, blend);
|
||||
_perf?.RecordRaster(sw, sh, localToDest, ScreenWidth, ScreenHeight, dynamic, blend,
|
||||
PerformanceFrameLog.Timestamp() - rasterStarted);
|
||||
}
|
||||
|
||||
private void FillAffineQuad(int w, int h, Age.Engine.Model.Affine2D localToDest, long tint, float alpha)
|
||||
{
|
||||
long rasterStarted = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
Age.Engine.Model.SoftwareAffineRasterizer.FillRgba(
|
||||
_screenPixels, ScreenWidth, ScreenHeight, w, h, localToDest, tint, alpha);
|
||||
_perf?.RecordRaster(w, h, localToDest, ScreenWidth, ScreenHeight, false, BlendKind.Alpha,
|
||||
PerformanceFrameLog.Timestamp() - rasterStarted);
|
||||
}
|
||||
|
||||
// Alpha-blend a solid tint (0xRRGGBB) rectangle over the screen — the surfaceless fade/flash fill.
|
||||
@@ -1094,6 +1217,7 @@ public partial class Main : Godot.Control
|
||||
{
|
||||
int ia = (int)(System.Math.Clamp(alpha, 0f, 1f) * 255);
|
||||
if (ia == 0) return;
|
||||
long rasterStarted = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||
int tr = (int)((tint >> 16) & 0xff), tg = (int)((tint >> 8) & 0xff), tb = (int)(tint & 0xff);
|
||||
byte[] dst = _screenPixels;
|
||||
int dw = ScreenWidth, dh = ScreenHeight;
|
||||
@@ -1110,6 +1234,10 @@ public partial class Main : Godot.Control
|
||||
dst[di + 2] = (byte)((tb * ia + dst[di + 2] * (255 - ia)) / 255);
|
||||
dst[di + 3] = (byte)System.Math.Min(255, dst[di + 3] + ia);
|
||||
}
|
||||
_perf?.RecordFillLayer();
|
||||
_perf?.RecordRaster(w, h, new Affine2D(1, 0, 0, 1, dstX, dstY),
|
||||
ScreenWidth, ScreenHeight, false, BlendKind.Alpha,
|
||||
PerformanceFrameLog.Timestamp() - rasterStarted);
|
||||
}
|
||||
|
||||
// Make colorkey-matching texels transparent (native colorkey is baked at surface load).
|
||||
|
||||
@@ -94,16 +94,18 @@ internal sealed class MovieSurfaceRegistry
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var binding in _byPlayback.Values
|
||||
.Where(binding => binding.ResourceId == resourceId)
|
||||
.OrderByDescending(binding => binding.PlaybackId))
|
||||
long newestPlaybackId = long.MinValue;
|
||||
MovieSurfaceFrame? newestFrame = null;
|
||||
foreach (var binding in _byPlayback.Values)
|
||||
{
|
||||
if (!_frames.TryGetValue(binding.PlaybackId, out var found)) continue;
|
||||
frame = found;
|
||||
return true;
|
||||
if (binding.ResourceId != resourceId || binding.PlaybackId <= newestPlaybackId ||
|
||||
!_frames.TryGetValue(binding.PlaybackId, out var found))
|
||||
continue;
|
||||
newestPlaybackId = binding.PlaybackId;
|
||||
newestFrame = found;
|
||||
}
|
||||
frame = null;
|
||||
return false;
|
||||
frame = newestFrame;
|
||||
return newestFrame != null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
297
godot/PerformanceFrameLog.cs
Normal file
297
godot/PerformanceFrameLog.cs
Normal file
@@ -0,0 +1,297 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Age.Engine.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Buffered, diagnostic-only CSV writer for real Godot frame and retained-compositor cost. The writer is
|
||||
/// deliberately independent of Godot types so its schema and clipping arithmetic can be unit tested.
|
||||
/// </summary>
|
||||
public sealed class PerformanceFrameLog : IDisposable
|
||||
{
|
||||
private const int FlushIntervalFrames = 120;
|
||||
private static readonly double MillisecondsPerTick = 1000.0 / Stopwatch.Frequency;
|
||||
private readonly StreamWriter _writer;
|
||||
private Frame _current = new();
|
||||
private bool _frameOpen;
|
||||
private bool _disposed;
|
||||
private int _framesSinceFlush;
|
||||
|
||||
public long FrameCount { get; private set; }
|
||||
public long RecompositeCount { get; private set; }
|
||||
public string Path { get; }
|
||||
|
||||
public PerformanceFrameLog(string path)
|
||||
{
|
||||
Path = path;
|
||||
var directory = System.IO.Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory);
|
||||
_writer = new StreamWriter(path, append: false, Encoding.UTF8, 64 * 1024);
|
||||
// Presentation coordinates are appended separately so a VM thread released by PulseFrame can be
|
||||
// distinguished from the coordinate observed at _Process entry.
|
||||
_writer.WriteLine(
|
||||
"frame,now_ms,delta_ms,main_ms,pulse_ms,movie_ms,should_recomposite_ms,recomposite_ms," +
|
||||
"clear_ms,snapshot_ms,resolve_ms,source_prep_ms,raster_ms,set_data_ms,texture_update_ms,ui_ms," +
|
||||
"allocated_bytes,recompose_allocated_bytes,snapshot_allocated_bytes," +
|
||||
"composite_allocated_bytes,source_prep_allocated_bytes,set_data_allocated_bytes," +
|
||||
"ui_allocated_bytes,gen0,gen1,gen2,recomposited,screen_transition," +
|
||||
"present_host_request,present_screen_transition,present_retained_mutation," +
|
||||
"present_continuous_channel,present_discrete_cell,object_visits," +
|
||||
"time_varying_objects,draw_layers,fill_layers,transition_layers,skipped_layers," +
|
||||
"integer_layers,fractional_translation_layers,axis_aligned_scale_layers," +
|
||||
"general_affine_layers,affine_layers,singular_layers,dynamic_layers,opaque_layers,alpha_layers," +
|
||||
"additive_layers,source_pixels,candidate_pixels,full_screen_layers,script,offset,opcode," +
|
||||
"present_script,present_offset,present_opcode");
|
||||
}
|
||||
|
||||
public static long Timestamp() => Stopwatch.GetTimestamp();
|
||||
public static long AllocatedBytes() => GC.GetAllocatedBytesForCurrentThread();
|
||||
|
||||
public void BeginFrame(int frame, long nowMs, double deltaSeconds,
|
||||
string script, int offset, int opcode)
|
||||
{
|
||||
if (_disposed) return;
|
||||
if (_frameOpen) EndFrame();
|
||||
_current = new Frame
|
||||
{
|
||||
Number = frame,
|
||||
NowMs = nowMs,
|
||||
DeltaMs = deltaSeconds * 1000.0,
|
||||
Script = script,
|
||||
Offset = offset,
|
||||
Opcode = opcode,
|
||||
Started = Timestamp(),
|
||||
AllocatedStart = GC.GetAllocatedBytesForCurrentThread(),
|
||||
Gen0Start = GC.CollectionCount(0),
|
||||
Gen1Start = GC.CollectionCount(1),
|
||||
Gen2Start = GC.CollectionCount(2),
|
||||
};
|
||||
_frameOpen = true;
|
||||
}
|
||||
|
||||
public void RecordPulse(long ticks) => _current.PulseTicks += ticks;
|
||||
public void RecordMovies(long ticks) => _current.MovieTicks += ticks;
|
||||
public void RecordShouldRecomposite(long ticks) => _current.ShouldTicks += ticks;
|
||||
public void RecordPresentationReasons(int reasons)
|
||||
{
|
||||
_current.PresentHostRequest |= (reasons & 1) != 0;
|
||||
_current.PresentScreenTransition |= (reasons & 2) != 0;
|
||||
_current.PresentRetainedMutation |= (reasons & 4) != 0;
|
||||
_current.PresentContinuousChannel |= (reasons & 8) != 0;
|
||||
_current.PresentDiscreteCell |= (reasons & 16) != 0;
|
||||
}
|
||||
public void RecordUi(long ticks) => _current.UiTicks += ticks;
|
||||
public void RecordClear(long ticks) => _current.ClearTicks += ticks;
|
||||
public void RecordSnapshot(long ticks) => _current.SnapshotTicks += ticks;
|
||||
public void RecordResolve(long ticks) => _current.ResolveTicks += ticks;
|
||||
public void RecordSourcePrep(long ticks) => _current.SourcePrepTicks += ticks;
|
||||
public void RecordRecomposeAllocation(long bytes) => _current.RecomposeAllocatedBytes += Math.Max(0, bytes);
|
||||
public void RecordSnapshotAllocation(long bytes) => _current.SnapshotAllocatedBytes += Math.Max(0, bytes);
|
||||
public void RecordCompositeAllocation(long bytes) => _current.CompositeAllocatedBytes += Math.Max(0, bytes);
|
||||
public void RecordSourcePrepAllocation(long bytes) => _current.SourcePrepAllocatedBytes += Math.Max(0, bytes);
|
||||
public void RecordSetDataAllocation(long bytes) => _current.SetDataAllocatedBytes += Math.Max(0, bytes);
|
||||
public void RecordUiAllocation(long bytes) => _current.UiAllocatedBytes += Math.Max(0, bytes);
|
||||
public void RecordSetData(long ticks) => _current.SetDataTicks += ticks;
|
||||
public void RecordTextureUpdate(long ticks) => _current.TextureUpdateTicks += ticks;
|
||||
|
||||
public void BeginRecomposite(bool screenTransition)
|
||||
{
|
||||
_current.Recomposited = true;
|
||||
_current.ScreenTransition |= screenTransition;
|
||||
_current.RecompositeStarted = Timestamp();
|
||||
}
|
||||
|
||||
public void RecordPresentationCoordinate(string script, int offset, int opcode)
|
||||
{
|
||||
_current.PresentScript = script;
|
||||
_current.PresentOffset = offset;
|
||||
_current.PresentOpcode = opcode;
|
||||
}
|
||||
|
||||
public void EndRecomposite()
|
||||
{
|
||||
if (_current.RecompositeStarted == 0) return;
|
||||
_current.RecompositeTicks += Timestamp() - _current.RecompositeStarted;
|
||||
_current.RecompositeStarted = 0;
|
||||
}
|
||||
|
||||
public void RecordObject(bool timeVarying)
|
||||
{
|
||||
_current.ObjectVisits++;
|
||||
if (timeVarying) _current.TimeVaryingObjects++;
|
||||
}
|
||||
|
||||
public void RecordFillLayer() => _current.FillLayers++;
|
||||
public void RecordTransitionLayer() => _current.TransitionLayers++;
|
||||
public void RecordSkippedLayer() => _current.SkippedLayers++;
|
||||
|
||||
public void RecordRaster(int sourceWidth, int sourceHeight, Affine2D localToDest,
|
||||
int destinationWidth, int destinationHeight, bool dynamic,
|
||||
BlendKind blend, long ticks)
|
||||
{
|
||||
_current.DrawLayers++;
|
||||
_current.RasterTicks += ticks;
|
||||
_current.SourcePixels += Math.Max(0L, (long)sourceWidth * sourceHeight);
|
||||
long candidates = EstimateCandidatePixels(localToDest, sourceWidth, sourceHeight,
|
||||
destinationWidth, destinationHeight);
|
||||
_current.CandidatePixels += candidates;
|
||||
if (candidates >= (long)destinationWidth * destinationHeight) _current.FullScreenLayers++;
|
||||
if (IsIntegerTranslation(localToDest)) _current.IntegerLayers++;
|
||||
else if (!localToDest.TryInverse(out _)) _current.SingularLayers++;
|
||||
else
|
||||
{
|
||||
_current.AffineLayers++;
|
||||
if (IsTranslation(localToDest)) _current.FractionalTranslationLayers++;
|
||||
else if (IsAxisAligned(localToDest)) _current.AxisAlignedScaleLayers++;
|
||||
else _current.GeneralAffineLayers++;
|
||||
}
|
||||
if (dynamic) _current.DynamicLayers++;
|
||||
switch (blend)
|
||||
{
|
||||
case BlendKind.Opaque: _current.OpaqueLayers++; break;
|
||||
case BlendKind.Additive: _current.AdditiveLayers++; break;
|
||||
default: _current.AlphaLayers++; break;
|
||||
}
|
||||
}
|
||||
|
||||
public void EndFrame()
|
||||
{
|
||||
if (!_frameOpen || _disposed) return;
|
||||
long ended = Timestamp();
|
||||
_current.MainTicks = ended - _current.Started;
|
||||
_current.AllocatedBytes = Math.Max(0, GC.GetAllocatedBytesForCurrentThread() - _current.AllocatedStart);
|
||||
_current.Gen0 = GC.CollectionCount(0) - _current.Gen0Start;
|
||||
_current.Gen1 = GC.CollectionCount(1) - _current.Gen1Start;
|
||||
_current.Gen2 = GC.CollectionCount(2) - _current.Gen2Start;
|
||||
Write(_current);
|
||||
FrameCount++;
|
||||
if (_current.Recomposited) RecompositeCount++;
|
||||
_frameOpen = false;
|
||||
if (++_framesSinceFlush >= FlushIntervalFrames)
|
||||
{
|
||||
_writer.Flush();
|
||||
_framesSinceFlush = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsIntegerTranslation(Affine2D m)
|
||||
=> IsTranslation(m) &&
|
||||
m.TX == Math.Truncate(m.TX) && m.TY == Math.Truncate(m.TY) &&
|
||||
m.TX >= int.MinValue && m.TX <= int.MaxValue &&
|
||||
m.TY >= int.MinValue && m.TY <= int.MaxValue;
|
||||
|
||||
public static bool IsTranslation(Affine2D m)
|
||||
=> m.XX == 1 && m.XY == 0 && m.YX == 0 && m.YY == 1;
|
||||
|
||||
public static bool IsAxisAligned(Affine2D m)
|
||||
=> m.XY == 0 && m.YX == 0;
|
||||
|
||||
public static long EstimateCandidatePixels(Affine2D m, int width, int height,
|
||||
int destinationWidth, int destinationHeight)
|
||||
{
|
||||
if (width <= 0 || height <= 0 || destinationWidth <= 0 || destinationHeight <= 0) return 0;
|
||||
var a = m.Apply(0, 0);
|
||||
var b = m.Apply(width, 0);
|
||||
var c = m.Apply(0, height);
|
||||
var d = m.Apply(width, height);
|
||||
double left = Math.Min(Math.Min(a.X, b.X), Math.Min(c.X, d.X));
|
||||
double top = Math.Min(Math.Min(a.Y, b.Y), Math.Min(c.Y, d.Y));
|
||||
double right = Math.Max(Math.Max(a.X, b.X), Math.Max(c.X, d.X));
|
||||
double bottom = Math.Max(Math.Max(a.Y, b.Y), Math.Max(c.Y, d.Y));
|
||||
long x0 = Math.Max(0, ClampFloor(left));
|
||||
long y0 = Math.Max(0, ClampFloor(top));
|
||||
long x1 = Math.Min(destinationWidth, ClampCeiling(right));
|
||||
long y1 = Math.Min(destinationHeight, ClampCeiling(bottom));
|
||||
return x1 <= x0 || y1 <= y0 ? 0 : checked((x1 - x0) * (y1 - y0));
|
||||
}
|
||||
|
||||
private static long ClampFloor(double value)
|
||||
=> !double.IsFinite(value) ? 0 : value <= long.MinValue ? long.MinValue
|
||||
: value >= long.MaxValue ? long.MaxValue : (long)Math.Floor(value);
|
||||
|
||||
private static long ClampCeiling(double value)
|
||||
=> !double.IsFinite(value) ? 0 : value <= long.MinValue ? long.MinValue
|
||||
: value >= long.MaxValue ? long.MaxValue : (long)Math.Ceiling(value);
|
||||
|
||||
private void Write(Frame f)
|
||||
{
|
||||
var b = new StringBuilder(512);
|
||||
Append(b, f.Number); Append(b, f.NowMs); Append(b, f.DeltaMs);
|
||||
AppendTicks(b, f.MainTicks); AppendTicks(b, f.PulseTicks); AppendTicks(b, f.MovieTicks);
|
||||
AppendTicks(b, f.ShouldTicks); AppendTicks(b, f.RecompositeTicks); AppendTicks(b, f.ClearTicks);
|
||||
AppendTicks(b, f.SnapshotTicks); AppendTicks(b, f.ResolveTicks); AppendTicks(b, f.SourcePrepTicks);
|
||||
AppendTicks(b, f.RasterTicks);
|
||||
AppendTicks(b, f.SetDataTicks); AppendTicks(b, f.TextureUpdateTicks); AppendTicks(b, f.UiTicks);
|
||||
Append(b, f.AllocatedBytes); Append(b, f.RecomposeAllocatedBytes);
|
||||
Append(b, f.SnapshotAllocatedBytes); Append(b, f.CompositeAllocatedBytes);
|
||||
Append(b, f.SourcePrepAllocatedBytes); Append(b, f.SetDataAllocatedBytes);
|
||||
Append(b, f.UiAllocatedBytes); Append(b, f.Gen0); Append(b, f.Gen1); Append(b, f.Gen2);
|
||||
Append(b, f.Recomposited ? 1 : 0); Append(b, f.ScreenTransition ? 1 : 0);
|
||||
Append(b, f.PresentHostRequest ? 1 : 0); Append(b, f.PresentScreenTransition ? 1 : 0);
|
||||
Append(b, f.PresentRetainedMutation ? 1 : 0); Append(b, f.PresentContinuousChannel ? 1 : 0);
|
||||
Append(b, f.PresentDiscreteCell ? 1 : 0);
|
||||
Append(b, f.ObjectVisits); Append(b, f.TimeVaryingObjects); Append(b, f.DrawLayers);
|
||||
Append(b, f.FillLayers); Append(b, f.TransitionLayers); Append(b, f.SkippedLayers);
|
||||
Append(b, f.IntegerLayers); Append(b, f.FractionalTranslationLayers);
|
||||
Append(b, f.AxisAlignedScaleLayers); Append(b, f.GeneralAffineLayers);
|
||||
Append(b, f.AffineLayers); Append(b, f.SingularLayers);
|
||||
Append(b, f.DynamicLayers); Append(b, f.OpaqueLayers); Append(b, f.AlphaLayers);
|
||||
Append(b, f.AdditiveLayers); Append(b, f.SourcePixels); Append(b, f.CandidatePixels);
|
||||
Append(b, f.FullScreenLayers); AppendEscaped(b, f.Script); Append(b, f.Offset);
|
||||
Append(b, f.Opcode); AppendEscaped(b, f.PresentScript); Append(b, f.PresentOffset);
|
||||
Append(b, f.PresentOpcode, last: true);
|
||||
_writer.WriteLine(b.ToString());
|
||||
}
|
||||
|
||||
private static void AppendTicks(StringBuilder b, long ticks)
|
||||
=> Append(b, ticks * MillisecondsPerTick);
|
||||
|
||||
private static void Append(StringBuilder b, long value, bool last = false)
|
||||
{
|
||||
b.Append(value.ToString(CultureInfo.InvariantCulture));
|
||||
if (!last) b.Append(',');
|
||||
}
|
||||
|
||||
private static void Append(StringBuilder b, double value)
|
||||
{
|
||||
b.Append(value.ToString("0.0000", CultureInfo.InvariantCulture));
|
||||
b.Append(',');
|
||||
}
|
||||
|
||||
private static void AppendEscaped(StringBuilder b, string value)
|
||||
{
|
||||
b.Append('"').Append(value.Replace("\"", "\"\"")).Append("\",");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
if (_frameOpen) EndFrame();
|
||||
_disposed = true;
|
||||
_writer.Dispose();
|
||||
}
|
||||
|
||||
private sealed class Frame
|
||||
{
|
||||
public int Number, Offset, Opcode;
|
||||
public long NowMs, Started, MainTicks, PulseTicks, MovieTicks, ShouldTicks, RecompositeTicks;
|
||||
public long RecompositeStarted, ClearTicks, SnapshotTicks, ResolveTicks, SourcePrepTicks, RasterTicks;
|
||||
public long SetDataTicks, TextureUpdateTicks, UiTicks, AllocatedStart, AllocatedBytes;
|
||||
public long RecomposeAllocatedBytes, SnapshotAllocatedBytes, CompositeAllocatedBytes;
|
||||
public long SourcePrepAllocatedBytes, SetDataAllocatedBytes, UiAllocatedBytes;
|
||||
public int Gen0Start, Gen1Start, Gen2Start, Gen0, Gen1, Gen2;
|
||||
public double DeltaMs;
|
||||
public string Script = "<unknown>";
|
||||
public string PresentScript = "<none>";
|
||||
public int PresentOffset = -1, PresentOpcode = -1;
|
||||
public bool Recomposited, ScreenTransition;
|
||||
public bool PresentHostRequest, PresentScreenTransition, PresentRetainedMutation;
|
||||
public bool PresentContinuousChannel, PresentDiscreteCell;
|
||||
public long ObjectVisits, TimeVaryingObjects, DrawLayers, FillLayers, TransitionLayers, SkippedLayers;
|
||||
public long IntegerLayers, FractionalTranslationLayers, AxisAlignedScaleLayers;
|
||||
public long GeneralAffineLayers, AffineLayers, SingularLayers, DynamicLayers;
|
||||
public long OpaqueLayers, AlphaLayers, AdditiveLayers, SourcePixels, CandidatePixels, FullScreenLayers;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user