From 78174df03ae86db9f10451da9fb309fe89e630cd Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 11 Jul 2026 12:41:37 -0400 Subject: [PATCH] Fix movie compositor publication --- docs/asset-resolution-re.md | 9 ++++++--- docs/engine-re.md | 17 +++++++++-------- docs/phase-a-slice-plan.md | 11 +++++++---- godot/GodotAdvHost.cs | 19 +++---------------- godot/Main.cs | 23 ++++++++++++++--------- 5 files changed, 39 insertions(+), 40 deletions(-) diff --git a/docs/asset-resolution-re.md b/docs/asset-resolution-re.md index f94ffa5..8d9b117 100644 --- a/docs/asset-resolution-re.md +++ b/docs/asset-resolution-re.md @@ -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. diff --git a/docs/engine-re.md b/docs/engine-re.md index ebf69f4..fb1befe 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -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 diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 5d000a4..9e9dfb2 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -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: diff --git a/godot/GodotAdvHost.cs b/godot/GodotAdvHost.cs index f9efa72..9d1ad3f 100644 --- a/godot/GodotAdvHost.cs +++ b/godot/GodotAdvHost.cs @@ -257,14 +257,14 @@ public sealed class GodotAdvHost : IHost /// Resolve a gfx surface through scene-local or universal raw-id addressing and decode it /// from the loose-first asset store. - 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) diff --git a/godot/Main.cs b/godot/Main.cs index b2a8607..1abe25a 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -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? 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);