Fix movie compositor publication

This commit is contained in:
gamer147
2026-07-11 12:41:37 -04:00
parent 024accd8d1
commit 78174df03a
5 changed files with 39 additions and 40 deletions

View File

@@ -280,9 +280,12 @@ frame; both still pass with the entire `extracted/` tree physically moved aside
SC0000's native capture verifies operands `(0x33, 0, 2, 0)` and immediate VM continuation at `0x13d1`.
In Godot the real site opens the same 8,194,052 VFS bytes, publishes changing 800x600 frames, and retains
them across pre-yield static surface preparation. The later `0x21c` presentation service remains parked
until DirectShow EOF, then scene cleanup stops the movie. Manual interactive validation still does not show
the movie despite those successful lifecycle logs; visible compositor/layer publication is the next bounded
investigation. The real-scene trace remains a separate extracted-present test
until DirectShow EOF, then scene cleanup stops the movie. The initial invisible result was compositor-only:
the static `(assetId,colorKey)` image cache froze the first movie sample, while an extra current-sample
background copy was covered by the correctly positioned retained movie object. Dynamic movie surfaces now
bypass that cache and publish only at their retained z-position. A windowed run reached first frame 101 and
stop frame 190, and manual observation confirmed visible changing video. Movie audio remains intentionally
unrendered. The real-scene trace remains a separate extracted-present test
because the current `Paths.Scripts()` test bootstrap still locates its root `*.BIN` fixtures there; migrating
that test/bootstrap path is unrelated to movie asset loading and was not folded into this slice.

View File

@@ -871,14 +871,15 @@ not itself block the VM. SC0000 prepares additional static layers, then reaches
service continues sampling the retained movie until DirectShow EOF, after which the following script
cleanup releases it. The static preparation before `0x21c` is not a movie teardown boundary.
**Manual-test correction (2026-07-11):** the initial port incorrectly treated those pre-yield static loads
as surface replacement, producing start/first-frame/stop all in render frame 0. The bounded host now keeps
the movie as an independently updating retained layer, parks `0x21c` until the DirectShow completion event,
then permits cleanup. A real-render lifecycle run records first frame 98 and stop frame 181. The screenshot
sequence's known stale-CG/transition behavior can obscure this layer, so screenshot appearance is not used
as the visual oracle. **Manual recheck remains unresolved:** normal interactive SC0000 logs start, changing
frame delivery, EOF, and delayed stop, but the movie is not visibly presented in the application. The next
slice must diagnose compositor/layer publication rather than reopening decode or VM-lifecycle timing.
**Manual-test corrections (2026-07-11):** the initial port incorrectly treated pre-yield static loads as
surface replacement, producing start/first-frame/stop all in render frame 0. The bounded host now retains
the movie through `0x21c` until DirectShow completion. A follow-up compositor trace proved the movie object
was present at the correct z-position, but the static AGF cache reused the first decoded sample forever
because every sample has the same `(assetId,colorKey)`. An unconditional current-sample backbuffer copy did
not help because normal retained composition immediately covered it with that cached first sample. Movie
surfaces now bypass the static cache and publish only through the retained object. A windowed run records
first frame 101 and stop frame 190; the user manually confirmed visible playback. The separate lower white
textbox-area object remains outside this finding, as does the deliberately unrendered movie audio stream.
The `/v2` image names/comments the handler; movie ctor/interface/open/play/volume/release workers; sound
route helpers; renderer media-type/sample workers; and stop/detach/destructor lifecycle. The image was

View File

@@ -1226,10 +1226,13 @@ saved. Validation covers the exact real-SC0000 operands and `0x13c8 -> 0x13d1` b
payload identity, actual 800x600 RGBA DirectShow decode, .NET/Godot builds, and the real Godot site opening
the same VFS byte count. Manual testing exposed and corrected an initial same-frame teardown: pre-yield
static loads no longer destroy the movie, `0x21c` waits for DirectShow completion, and a real-render run
records first frame 98 / stop frame 181. The shot-sequence's known stale-CG behavior can cover the movie,
so it is not treated as visual proof. **Manual recheck is not visually complete:** the normal application
logs start, changing frame delivery, EOF, and delayed stop, but shows no movie. Carry compositor/layer
publication forward as the next bounded diagnostic; do not reopen the proven VFS/decode/resume lifecycle.
records first frame 98 / stop frame 181. A follow-up compositor-only investigation found that the movie
object was present at the correct retained z-position, but the static AGF image cache keyed by
`(assetId,colorKey)` froze its first sample. A duplicate unconditional backbuffer copy of the current sample
was then covered by that same retained object. Dynamic movie surfaces now bypass the static-image cache and
publish only through their retained object. A windowed auto-play run recorded first frame 101 / stop frame
190, and the user manually confirmed that the video visibly played. The known lower white/textbox panel is
an unrelated retained-object issue. Movie audio remains deliberately unrendered and out of scope.
The decoder test
asserts two delivered MPEG frames differ. The two archive/decode tests
also pass with all of `extracted/` physically moved aside. The real-scene trace is intentionally separate:

