Split Main compositor into partial class
This commit is contained in:
@@ -140,6 +140,8 @@ companions keep cohesive surfaces independently navigable without changing the G
|
|||||||
paths: `godot/Main.SelfTest.cs` owns the synthetic threaded/headless regression harness, while
|
paths: `godot/Main.SelfTest.cs` owns the synthetic threaded/headless regression harness, while
|
||||||
`godot/Main.Audio.cs` owns BGM, voice, sound-effect, mixer-routing/persistence, and audio-bus control, and
|
`godot/Main.Audio.cs` owns BGM, voice, sound-effect, mixer-routing/persistence, and audio-bus control, and
|
||||||
`godot/Main.Movie.cs` owns decoder staging, movie frame/audio publication, completion, and teardown.
|
`godot/Main.Movie.cs` owns decoder staging, movie frame/audio publication, completion, and teardown.
|
||||||
|
`godot/Main.Compositor.cs` owns retained/GPU/software composition state, texture resolution and caching,
|
||||||
|
surface-transition drawing, raster helpers, and compositor decision logging.
|
||||||
|
|
||||||
The disposable `build/page-map-<SCENE>.jsonl` files are produced by editor/development Godot runs and map
|
The disposable `build/page-map-<SCENE>.jsonl` files are produced by editor/development Godot runs and map
|
||||||
runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports
|
runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports
|
||||||
|
|||||||
@@ -559,12 +559,13 @@ do not mix mechanical moves with semantic changes.
|
|||||||
stage, battle, card, routine, gallery, and general table implementations plus tests into importable
|
stage, battle, card, routine, gallery, and general table implementations plus tests into importable
|
||||||
modules.
|
modules.
|
||||||
|
|
||||||
**Progress (2026-08-02):** the first three bounded splits moved `Main`'s synthetic scene builder/threaded
|
**Progress (2026-08-02):** the first four bounded splits moved `Main`'s synthetic scene builder/threaded
|
||||||
self-test harness into `godot/Main.SelfTest.cs`, then its BGM, voice, sound-effect, mixer, and bus-control
|
self-test harness into `godot/Main.SelfTest.cs`, then its BGM, voice, sound-effect, mixer, and bus-control
|
||||||
surface into `godot/Main.Audio.cs`, then decoder staging, movie frame/audio publication, completion, and
|
surface into `godot/Main.Audio.cs`, then decoder staging, movie frame/audio publication, completion, and
|
||||||
teardown into `godot/Main.Movie.cs`. The partial class retains the same node type, fields, signatures,
|
teardown into `godot/Main.Movie.cs`, then retained GPU/software composition, texture caching, transitions,
|
||||||
execution order, and call sites; runtime validation, including all 590 engine tests and the Godot threaded
|
raster helpers, and decision logging into `godot/Main.Compositor.cs`. The partial class retains the same
|
||||||
self-test, remains green after each move.
|
node type, fields, signatures, execution order, and call sites; runtime validation, including all 590
|
||||||
|
engine tests and the Godot threaded self-test, remains green after each move.
|
||||||
|
|
||||||
**Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where
|
**Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where
|
||||||
deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after
|
deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after
|
||||||
@@ -936,8 +937,8 @@ layer's rendering diverges from ADV; save layout.
|
|||||||
|
|
||||||
## 8. Immediate next step
|
## 8. Immediate next step
|
||||||
Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed
|
Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed
|
||||||
by the tracked launcher and layered validation driver. With the embedded `Main` self-test, audio, and movie
|
by the tracked launcher and layered validation driver. With the embedded `Main` self-test, audio, movie, and
|
||||||
surfaces isolated, move the retained compositor surface next, then one existing domain at a time while
|
retained compositor surfaces isolated, move input/cursor/debug-launcher handling next, then continue one
|
||||||
preserving public types, commands, and generated output.
|
existing domain at a time while preserving public types, commands, and generated output.
|
||||||
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does
|
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does
|
||||||
not replace Phase B gameplay validation or the open cross-platform gates.
|
not replace Phase B gameplay validation or the open cross-platform gates.
|
||||||
|
|||||||
576
godot/Main.Compositor.cs
Normal file
576
godot/Main.Compositor.cs
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Godot;
|
||||||
|
using Age.Engine.Model;
|
||||||
|
using Age.Engine.Sys4;
|
||||||
|
|
||||||
|
public partial class Main
|
||||||
|
{
|
||||||
|
private TextureRect _screenView = null!; // shows the composited screen backbuffer
|
||||||
|
private Image _screen = null!; // SYS4INI-sized immediate-mode canvas
|
||||||
|
private ImageTexture _screenTex = null!;
|
||||||
|
private GpuRetainedRenderer _gpuRenderer = null!;
|
||||||
|
private bool _useGpuBackend = true;
|
||||||
|
// One managed composition target for the entire frame. Layer helpers mutate it in place; only the
|
||||||
|
// completed frame crosses the Godot Image boundary, avoiding a full GetData/SetData round-trip per layer.
|
||||||
|
private byte[] _screenPixels = [];
|
||||||
|
|
||||||
|
private string? _gfxLogPath; // --gfx-log <file>: log per-object compositor draw/skip CHANGES
|
||||||
|
private System.IO.StreamWriter? _gfxLog;
|
||||||
|
private readonly System.Collections.Generic.Dictionary<long, string> _lastGfxDecision = new();
|
||||||
|
private int _gfxLogFrame;
|
||||||
|
|
||||||
|
// ---- retained per-frame compositor (main thread, from _Process) ----
|
||||||
|
// Clear the screen and composite the VM's current VISIBLE gfx objects in ascending-handle order (= the
|
||||||
|
// engine's z-order), each blitting its live surface's rect at its position. Decoded AGF pixels are cached
|
||||||
|
// by catalog identity (this runs every frame). Native scale/translation matrix channels are sampled independently by
|
||||||
|
// 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 void Recomposite()
|
||||||
|
{
|
||||||
|
bool gpuSnapshotCaptured = false;
|
||||||
|
BackbufferPublicationPolicy publicationPolicy = default;
|
||||||
|
if (_useGpuBackend &&
|
||||||
|
TryRecompositeGpu(out gpuSnapshotCaptured, out publicationPolicy)) return;
|
||||||
|
_gpuRenderer.Visible = false;
|
||||||
|
_screenView.Visible = true;
|
||||||
|
RecompositeSoftware(
|
||||||
|
gpuSnapshotCaptured ? _visibleSnapshot : null,
|
||||||
|
publicationPolicy.PreserveExistingPixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryRecompositeGpu(
|
||||||
|
out bool snapshotCaptured,
|
||||||
|
out BackbufferPublicationPolicy publicationPolicy)
|
||||||
|
{
|
||||||
|
snapshotCaptured = false;
|
||||||
|
publicationPolicy = default;
|
||||||
|
// Preserve the existing high-volume object/timeline diagnostics exactly. They are debugging tools,
|
||||||
|
// not performance workloads, and their software decision strings remain the canonical evidence.
|
||||||
|
if (_gfxLogPath != null || _timeline != null) return false;
|
||||||
|
|
||||||
|
long phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||||
|
long allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||||
|
if (_host.TrySnapshotScreenTransition(out _)) return false; // P4: whole-screen offscreen targets
|
||||||
|
publicationPolicy =
|
||||||
|
_host.SnapshotBackbufferObjects(_vm.Gfx, _clock.NowMs, _visibleSnapshot);
|
||||||
|
snapshotCaptured = true;
|
||||||
|
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||||
|
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
|
||||||
|
|
||||||
|
// Additive LERP-tint has not appeared in the target workloads and needs a dedicated additive shader
|
||||||
|
// variant before leaving the software oracle.
|
||||||
|
if (_visibleSnapshot.Any(v =>
|
||||||
|
v.Blend == BlendKind.Additive && !v.MultiplyTint && v.TintStrength > 0))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_perf != null)
|
||||||
|
{
|
||||||
|
var presentStep = _trace.LatestStep;
|
||||||
|
_perf.RecordPresentationCoordinate(presentStep?.Script ?? "<startup>",
|
||||||
|
presentStep?.Offset ?? -1, presentStep?.Opcode ?? -1);
|
||||||
|
}
|
||||||
|
_perf?.BeginRecomposite(screenTransition: false);
|
||||||
|
_gpuRenderer.BeginFrame(publicationPolicy.AppendGpuLayers);
|
||||||
|
foreach (var v in _visibleSnapshot)
|
||||||
|
{
|
||||||
|
_perf?.RecordObject(v.TimeVarying);
|
||||||
|
var affine = Transform2DMath.Build(v.Transform, v.Rotation, v.ScaleCycle)
|
||||||
|
.FromLocalOrigin(v.DstX, v.DstY);
|
||||||
|
if (v.RangeTransform is { } rangeTransform) affine = affine.Then(rangeTransform);
|
||||||
|
float opacity = v.Alpha / 255f;
|
||||||
|
var rawObject = _vm.Gfx.TryGet(v.Handle);
|
||||||
|
long resolveStarted = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||||
|
var texture = rawObject != null
|
||||||
|
? _host.ResolveSurfaceTexture(rawObject.SourceSlot, v.SurfaceResId)
|
||||||
|
: null;
|
||||||
|
_perf?.RecordResolve(PerformanceFrameLog.Timestamp() - resolveStarted);
|
||||||
|
bool movieSurfaceBound = rawObject != null && _host.IsMovieSurfaceBound(rawObject.SourceSlot);
|
||||||
|
|
||||||
|
if (v.SurfaceTransition is { } transition)
|
||||||
|
{
|
||||||
|
_perf?.RecordTransitionLayer();
|
||||||
|
DrawTransitionRangeGpu(_visibleSnapshot, transition);
|
||||||
|
}
|
||||||
|
else if (v.SurfaceResId == 0 && texture == null)
|
||||||
|
{
|
||||||
|
if (v.Blend != BlendKind.Opaque)
|
||||||
|
{
|
||||||
|
int width = v.W > 0 ? v.W : _screenWidth;
|
||||||
|
int height = v.H > 0 ? v.H : _screenHeight;
|
||||||
|
float fillOpacity = v.MultiplyTint
|
||||||
|
? opacity
|
||||||
|
: opacity * v.TintStrength / 255f;
|
||||||
|
_perf?.RecordFillLayer();
|
||||||
|
if (_gpuRenderer.DrawFill(width, height, affine, v.Tint, fillOpacity))
|
||||||
|
_perf?.RecordGpuLayer(width, height, affine, _screenWidth, _screenHeight,
|
||||||
|
dynamic: false, BlendKind.Alpha);
|
||||||
|
}
|
||||||
|
else _perf?.RecordSkippedLayer();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var resolved = texture.Value;
|
||||||
|
bool drawn = _gpuRenderer.DrawTexture(resolved.Image, resolved.AssetId, v.ColorKey,
|
||||||
|
v.SrcX, v.SrcY, v.W, v.H, affine, v.Tint, v.TintStrength,
|
||||||
|
opacity, v.MultiplyTint, resolved.IsDynamic,
|
||||||
|
rawObject?.SourceSlot ?? v.Handle, v.Blend);
|
||||||
|
if (drawn)
|
||||||
|
_perf?.RecordGpuLayer(v.W, v.H, affine, _screenWidth, _screenHeight,
|
||||||
|
resolved.IsDynamic, v.Blend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats = _gpuRenderer.EndFrame();
|
||||||
|
_perf?.RecordGpu(stats.DrawItems, stats.TextureUploads, stats.TextureUploadTicks);
|
||||||
|
_screenView.Visible = false;
|
||||||
|
_gpuRenderer.Visible = true;
|
||||||
|
_perf?.EndRecomposite();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native type-0 retained range transition: range A has already passed through ordinary z-order;
|
||||||
|
// republish range B at the transition placeholder with progress-scaled source opacity. This mirrors
|
||||||
|
// DrawTransitionRange's software-oracle order without allocating an offscreen CPU surface.
|
||||||
|
private int DrawTransitionRangeGpu(IReadOnlyList<RenderObject> visible, SurfaceTransitionState transition)
|
||||||
|
{
|
||||||
|
int drawn = 0;
|
||||||
|
long end = transition.RangeBStart + transition.RangeBCount;
|
||||||
|
foreach (var source in visible)
|
||||||
|
{
|
||||||
|
if (source.Handle < transition.RangeBStart || source.Handle >= end || source.SurfaceTransition != null)
|
||||||
|
continue;
|
||||||
|
_perf?.RecordObject(source.TimeVarying);
|
||||||
|
var affine = Transform2DMath.Build(source.Transform, source.Rotation, source.ScaleCycle)
|
||||||
|
.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)
|
||||||
|
{
|
||||||
|
_perf?.RecordSkippedLayer();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int width = source.W > 0 ? source.W : _screenWidth;
|
||||||
|
int height = source.H > 0 ? source.H : _screenHeight;
|
||||||
|
_perf?.RecordFillLayer();
|
||||||
|
if (_gpuRenderer.DrawFill(width, height, affine, source.Tint,
|
||||||
|
opacity * source.TintStrength / 255f))
|
||||||
|
{
|
||||||
|
_perf?.RecordGpuLayer(width, height, affine, _screenWidth, _screenHeight,
|
||||||
|
dynamic: false, BlendKind.Alpha);
|
||||||
|
drawn++;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
var resolved = texture.Value;
|
||||||
|
if (_gpuRenderer.DrawTexture(resolved.Image, resolved.AssetId, source.ColorKey,
|
||||||
|
source.SrcX, source.SrcY, source.W, source.H, affine, source.Tint, source.TintStrength,
|
||||||
|
opacity, source.MultiplyTint, resolved.IsDynamic,
|
||||||
|
rawObject?.SourceSlot ?? source.Handle, source.Blend))
|
||||||
|
{
|
||||||
|
_perf?.RecordGpuLayer(source.W, source.H, affine, _screenWidth, _screenHeight,
|
||||||
|
resolved.IsDynamic, source.Blend);
|
||||||
|
drawn++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return drawn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecompositeSoftware(
|
||||||
|
IReadOnlyList<RenderObject>? sampledVisible = null,
|
||||||
|
bool preserveExistingPixels = false)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
if (!hasScreenTransition && sampledVisible == null)
|
||||||
|
{
|
||||||
|
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||||
|
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||||
|
preserveExistingPixels = _host.SnapshotBackbufferObjects(
|
||||||
|
_vm.Gfx, _clock.NowMs, _visibleSnapshot).PreserveExistingPixels;
|
||||||
|
_perf?.RecordSnapshotAllocation(
|
||||||
|
PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||||
|
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
|
||||||
|
}
|
||||||
|
if (hasScreenTransition) preserveExistingPixels = false;
|
||||||
|
_perf?.BeginRecomposite(hasScreenTransition);
|
||||||
|
|
||||||
|
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
||||||
|
if (!preserveExistingPixels)
|
||||||
|
{
|
||||||
|
System.Array.Clear(_screenPixels);
|
||||||
|
}
|
||||||
|
_perf?.RecordClear(PerformanceFrameLog.Timestamp() - phase);
|
||||||
|
System.Collections.Generic.Dictionary<long, string>? decisions = _gfxLogPath != null || _timeline != null ? new() : null;
|
||||||
|
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, decisions);
|
||||||
|
FillQuad(0, 0, _screenWidth, _screenHeight, 0, (float)transition.Progress);
|
||||||
|
CompositeVisibleObjects(transition.Target, (float)transition.Progress, decisions);
|
||||||
|
_perf?.RecordCompositeAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
||||||
|
CompositeVisibleObjects(sampledVisible ?? _visibleSnapshot, 1f, decisions);
|
||||||
|
_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,
|
||||||
|
System.Collections.Generic.Dictionary<long, string>? decisions)
|
||||||
|
{
|
||||||
|
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, v.ScaleCycle);
|
||||||
|
var localToDest = affine.FromLocalOrigin(v.DstX, v.DstY);
|
||||||
|
if (v.RangeTransform is { } rangeTransform)
|
||||||
|
localToDest = localToDest.Then(rangeTransform);
|
||||||
|
var projected = localToDest.Apply(0, 0);
|
||||||
|
int dstX = (int)System.Math.Round(projected.X);
|
||||||
|
int dstY = (int)System.Math.Round(projected.Y);
|
||||||
|
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);
|
||||||
|
// 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);
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
// A colored object with no bound surface = a fade/flash fill (e.g. fade-to-black). Its presence
|
||||||
|
// is the tint STRENGTH (0=absent, 255=solid), scaled by any object opacity. Uncolored surfaceless
|
||||||
|
// objects are render targets — still skipped (slice C).
|
||||||
|
if (v.Blend != Age.Engine.Model.BlendKind.Opaque)
|
||||||
|
{
|
||||||
|
int baseW = v.W > 0 ? v.W : _screenWidth;
|
||||||
|
int baseH = v.H > 0 ? v.H : _screenHeight;
|
||||||
|
// 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);
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
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}";
|
||||||
|
z++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ColorTimeline(Age.Engine.Model.ColorTransitionState? state)
|
||||||
|
=> state is { } c
|
||||||
|
? $" color=0x{c.Current:x8}->0x{c.Target:x8} colorProgress={c.Progress:0.000}"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
// Native type-0 surface commands first leave range A in normal z-order, then alpha-composite range B
|
||||||
|
// into the target surface. SC0000 binds that target to handle+2, above both source handles, so drawing
|
||||||
|
// range B here with progress produces old*(1-progress)+new*progress without disturbing ambient channels.
|
||||||
|
private int DrawTransitionRange(IReadOnlyList<RenderObject> visible, SurfaceTransitionState transition)
|
||||||
|
{
|
||||||
|
int drawn = 0;
|
||||||
|
long end = transition.RangeBStart + transition.RangeBCount;
|
||||||
|
foreach (var source in visible)
|
||||||
|
{
|
||||||
|
if (source.Handle < transition.RangeBStart || source.Handle >= end || source.SurfaceTransition != null)
|
||||||
|
continue;
|
||||||
|
_perf?.RecordObject(source.TimeVarying);
|
||||||
|
var affine = Transform2DMath.Build(source.Transform, source.Rotation, source.ScaleCycle)
|
||||||
|
.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)
|
||||||
|
{
|
||||||
|
_perf?.RecordSkippedLayer();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int w = source.W > 0 ? source.W : _screenWidth;
|
||||||
|
int h = source.H > 0 ? source.H : _screenHeight;
|
||||||
|
_perf?.RecordFillLayer();
|
||||||
|
FillAffineQuad(w, h, affine, source.Tint, opacity * source.TintStrength / 255f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
drawn++;
|
||||||
|
}
|
||||||
|
return drawn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diagnostic (--gfx-log): print, per rendered frame, only the objects whose compositor outcome CHANGED
|
||||||
|
// since last frame (added / gone / drawn↔skip / resId change). Quiet until something actually changes, so
|
||||||
|
// the frame where the background drops out — and WHY — stands out. See systematic-debugging of the grey-BG.
|
||||||
|
private void LogGfxDecisionChanges(System.Collections.Generic.Dictionary<long, string> curr)
|
||||||
|
{
|
||||||
|
if (_gfxLogPath != null && _gfxLog == null)
|
||||||
|
{
|
||||||
|
var dir = System.IO.Path.GetDirectoryName(_gfxLogPath);
|
||||||
|
if (!string.IsNullOrEmpty(dir)) System.IO.Directory.CreateDirectory(dir);
|
||||||
|
_gfxLog = new System.IO.StreamWriter(_gfxLogPath!) { AutoFlush = true };
|
||||||
|
}
|
||||||
|
_gfxLogFrame++;
|
||||||
|
var lines = new System.Collections.Generic.List<string>();
|
||||||
|
foreach (var kv in curr)
|
||||||
|
if (!_lastGfxDecision.TryGetValue(kv.Key, out var prev) || prev != kv.Value)
|
||||||
|
lines.Add($" 0x{kv.Key:x}: {kv.Value}" + (_lastGfxDecision.ContainsKey(kv.Key) ? "" : " [NEW]"));
|
||||||
|
foreach (var kv in _lastGfxDecision)
|
||||||
|
if (!curr.ContainsKey(kv.Key))
|
||||||
|
lines.Add($" 0x{kv.Key:x}: GONE (was {kv.Value})");
|
||||||
|
if (lines.Count > 0 && _gfxLog != null)
|
||||||
|
{
|
||||||
|
_gfxLog.WriteLine($"[frame {_gfxLogFrame} nowMs={_clock.NowMs} page={_pageCount}] {curr.Count} visible, {lines.Count} changes:");
|
||||||
|
foreach (var l in lines) _gfxLog.WriteLine(l);
|
||||||
|
}
|
||||||
|
if (lines.Count > 0)
|
||||||
|
_timeline?.Event("objects", new() { ["visible_count"] = curr.Count, ["changes"] = lines.ToArray() });
|
||||||
|
_lastGfxDecision.Clear();
|
||||||
|
foreach (var kv in curr) _lastGfxDecision[kv.Key] = kv.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blit one object's surface rect. Static source pixels are cached per (assetId, colorKey): on first use, texels
|
||||||
|
// matching the surface colorkey are made transparent (native bakes the key at load — engine-re.md §Blend).
|
||||||
|
// Mode 0 uses tintStrength to LERP texel RGB toward tint. Mode 1 uses packed RGB modulation and
|
||||||
|
// SRCALPHA/ONE additive composition; black source pixels therefore contribute nothing.
|
||||||
|
private void BlitLayer(RgbaImage decoded, int assetId, long colorKey, long tint, float tintStrength, int srcX, int srcY, int w, int h,
|
||||||
|
Age.Engine.Model.Affine2D localToDest, float alpha = 1f, bool multiplyTint = false,
|
||||||
|
bool dynamic = false, BlendKind blend = BlendKind.Alpha)
|
||||||
|
{
|
||||||
|
// Native gfx_object_blit_d3d9 clips the explicit source rectangle and returns without drawing when
|
||||||
|
// 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;
|
||||||
|
if (dynamic)
|
||||||
|
{
|
||||||
|
// Decoder samples replace the pixels of one retained surface. Never enter them in the static cache.
|
||||||
|
// Clone only when applying a key so the decoder-owned newest-frame buffer remains untouched.
|
||||||
|
sourceWidth = decoded.Width;
|
||||||
|
sourceHeight = decoded.Height;
|
||||||
|
sourcePixels = decoded.Pixels;
|
||||||
|
if (Age.Engine.Model.BlendMath.HasColorKey(colorKey))
|
||||||
|
{
|
||||||
|
sourcePixels = (byte[])sourcePixels.Clone();
|
||||||
|
BakeColorKey(sourcePixels, colorKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!_pixelCache.TryGetValue(cacheKey, out var cached))
|
||||||
|
{
|
||||||
|
byte[] pixels = decoded.Pixels;
|
||||||
|
if (Age.Engine.Model.BlendMath.HasColorKey(colorKey))
|
||||||
|
{
|
||||||
|
pixels = (byte[])pixels.Clone();
|
||||||
|
BakeColorKey(pixels, colorKey);
|
||||||
|
}
|
||||||
|
cached = new CachedPixels(decoded.Width, decoded.Height, pixels);
|
||||||
|
_pixelCache[cacheKey] = cached;
|
||||||
|
}
|
||||||
|
sourceWidth = cached.Width;
|
||||||
|
sourceHeight = cached.Height;
|
||||||
|
sourcePixels = cached.Rgba;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sw = w;
|
||||||
|
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.
|
||||||
|
private void FillQuad(int dstX, int dstY, int w, int h, long tint, float alpha)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
int x0 = System.Math.Max(0, -dstX), x1 = System.Math.Min(w, dw - dstX);
|
||||||
|
int y0 = System.Math.Max(0, -dstY), y1 = System.Math.Min(h, dh - dstY);
|
||||||
|
if (x1 <= x0 || y1 <= y0) return;
|
||||||
|
for (int y = y0; y < y1; y++)
|
||||||
|
for (int x = x0; x < x1; x++)
|
||||||
|
{
|
||||||
|
int dxp = dstX + x, dyp = dstY + y;
|
||||||
|
int di = (dyp * dw + dxp) * 4;
|
||||||
|
dst[di] = (byte)((tr * ia + dst[di] * (255 - ia)) / 255);
|
||||||
|
dst[di + 1] = (byte)((tg * ia + dst[di + 1] * (255 - ia)) / 255);
|
||||||
|
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).
|
||||||
|
private static void BakeColorKey(byte[] px, long colorKey)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < px.Length; i += 4)
|
||||||
|
if (Age.Engine.Model.BlendMath.ColorKeyMatches(px[i], px[i + 1], px[i + 2], colorKey))
|
||||||
|
px[i + 3] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
565
godot/Main.cs
565
godot/Main.cs
@@ -25,15 +25,7 @@ public partial class Main : Godot.Control
|
|||||||
private WindowLaunchOptions _windowOptions;
|
private WindowLaunchOptions _windowOptions;
|
||||||
private Sys4AssetCatalog _catalog = null!;
|
private Sys4AssetCatalog _catalog = null!;
|
||||||
private IAssetStore _assetStore = null!;
|
private IAssetStore _assetStore = null!;
|
||||||
private TextureRect _screenView = null!; // shows the composited screen backbuffer
|
|
||||||
private Image _screen = null!; // SYS4INI-sized immediate-mode canvas
|
|
||||||
private ImageTexture _screenTex = null!;
|
|
||||||
private GpuRetainedRenderer _gpuRenderer = null!;
|
|
||||||
private bool _useGpuBackend = true;
|
|
||||||
private ImageTexture? _ageCursorTexture;
|
private ImageTexture? _ageCursorTexture;
|
||||||
// One managed composition target for the entire frame. Layer helpers mutate it in place; only the
|
|
||||||
// completed frame crosses the Godot Image boundary, avoiding a full GetData/SetData round-trip per layer.
|
|
||||||
private byte[] _screenPixels = [];
|
|
||||||
private Label _status = null!;
|
private Label _status = null!;
|
||||||
private Label _locatorHud = null!;
|
private Label _locatorHud = null!;
|
||||||
private VirtualMachine _vm = null!;
|
private VirtualMachine _vm = null!;
|
||||||
@@ -65,10 +57,6 @@ public partial class Main : Godot.Control
|
|||||||
private string? _seqDir; // --shot-sequence <dir>: dump one PNG per frame (verify paced anim)
|
private string? _seqDir; // --shot-sequence <dir>: dump one PNG per frame (verify paced anim)
|
||||||
private int _seqFrames = 180; // --frames <n>: how many frames to dump (default ~3s @60fps)
|
private int _seqFrames = 180; // --frames <n>: how many frames to dump (default ~3s @60fps)
|
||||||
private int _seqIdx;
|
private int _seqIdx;
|
||||||
private string? _gfxLogPath; // --gfx-log <file>: log per-object compositor draw/skip CHANGES
|
|
||||||
private System.IO.StreamWriter? _gfxLog;
|
|
||||||
private readonly System.Collections.Generic.Dictionary<long, string> _lastGfxDecision = new();
|
|
||||||
private int _gfxLogFrame;
|
|
||||||
private string? _timelineLogPath; // --timeline-log <jsonl>: synchronized VM/host/compositor evidence
|
private string? _timelineLogPath; // --timeline-log <jsonl>: synchronized VM/host/compositor evidence
|
||||||
private GodotTimelineLog? _timeline;
|
private GodotTimelineLog? _timeline;
|
||||||
private int _timelineFrame;
|
private int _timelineFrame;
|
||||||
@@ -1046,559 +1034,6 @@ public partial class Main : Godot.Control
|
|||||||
catch (System.Exception e) { GD.Print($"[trace-histogram] write failed: {e.Message}"); }
|
catch (System.Exception e) { GD.Print($"[trace-histogram] write failed: {e.Message}"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- retained per-frame compositor (main thread, from _Process) ----
|
|
||||||
// Clear the screen and composite the VM's current VISIBLE gfx objects in ascending-handle order (= the
|
|
||||||
// engine's z-order), each blitting its live surface's rect at its position. Decoded AGF pixels are cached
|
|
||||||
// by catalog identity (this runs every frame). Native scale/translation matrix channels are sampled independently by
|
|
||||||
// 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 void Recomposite()
|
|
||||||
{
|
|
||||||
bool gpuSnapshotCaptured = false;
|
|
||||||
BackbufferPublicationPolicy publicationPolicy = default;
|
|
||||||
if (_useGpuBackend &&
|
|
||||||
TryRecompositeGpu(out gpuSnapshotCaptured, out publicationPolicy)) return;
|
|
||||||
_gpuRenderer.Visible = false;
|
|
||||||
_screenView.Visible = true;
|
|
||||||
RecompositeSoftware(
|
|
||||||
gpuSnapshotCaptured ? _visibleSnapshot : null,
|
|
||||||
publicationPolicy.PreserveExistingPixels);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryRecompositeGpu(
|
|
||||||
out bool snapshotCaptured,
|
|
||||||
out BackbufferPublicationPolicy publicationPolicy)
|
|
||||||
{
|
|
||||||
snapshotCaptured = false;
|
|
||||||
publicationPolicy = default;
|
|
||||||
// Preserve the existing high-volume object/timeline diagnostics exactly. They are debugging tools,
|
|
||||||
// not performance workloads, and their software decision strings remain the canonical evidence.
|
|
||||||
if (_gfxLogPath != null || _timeline != null) return false;
|
|
||||||
|
|
||||||
long phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
|
||||||
long allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
|
||||||
if (_host.TrySnapshotScreenTransition(out _)) return false; // P4: whole-screen offscreen targets
|
|
||||||
publicationPolicy =
|
|
||||||
_host.SnapshotBackbufferObjects(_vm.Gfx, _clock.NowMs, _visibleSnapshot);
|
|
||||||
snapshotCaptured = true;
|
|
||||||
_perf?.RecordSnapshotAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
|
||||||
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
|
|
||||||
|
|
||||||
// Additive LERP-tint has not appeared in the target workloads and needs a dedicated additive shader
|
|
||||||
// variant before leaving the software oracle.
|
|
||||||
if (_visibleSnapshot.Any(v =>
|
|
||||||
v.Blend == BlendKind.Additive && !v.MultiplyTint && v.TintStrength > 0))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_perf != null)
|
|
||||||
{
|
|
||||||
var presentStep = _trace.LatestStep;
|
|
||||||
_perf.RecordPresentationCoordinate(presentStep?.Script ?? "<startup>",
|
|
||||||
presentStep?.Offset ?? -1, presentStep?.Opcode ?? -1);
|
|
||||||
}
|
|
||||||
_perf?.BeginRecomposite(screenTransition: false);
|
|
||||||
_gpuRenderer.BeginFrame(publicationPolicy.AppendGpuLayers);
|
|
||||||
foreach (var v in _visibleSnapshot)
|
|
||||||
{
|
|
||||||
_perf?.RecordObject(v.TimeVarying);
|
|
||||||
var affine = Transform2DMath.Build(v.Transform, v.Rotation, v.ScaleCycle)
|
|
||||||
.FromLocalOrigin(v.DstX, v.DstY);
|
|
||||||
if (v.RangeTransform is { } rangeTransform) affine = affine.Then(rangeTransform);
|
|
||||||
float opacity = v.Alpha / 255f;
|
|
||||||
var rawObject = _vm.Gfx.TryGet(v.Handle);
|
|
||||||
long resolveStarted = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
|
||||||
var texture = rawObject != null
|
|
||||||
? _host.ResolveSurfaceTexture(rawObject.SourceSlot, v.SurfaceResId)
|
|
||||||
: null;
|
|
||||||
_perf?.RecordResolve(PerformanceFrameLog.Timestamp() - resolveStarted);
|
|
||||||
bool movieSurfaceBound = rawObject != null && _host.IsMovieSurfaceBound(rawObject.SourceSlot);
|
|
||||||
|
|
||||||
if (v.SurfaceTransition is { } transition)
|
|
||||||
{
|
|
||||||
_perf?.RecordTransitionLayer();
|
|
||||||
DrawTransitionRangeGpu(_visibleSnapshot, transition);
|
|
||||||
}
|
|
||||||
else if (v.SurfaceResId == 0 && texture == null)
|
|
||||||
{
|
|
||||||
if (v.Blend != BlendKind.Opaque)
|
|
||||||
{
|
|
||||||
int width = v.W > 0 ? v.W : _screenWidth;
|
|
||||||
int height = v.H > 0 ? v.H : _screenHeight;
|
|
||||||
float fillOpacity = v.MultiplyTint
|
|
||||||
? opacity
|
|
||||||
: opacity * v.TintStrength / 255f;
|
|
||||||
_perf?.RecordFillLayer();
|
|
||||||
if (_gpuRenderer.DrawFill(width, height, affine, v.Tint, fillOpacity))
|
|
||||||
_perf?.RecordGpuLayer(width, height, affine, _screenWidth, _screenHeight,
|
|
||||||
dynamic: false, BlendKind.Alpha);
|
|
||||||
}
|
|
||||||
else _perf?.RecordSkippedLayer();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var resolved = texture.Value;
|
|
||||||
bool drawn = _gpuRenderer.DrawTexture(resolved.Image, resolved.AssetId, v.ColorKey,
|
|
||||||
v.SrcX, v.SrcY, v.W, v.H, affine, v.Tint, v.TintStrength,
|
|
||||||
opacity, v.MultiplyTint, resolved.IsDynamic,
|
|
||||||
rawObject?.SourceSlot ?? v.Handle, v.Blend);
|
|
||||||
if (drawn)
|
|
||||||
_perf?.RecordGpuLayer(v.W, v.H, affine, _screenWidth, _screenHeight,
|
|
||||||
resolved.IsDynamic, v.Blend);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var stats = _gpuRenderer.EndFrame();
|
|
||||||
_perf?.RecordGpu(stats.DrawItems, stats.TextureUploads, stats.TextureUploadTicks);
|
|
||||||
_screenView.Visible = false;
|
|
||||||
_gpuRenderer.Visible = true;
|
|
||||||
_perf?.EndRecomposite();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Native type-0 retained range transition: range A has already passed through ordinary z-order;
|
|
||||||
// republish range B at the transition placeholder with progress-scaled source opacity. This mirrors
|
|
||||||
// DrawTransitionRange's software-oracle order without allocating an offscreen CPU surface.
|
|
||||||
private int DrawTransitionRangeGpu(IReadOnlyList<RenderObject> visible, SurfaceTransitionState transition)
|
|
||||||
{
|
|
||||||
int drawn = 0;
|
|
||||||
long end = transition.RangeBStart + transition.RangeBCount;
|
|
||||||
foreach (var source in visible)
|
|
||||||
{
|
|
||||||
if (source.Handle < transition.RangeBStart || source.Handle >= end || source.SurfaceTransition != null)
|
|
||||||
continue;
|
|
||||||
_perf?.RecordObject(source.TimeVarying);
|
|
||||||
var affine = Transform2DMath.Build(source.Transform, source.Rotation, source.ScaleCycle)
|
|
||||||
.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)
|
|
||||||
{
|
|
||||||
_perf?.RecordSkippedLayer();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int width = source.W > 0 ? source.W : _screenWidth;
|
|
||||||
int height = source.H > 0 ? source.H : _screenHeight;
|
|
||||||
_perf?.RecordFillLayer();
|
|
||||||
if (_gpuRenderer.DrawFill(width, height, affine, source.Tint,
|
|
||||||
opacity * source.TintStrength / 255f))
|
|
||||||
{
|
|
||||||
_perf?.RecordGpuLayer(width, height, affine, _screenWidth, _screenHeight,
|
|
||||||
dynamic: false, BlendKind.Alpha);
|
|
||||||
drawn++;
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
var resolved = texture.Value;
|
|
||||||
if (_gpuRenderer.DrawTexture(resolved.Image, resolved.AssetId, source.ColorKey,
|
|
||||||
source.SrcX, source.SrcY, source.W, source.H, affine, source.Tint, source.TintStrength,
|
|
||||||
opacity, source.MultiplyTint, resolved.IsDynamic,
|
|
||||||
rawObject?.SourceSlot ?? source.Handle, source.Blend))
|
|
||||||
{
|
|
||||||
_perf?.RecordGpuLayer(source.W, source.H, affine, _screenWidth, _screenHeight,
|
|
||||||
resolved.IsDynamic, source.Blend);
|
|
||||||
drawn++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return drawn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RecompositeSoftware(
|
|
||||||
IReadOnlyList<RenderObject>? sampledVisible = null,
|
|
||||||
bool preserveExistingPixels = false)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
if (!hasScreenTransition && sampledVisible == null)
|
|
||||||
{
|
|
||||||
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
|
||||||
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
|
||||||
preserveExistingPixels = _host.SnapshotBackbufferObjects(
|
|
||||||
_vm.Gfx, _clock.NowMs, _visibleSnapshot).PreserveExistingPixels;
|
|
||||||
_perf?.RecordSnapshotAllocation(
|
|
||||||
PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
|
||||||
_perf?.RecordSnapshot(PerformanceFrameLog.Timestamp() - phase);
|
|
||||||
}
|
|
||||||
if (hasScreenTransition) preserveExistingPixels = false;
|
|
||||||
_perf?.BeginRecomposite(hasScreenTransition);
|
|
||||||
|
|
||||||
phase = _perf != null ? PerformanceFrameLog.Timestamp() : 0;
|
|
||||||
if (!preserveExistingPixels)
|
|
||||||
{
|
|
||||||
System.Array.Clear(_screenPixels);
|
|
||||||
}
|
|
||||||
_perf?.RecordClear(PerformanceFrameLog.Timestamp() - phase);
|
|
||||||
System.Collections.Generic.Dictionary<long, string>? decisions = _gfxLogPath != null || _timeline != null ? new() : null;
|
|
||||||
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, decisions);
|
|
||||||
FillQuad(0, 0, _screenWidth, _screenHeight, 0, (float)transition.Progress);
|
|
||||||
CompositeVisibleObjects(transition.Target, (float)transition.Progress, decisions);
|
|
||||||
_perf?.RecordCompositeAllocation(PerformanceFrameLog.AllocatedBytes() - allocationPhase);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
allocationPhase = _perf != null ? PerformanceFrameLog.AllocatedBytes() : 0;
|
|
||||||
CompositeVisibleObjects(sampledVisible ?? _visibleSnapshot, 1f, decisions);
|
|
||||||
_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,
|
|
||||||
System.Collections.Generic.Dictionary<long, string>? decisions)
|
|
||||||
{
|
|
||||||
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, v.ScaleCycle);
|
|
||||||
var localToDest = affine.FromLocalOrigin(v.DstX, v.DstY);
|
|
||||||
if (v.RangeTransform is { } rangeTransform)
|
|
||||||
localToDest = localToDest.Then(rangeTransform);
|
|
||||||
var projected = localToDest.Apply(0, 0);
|
|
||||||
int dstX = (int)System.Math.Round(projected.X);
|
|
||||||
int dstY = (int)System.Math.Round(projected.Y);
|
|
||||||
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);
|
|
||||||
// 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);
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
// A colored object with no bound surface = a fade/flash fill (e.g. fade-to-black). Its presence
|
|
||||||
// is the tint STRENGTH (0=absent, 255=solid), scaled by any object opacity. Uncolored surfaceless
|
|
||||||
// objects are render targets — still skipped (slice C).
|
|
||||||
if (v.Blend != Age.Engine.Model.BlendKind.Opaque)
|
|
||||||
{
|
|
||||||
int baseW = v.W > 0 ? v.W : _screenWidth;
|
|
||||||
int baseH = v.H > 0 ? v.H : _screenHeight;
|
|
||||||
// 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);
|
|
||||||
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
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
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}";
|
|
||||||
z++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ColorTimeline(Age.Engine.Model.ColorTransitionState? state)
|
|
||||||
=> state is { } c
|
|
||||||
? $" color=0x{c.Current:x8}->0x{c.Target:x8} colorProgress={c.Progress:0.000}"
|
|
||||||
: "";
|
|
||||||
|
|
||||||
// Native type-0 surface commands first leave range A in normal z-order, then alpha-composite range B
|
|
||||||
// into the target surface. SC0000 binds that target to handle+2, above both source handles, so drawing
|
|
||||||
// range B here with progress produces old*(1-progress)+new*progress without disturbing ambient channels.
|
|
||||||
private int DrawTransitionRange(IReadOnlyList<RenderObject> visible, SurfaceTransitionState transition)
|
|
||||||
{
|
|
||||||
int drawn = 0;
|
|
||||||
long end = transition.RangeBStart + transition.RangeBCount;
|
|
||||||
foreach (var source in visible)
|
|
||||||
{
|
|
||||||
if (source.Handle < transition.RangeBStart || source.Handle >= end || source.SurfaceTransition != null)
|
|
||||||
continue;
|
|
||||||
_perf?.RecordObject(source.TimeVarying);
|
|
||||||
var affine = Transform2DMath.Build(source.Transform, source.Rotation, source.ScaleCycle)
|
|
||||||
.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)
|
|
||||||
{
|
|
||||||
_perf?.RecordSkippedLayer();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int w = source.W > 0 ? source.W : _screenWidth;
|
|
||||||
int h = source.H > 0 ? source.H : _screenHeight;
|
|
||||||
_perf?.RecordFillLayer();
|
|
||||||
FillAffineQuad(w, h, affine, source.Tint, opacity * source.TintStrength / 255f);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
drawn++;
|
|
||||||
}
|
|
||||||
return drawn;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Diagnostic (--gfx-log): print, per rendered frame, only the objects whose compositor outcome CHANGED
|
|
||||||
// since last frame (added / gone / drawn↔skip / resId change). Quiet until something actually changes, so
|
|
||||||
// the frame where the background drops out — and WHY — stands out. See systematic-debugging of the grey-BG.
|
|
||||||
private void LogGfxDecisionChanges(System.Collections.Generic.Dictionary<long, string> curr)
|
|
||||||
{
|
|
||||||
if (_gfxLogPath != null && _gfxLog == null)
|
|
||||||
{
|
|
||||||
var dir = System.IO.Path.GetDirectoryName(_gfxLogPath);
|
|
||||||
if (!string.IsNullOrEmpty(dir)) System.IO.Directory.CreateDirectory(dir);
|
|
||||||
_gfxLog = new System.IO.StreamWriter(_gfxLogPath!) { AutoFlush = true };
|
|
||||||
}
|
|
||||||
_gfxLogFrame++;
|
|
||||||
var lines = new System.Collections.Generic.List<string>();
|
|
||||||
foreach (var kv in curr)
|
|
||||||
if (!_lastGfxDecision.TryGetValue(kv.Key, out var prev) || prev != kv.Value)
|
|
||||||
lines.Add($" 0x{kv.Key:x}: {kv.Value}" + (_lastGfxDecision.ContainsKey(kv.Key) ? "" : " [NEW]"));
|
|
||||||
foreach (var kv in _lastGfxDecision)
|
|
||||||
if (!curr.ContainsKey(kv.Key))
|
|
||||||
lines.Add($" 0x{kv.Key:x}: GONE (was {kv.Value})");
|
|
||||||
if (lines.Count > 0 && _gfxLog != null)
|
|
||||||
{
|
|
||||||
_gfxLog.WriteLine($"[frame {_gfxLogFrame} nowMs={_clock.NowMs} page={_pageCount}] {curr.Count} visible, {lines.Count} changes:");
|
|
||||||
foreach (var l in lines) _gfxLog.WriteLine(l);
|
|
||||||
}
|
|
||||||
if (lines.Count > 0)
|
|
||||||
_timeline?.Event("objects", new() { ["visible_count"] = curr.Count, ["changes"] = lines.ToArray() });
|
|
||||||
_lastGfxDecision.Clear();
|
|
||||||
foreach (var kv in curr) _lastGfxDecision[kv.Key] = kv.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blit one object's surface rect. Static source pixels are cached per (assetId, colorKey): on first use, texels
|
|
||||||
// matching the surface colorkey are made transparent (native bakes the key at load — engine-re.md §Blend).
|
|
||||||
// Mode 0 uses tintStrength to LERP texel RGB toward tint. Mode 1 uses packed RGB modulation and
|
|
||||||
// SRCALPHA/ONE additive composition; black source pixels therefore contribute nothing.
|
|
||||||
private void BlitLayer(RgbaImage decoded, int assetId, long colorKey, long tint, float tintStrength, int srcX, int srcY, int w, int h,
|
|
||||||
Age.Engine.Model.Affine2D localToDest, float alpha = 1f, bool multiplyTint = false,
|
|
||||||
bool dynamic = false, BlendKind blend = BlendKind.Alpha)
|
|
||||||
{
|
|
||||||
// Native gfx_object_blit_d3d9 clips the explicit source rectangle and returns without drawing when
|
|
||||||
// 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;
|
|
||||||
if (dynamic)
|
|
||||||
{
|
|
||||||
// Decoder samples replace the pixels of one retained surface. Never enter them in the static cache.
|
|
||||||
// Clone only when applying a key so the decoder-owned newest-frame buffer remains untouched.
|
|
||||||
sourceWidth = decoded.Width;
|
|
||||||
sourceHeight = decoded.Height;
|
|
||||||
sourcePixels = decoded.Pixels;
|
|
||||||
if (Age.Engine.Model.BlendMath.HasColorKey(colorKey))
|
|
||||||
{
|
|
||||||
sourcePixels = (byte[])sourcePixels.Clone();
|
|
||||||
BakeColorKey(sourcePixels, colorKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!_pixelCache.TryGetValue(cacheKey, out var cached))
|
|
||||||
{
|
|
||||||
byte[] pixels = decoded.Pixels;
|
|
||||||
if (Age.Engine.Model.BlendMath.HasColorKey(colorKey))
|
|
||||||
{
|
|
||||||
pixels = (byte[])pixels.Clone();
|
|
||||||
BakeColorKey(pixels, colorKey);
|
|
||||||
}
|
|
||||||
cached = new CachedPixels(decoded.Width, decoded.Height, pixels);
|
|
||||||
_pixelCache[cacheKey] = cached;
|
|
||||||
}
|
|
||||||
sourceWidth = cached.Width;
|
|
||||||
sourceHeight = cached.Height;
|
|
||||||
sourcePixels = cached.Rgba;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sw = w;
|
|
||||||
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.
|
|
||||||
private void FillQuad(int dstX, int dstY, int w, int h, long tint, float alpha)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
int x0 = System.Math.Max(0, -dstX), x1 = System.Math.Min(w, dw - dstX);
|
|
||||||
int y0 = System.Math.Max(0, -dstY), y1 = System.Math.Min(h, dh - dstY);
|
|
||||||
if (x1 <= x0 || y1 <= y0) return;
|
|
||||||
for (int y = y0; y < y1; y++)
|
|
||||||
for (int x = x0; x < x1; x++)
|
|
||||||
{
|
|
||||||
int dxp = dstX + x, dyp = dstY + y;
|
|
||||||
int di = (dyp * dw + dxp) * 4;
|
|
||||||
dst[di] = (byte)((tr * ia + dst[di] * (255 - ia)) / 255);
|
|
||||||
dst[di + 1] = (byte)((tg * ia + dst[di + 1] * (255 - ia)) / 255);
|
|
||||||
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).
|
|
||||||
private static void BakeColorKey(byte[] px, long colorKey)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < px.Length; i += 4)
|
|
||||||
if (Age.Engine.Model.BlendMath.ColorKeyMatches(px[i], px[i + 1], px[i + 2], colorKey))
|
|
||||||
px[i + 3] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void PageBreak()
|
public void PageBreak()
|
||||||
{
|
{
|
||||||
_pageCount++;
|
_pageCount++;
|
||||||
|
|||||||
Reference in New Issue
Block a user