Batch compositor backbuffer updates
This commit is contained in:
@@ -1294,3 +1294,27 @@ builds with zero warnings and threaded `SELFTEST OK`. A matching hidden-window V
|
|||||||
using `--speed 8` only to reach the static state quickly, held **60 FPS / 16.66 ms per frame** for all eight
|
using `--speed 8` only to reach the static state quickly, held **60 FPS / 16.66 ms per frame** for all eight
|
||||||
reported samples, versus the pre-change 23-25 FPS. Buffer batching/source-pixel caching and rasterizer fast
|
reported samples, versus the pre-change 23-25 FPS. Buffer batching/source-pixel caching and rasterizer fast
|
||||||
paths remain independent follow-ups.
|
paths remain independent follow-ups.
|
||||||
|
|
||||||
|
**Quick win 2 implemented (2026-07-11).** The Godot compositor now owns one reusable 800x600 RGBA8
|
||||||
|
managed backbuffer. `Recomposite()` clears it once, every image/fill/transition layer mutates that same
|
||||||
|
array in z-order through the unchanged `SoftwareAffineRasterizer`, and only the completed frame crosses the
|
||||||
|
Godot boundary via one `Image.SetData()` plus one `ImageTexture.Update()`. The former per-layer screen
|
||||||
|
`Image.GetData()` / `Image.SetData()` round trip is gone. Static source images still call `GetData()` per
|
||||||
|
draw; source-pixel caching remains the next independent optimization.
|
||||||
|
|
||||||
|
Visual validation used the same SC0000 page-1 command before and after the refactor (`--speed 8 --shot-page
|
||||||
|
1 --shot-settle 3`). The two PNG files are byte-identical, both SHA-256
|
||||||
|
`E669355772D4D9118BE80AC93595F78BB467B088659DEA4CC2D2B7D0F05B62BE`. A 12-second normal-speed windowed
|
||||||
|
Vulkan probe now reported 53, 26, and 39 FPS through the initial heavier active intervals, then 59-60 FPS
|
||||||
|
through lighter/static intervals; the old compositor had remained around 23-25 FPS. Validation also holds
|
||||||
|
at engine **135/135**, zero-warning Godot build, and threaded `SELFTEST OK`. The general per-pixel affine
|
||||||
|
loop is now the clearest remaining active-frame cost.
|
||||||
|
|
||||||
|
The byte-identical page-1 capture is a **differential compositor oracle only**, not a claim that the
|
||||||
|
automated `--shot` path has the correct interactive presentation state. Both the pre- and post-refactor
|
||||||
|
captures show fallback `SO013A` rather than the expected first event CG. The matching gfx log proves this is
|
||||||
|
not resource-load failure: `EV049AA.AGF` resolves and loads into slot 3, but at capture its handle `0x0` is
|
||||||
|
positioned at `(-400,-600)` and the full-screen `0xcf08` copy has completed its fade to alpha zero. The user
|
||||||
|
confirmed that ordinary manual play displays the correct CGs. Keep this shot-path state discrepancy open
|
||||||
|
and do not use page 1 as an absolute scene-fidelity oracle; it predates and is pixel-identical across quick
|
||||||
|
win 2.
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ using Script = Age.Engine.Model.Script; // disambiguate from Godot.Script
|
|||||||
[SupportedOSPlatform("windows")]
|
[SupportedOSPlatform("windows")]
|
||||||
public partial class Main : Godot.Control
|
public partial class Main : Godot.Control
|
||||||
{
|
{
|
||||||
|
private const int ScreenWidth = 800;
|
||||||
|
private const int ScreenHeight = 600;
|
||||||
private TextureRect _screenView = null!; // shows the composited screen backbuffer
|
private TextureRect _screenView = null!; // shows the composited screen backbuffer
|
||||||
private Image _screen = null!; // 800x600 immediate-mode canvas
|
private Image _screen = null!; // 800x600 immediate-mode canvas
|
||||||
private ImageTexture _screenTex = null!;
|
private ImageTexture _screenTex = null!;
|
||||||
|
// 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 readonly byte[] _screenPixels = new byte[ScreenWidth * ScreenHeight * 4];
|
||||||
private Label _text = null!;
|
private Label _text = null!;
|
||||||
private Label _speaker = null!;
|
private Label _speaker = null!;
|
||||||
private Label _status = null!;
|
private Label _status = null!;
|
||||||
@@ -55,7 +60,7 @@ public partial class Main : Godot.Control
|
|||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
// Screen backbuffer: one 800x600 canvas that draw-texture blits into, shown behind the dialogue.
|
// Screen backbuffer: one 800x600 canvas that draw-texture blits into, shown behind the dialogue.
|
||||||
_screen = Image.CreateEmpty(800, 600, false, Image.Format.Rgba8);
|
_screen = Image.CreateEmpty(ScreenWidth, ScreenHeight, false, Image.Format.Rgba8);
|
||||||
_screenTex = ImageTexture.CreateFromImage(_screen);
|
_screenTex = ImageTexture.CreateFromImage(_screen);
|
||||||
_screenView = new TextureRect
|
_screenView = new TextureRect
|
||||||
{
|
{
|
||||||
@@ -305,7 +310,7 @@ public partial class Main : Godot.Control
|
|||||||
|
|
||||||
private void Recomposite()
|
private void Recomposite()
|
||||||
{
|
{
|
||||||
_screen.Fill(new Color(0, 0, 0, 0));
|
System.Array.Clear(_screenPixels);
|
||||||
_speaker.Visible = false;
|
_speaker.Visible = false;
|
||||||
System.Collections.Generic.Dictionary<long, string>? decisions = _gfxLogPath != null || _timeline != null ? new() : null;
|
System.Collections.Generic.Dictionary<long, string>? decisions = _gfxLogPath != null || _timeline != null ? new() : null;
|
||||||
int z = 0;
|
int z = 0;
|
||||||
@@ -377,6 +382,7 @@ public partial class Main : Godot.Control
|
|||||||
}
|
}
|
||||||
z++;
|
z++;
|
||||||
}
|
}
|
||||||
|
_screen.SetData(ScreenWidth, ScreenHeight, false, Image.Format.Rgba8, _screenPixels);
|
||||||
_screenTex.Update(_screen);
|
_screenTex.Update(_screen);
|
||||||
if (decisions != null) LogGfxDecisionChanges(decisions);
|
if (decisions != null) LogGfxDecisionChanges(decisions);
|
||||||
}
|
}
|
||||||
@@ -487,19 +493,16 @@ public partial class Main : Godot.Control
|
|||||||
sw = System.Math.Min(sw, src.GetWidth() - srcX);
|
sw = System.Math.Min(sw, src.GetWidth() - srcX);
|
||||||
sh = System.Math.Min(sh, src.GetHeight() - srcY);
|
sh = System.Math.Min(sh, src.GetHeight() - srcY);
|
||||||
if (sw <= 0 || sh <= 0) return;
|
if (sw <= 0 || sh <= 0) return;
|
||||||
byte[] dst = _screen.GetData(); byte[] ss = src.GetData();
|
byte[] ss = src.GetData();
|
||||||
Age.Engine.Model.SoftwareAffineRasterizer.BlitRgba(
|
Age.Engine.Model.SoftwareAffineRasterizer.BlitRgba(
|
||||||
dst, _screen.GetWidth(), _screen.GetHeight(), ss, src.GetWidth(), src.GetHeight(),
|
_screenPixels, ScreenWidth, ScreenHeight, ss, src.GetWidth(), src.GetHeight(),
|
||||||
srcX, srcY, sw, sh, localToDest, tint, tintStrength, alpha, multiplyTint);
|
srcX, srcY, sw, sh, localToDest, tint, tintStrength, alpha, multiplyTint);
|
||||||
_screen.SetData(_screen.GetWidth(), _screen.GetHeight(), false, _screen.GetFormat(), dst);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FillAffineQuad(int w, int h, Age.Engine.Model.Affine2D localToDest, long tint, float alpha)
|
private void FillAffineQuad(int w, int h, Age.Engine.Model.Affine2D localToDest, long tint, float alpha)
|
||||||
{
|
{
|
||||||
byte[] dst = _screen.GetData();
|
|
||||||
Age.Engine.Model.SoftwareAffineRasterizer.FillRgba(
|
Age.Engine.Model.SoftwareAffineRasterizer.FillRgba(
|
||||||
dst, _screen.GetWidth(), _screen.GetHeight(), w, h, localToDest, tint, alpha);
|
_screenPixels, ScreenWidth, ScreenHeight, w, h, localToDest, tint, alpha);
|
||||||
_screen.SetData(_screen.GetWidth(), _screen.GetHeight(), false, _screen.GetFormat(), dst);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alpha-blend a solid tint (0xRRGGBB) rectangle over the screen — the surfaceless fade/flash fill.
|
// Alpha-blend a solid tint (0xRRGGBB) rectangle over the screen — the surfaceless fade/flash fill.
|
||||||
@@ -508,8 +511,8 @@ public partial class Main : Godot.Control
|
|||||||
int ia = (int)(System.Math.Clamp(alpha, 0f, 1f) * 255);
|
int ia = (int)(System.Math.Clamp(alpha, 0f, 1f) * 255);
|
||||||
if (ia == 0) return;
|
if (ia == 0) return;
|
||||||
int tr = (int)((tint >> 16) & 0xff), tg = (int)((tint >> 8) & 0xff), tb = (int)(tint & 0xff);
|
int tr = (int)((tint >> 16) & 0xff), tg = (int)((tint >> 8) & 0xff), tb = (int)(tint & 0xff);
|
||||||
byte[] dst = _screen.GetData();
|
byte[] dst = _screenPixels;
|
||||||
int dw = _screen.GetWidth(), dh = _screen.GetHeight();
|
int dw = ScreenWidth, dh = ScreenHeight;
|
||||||
int x0 = System.Math.Max(0, -dstX), x1 = System.Math.Min(w, dw - dstX);
|
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);
|
int y0 = System.Math.Max(0, -dstY), y1 = System.Math.Min(h, dh - dstY);
|
||||||
if (x1 <= x0 || y1 <= y0) return;
|
if (x1 <= x0 || y1 <= y0) return;
|
||||||
@@ -523,7 +526,6 @@ public partial class Main : Godot.Control
|
|||||||
dst[di + 2] = (byte)((tb * ia + dst[di + 2] * (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);
|
dst[di + 3] = (byte)System.Math.Min(255, dst[di + 3] + ia);
|
||||||
}
|
}
|
||||||
_screen.SetData(dw, dh, false, _screen.GetFormat(), dst);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make colorkey-matching texels transparent (native colorkey is baked at surface load).
|
// Make colorkey-matching texels transparent (native colorkey is baked at surface load).
|
||||||
|
|||||||
Reference in New Issue
Block a user