View File

@@ -257,14 +257,14 @@ public sealed class GodotAdvHost : IHost
/// <summary>Resolve a gfx surface through scene-local or universal raw-id addressing and decode it
/// from the loose-first asset store.</summary>
public (RgbaImage Image, string Name, int AssetId)? ResolveResIdTexture(long resId)
public (RgbaImage Image, string Name, int AssetId, bool IsDynamic)? ResolveResIdTexture(long resId)
{
lock (_imageLock)
if (_movieFrames.TryGetValue(resId, out var movie))
return (movie.Image, movie.Name, movie.RawIndex);
return (movie.Image, movie.Name, movie.RawIndex, true);
var asset = _res.ResolveTexture(_scene, resId);
var image = asset != null ? Decode(asset) : null;
return asset != null && image != null ? (image, asset.Name, asset.RawIndex) : null;
return asset != null && image != null ? (image, asset.Name, asset.RawIndex, false) : null;
}
public void PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask)
@@ -333,19 +333,6 @@ public sealed class GodotAdvHost : IHost
return false;
}
public bool TryGetActiveMovieFrame(out RgbaImage frame)
{
lock (_imageLock)
foreach (long resourceId in _movieBySurface.Values)
if (_movieFrames.TryGetValue(resourceId, out var movie))
{
frame = movie.Image;
return true;
}
frame = default!;
return false;
}
private RgbaImage? Decode(AssetEntry asset)
{
lock (_imageLock)

View File

@@ -306,11 +306,6 @@ public partial class Main : Godot.Control
private void Recomposite()
{
_screen.Fill(new Color(0, 0, 0, 0));
// SC0000's movie is an independently updating retained background. The script prepares later
// static surfaces before its 0x21c yield; those layers composite above the current movie sample.
if (_host.TryGetActiveMovieFrame(out var movieFrame) &&
movieFrame.Width == _screen.GetWidth() && movieFrame.Height == _screen.GetHeight())
_screen.SetData(movieFrame.Width, movieFrame.Height, false, Image.Format.Rgba8, movieFrame.Pixels);
_speaker.Visible = false;
System.Collections.Generic.Dictionary<long, string>? decisions = _gfxLogPath != null || _timeline != null ? new() : null;
int z = 0;
@@ -357,7 +352,7 @@ public partial class Main : Godot.Control
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);
localToDest, opacity, v.MultiplyTint, texture.Value.IsDynamic);
var raw = _vm.Gfx.TryGet(v.Handle);
outcome = $"slot={raw?.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}) " +
@@ -422,7 +417,8 @@ public partial class Main : Godot.Control
var texture = _host.ResolveResIdTexture(source.SurfaceResId);
if (texture == null) 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);
source.SrcX, source.SrcY, source.W, source.H, affine, opacity, source.MultiplyTint,
texture.Value.IsDynamic);
}
drawn++;
}
@@ -464,10 +460,19 @@ public partial class Main : Godot.Control
// Mode 0 uses tintStrength to LERP texel RGB toward tint. Mode 1 sets multiplyTint and uses packed RGB as
// multiplicative modulation while alpha is object opacity.
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)
Age.Engine.Model.Affine2D localToDest, float alpha = 1f, bool multiplyTint = false,
bool dynamic = false)
{
var cacheKey = (assetId, colorKey);
if (!_imgCache.TryGetValue(cacheKey, out var src))
Image? src;
if (dynamic)
{
// Decoder samples replace the pixels of one retained surface. Catalog identity is stable across
// those samples, so the static AGF cache key would otherwise freeze the very first movie frame.
src = Image.CreateFromData(decoded.Width, decoded.Height, false, Image.Format.Rgba8, decoded.Pixels);
if (Age.Engine.Model.BlendMath.HasColorKey(colorKey)) BakeColorKey(src, colorKey);
}
else if (!_imgCache.TryGetValue(cacheKey, out src))
{
src = Image.CreateFromData(decoded.Width, decoded.Height, false, Image.Format.Rgba8, decoded.Pixels);
if (Age.Engine.Model.BlendMath.HasColorKey(colorKey)) BakeColorKey(src, colorKey);