diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index c68f9d5..9939ca0 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -150,7 +150,9 @@ full-width text entry. history presentation, message-window alpha, and retained wait-indicator configuration/publication. `godot/GodotAdvHost.PresentationInput.cs` owns script/presentation synchronization, waits and timing, message-skip/input services, cursor and foreground waits, frame/backbuffer publication, transitions, and -scene-context lifecycle coordination. +scene-context lifecycle coordination. `godot/GodotAdvHost.Surfaces.cs` owns decoded-image caching, mutable +surface pixels/resources/dimensions, fill/copy/resolve operations, render-target publication, and surface/range +teardown. The disposable `build/page-map-.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 diff --git a/docs/remake-architecture-and-roadmap.md b/docs/remake-architecture-and-roadmap.md index 06d0f4e..14e9917 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -581,6 +581,11 @@ do not mix mechanical moves with semantic changes. movie, audio, and retained-text state remains directly accessible through the sealed partial class; runtime validation remains green. + The third bounded `GodotAdvHost` split moved decoded-image caching, mutable surface pixel/resource/dimension + state, allocation and binding, fill/copy/resolve operations, render-target publication, release/range teardown, + and texture decode caching into `godot/GodotAdvHost.Surfaces.cs`. Movie publication and teardown retain direct + access to the surface store through the sealed partial class; runtime validation remains green. + **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 each domain move. @@ -952,7 +957,7 @@ layer's rendering diverges from ADV; save layout. ## 8. Immediate next step Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed by the tracked launcher and layered validation driver. With the planned `Main` domains and the first -two `GodotAdvHost` domains isolated, move host surface storage/mutation next, then continue one existing domain -at a time while preserving public types, commands, and generated output. +three `GodotAdvHost` domains isolated, move host movie playback/mask ownership next, then continue one 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 not replace Phase B gameplay validation or the open cross-platform gates. diff --git a/godot/GodotAdvHost.Surfaces.cs b/godot/GodotAdvHost.Surfaces.cs new file mode 100644 index 0000000..91ccee5 --- /dev/null +++ b/godot/GodotAdvHost.Surfaces.cs @@ -0,0 +1,375 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Age.Engine.Hosting; +using Age.Engine.Model; +using Age.Engine.Sys4; +using Age.Engine.Text; + +public sealed partial class GodotAdvHost +{ + private readonly object _imageLock = new(); + private readonly Dictionary _images = new(); // packed catalog id -> decoded pixels + // Mutable AGE surfaces are published by replacing immutable RgbaImage snapshots, so the compositor + // can safely finish reading an old frame while the VM prepares a copied-rectangle update. + private readonly Dictionary _surfaceImages = new(); + private readonly Dictionary _surfaceColorKeys = new(); + private readonly Dictionary _surfaceResources = new(); // surface slot -> packed catalog id + // slot -> dimensions of the currently allocated surface. Slot 0 begins as the selected game's + // logical canvas, but op 0x1fa releases it like any other slot; subsequent queries return 0x0. + private readonly Dictionary _slotDims = new(); + + public void FillSurfaceRect(SurfaceRectFill fill) + { + RgbaImage? destination = ResolveSurfacePixels(fill.SurfaceSlot); + if (destination == null && _slotDims.TryGetValue(fill.SurfaceSlot, out var dimensions) + && dimensions.W >= 0 && dimensions.H >= 0) + destination = new RgbaImage(dimensions.W, dimensions.H, + new byte[checked(dimensions.W * dimensions.H * 4)]); + if (destination != null) + { + var updated = new RgbaImage(destination.Width, destination.Height, + (byte[])destination.Pixels.Clone()); + if (RgbaSurfaceOps.FillRect(updated, fill.X, fill.Y, fill.Width, fill.Height, + unchecked((byte)fill.Alpha), fill.Rgb)) + { + lock (_imageLock) _surfaceImages[fill.SurfaceSlot] = updated; + System.Threading.Interlocked.Exchange(ref _presentRequested, 1); + } + } + _timeline?.Event("surface-fill", new() + { + ["surface"] = fill.SurfaceSlot, ["x"] = fill.X, ["y"] = fill.Y, + ["w"] = fill.Width, ["h"] = fill.Height, ["alpha"] = fill.Alpha, ["rgb"] = fill.Rgb, + }); + } + + public bool TraceOps; // --gfx-log: print set-texture/create-texture slot assignments (diagnose slot collisions) + + public void CreateTexture(int slot, int width, int height) + { + lock (_textLock) _surfaceResources.Remove(slot); + int safeWidth = System.Math.Max(0, width); + int safeHeight = System.Math.Max(0, height); + lock (_imageLock) + { + _surfaceImages[slot] = new RgbaImage(safeWidth, safeHeight, + new byte[checked(safeWidth * safeHeight * 4)]); + _surfaceColorKeys.Remove(slot); + } + _slotDims[slot] = (safeWidth, safeHeight); + if (TraceOps) Godot.GD.Print($"[op] create-texture slot={slot} {width}x{height}"); + } + + public void SetTexture(long resourceId, int slot) => SetTexture(resourceId, slot, -1); + + public void SetTexture(long resourceId, int slot, long colorKey) + { + lock (_imageLock) + { + _surfaceImages.Remove(slot); + _surfaceColorKeys[slot] = colorKey; + } + lock (_textLock) + { + _surfaceResources[slot] = resourceId; + } + var asset = _res.ResolveTexture(resourceId); + var image = asset != null ? Decode(asset) : null; + _slotDims[slot] = image != null ? (image.Width, image.Height) : (0, 0); + if (TraceOps) Godot.GD.Print($"[op] set-texture slot={slot} resId=0x{resourceId:x} -> {(asset?.Name ?? "")}"); + } + + // AGF is decoded synchronously on the VM thread so geometry queried immediately afterward sees real dims. + public (int Width, int Height) GetTextureSize(int slot) + => _slotDims.TryGetValue(slot, out var d) ? (d.W, d.H) : (0, 0); + + public RgbaImage? CaptureSurfacePixels(int slot) + { + RgbaImage? image = ResolveSurfacePixels(slot); + return image == null + ? null + : new RgbaImage(image.Width, image.Height, (byte[])image.Pixels.Clone()); + } + + public bool ReplaceSurfacePixels(int slot, RgbaImage image) + { + if (image.Width <= 0 || image.Height <= 0 + || image.Pixels.Length != checked(image.Width * image.Height * 4)) + return false; + lock (_imageLock) + { + _surfaceImages[slot] = + new RgbaImage(image.Width, image.Height, (byte[])image.Pixels.Clone()); + _surfaceColorKeys.Remove(slot); + } + lock (_textLock) + { + _surfaceResources.Remove(slot); + foreach (int layoutSlot in _retainedTextLayouts + .Where(pair => pair.Value.Binding.SourceSurfaceSlot == slot) + .Select(pair => pair.Key) + .ToArray()) + { + _retainedTextLayouts.Remove(layoutSlot); + _retainedHistoryLayouts.Remove(layoutSlot); + } + } + _slotDims[slot] = (image.Width, image.Height); + return true; + } + + // Retained render model: draw-texture updates GfxState (object -> surface bind); Main._Process composites + // the visible objects each frame in ascending-handle order. No immediate blit here. + public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { } + + public void CopySurfaceRect(SurfaceRectCopy copy) + { + RgbaImage? source = ResolveSurfacePixels(copy.SourceSurface); + RgbaImage? destination = ResolveSurfacePixels(copy.DestinationSurface); + if (destination == null && _slotDims.TryGetValue(copy.DestinationSurface, out var dimensions) + && dimensions.W >= 0 && dimensions.H >= 0) + destination = new RgbaImage(dimensions.W, dimensions.H, + new byte[checked(dimensions.W * dimensions.H * 4)]); + if (source == null || destination == null) + { + ReportWarning($"surface copy unresolved source={copy.SourceSurface} destination={copy.DestinationSurface}"); + return; + } + + var updated = new RgbaImage(destination.Width, destination.Height, (byte[])destination.Pixels.Clone()); + if (RgbaSurfaceOps.CopyRect(source, updated, copy.SourceX, copy.SourceY, copy.Width, copy.Height, + copy.DestinationX, copy.DestinationY)) + { + lock (_imageLock) _surfaceImages[copy.DestinationSurface] = updated; + System.Threading.Interlocked.Exchange(ref _presentRequested, 1); + } + _timeline?.Event("surface-copy", new() + { + ["source"] = copy.SourceSurface, ["source_x"] = copy.SourceX, ["source_y"] = copy.SourceY, + ["w"] = copy.Width, ["h"] = copy.Height, ["destination"] = copy.DestinationSurface, + ["destination_x"] = copy.DestinationX, ["destination_y"] = copy.DestinationY, + }); + } + + private RgbaImage? ResolveSurfacePixels(int slot) + { + lock (_imageLock) + if (_surfaceImages.TryGetValue(slot, out var mutable)) return mutable; + long resourceId; + lock (_textLock) + if (!_surfaceResources.TryGetValue(slot, out resourceId)) return null; + var resolved = ResolveResIdTexture(resourceId); + if (resolved == null) return null; + long colorKey; + lock (_imageLock) colorKey = _surfaceColorKeys.GetValueOrDefault(slot, -1); + return RgbaSurfaceOps.WithColorKey(resolved.Value.Image, colorKey); + } + + /// Resolve a gfx surface through universal packed addressing and decode it from the + /// loose-first asset store. + public (RgbaImage Image, string Name, int AssetId, bool IsDynamic)? ResolveResIdTexture(long resId) + { + if (_movieSurfaces.TryResolveResource(resId, out var movie) && movie != null) + return (movie.Image, movie.Name, movie.AssetId, true); + // Movie payloads use the same .AGF extension as still images. Do not misclassify the MPEG program + // stream before its first frame or during the cleanup frame after its surface binding is detached. + // Packed catalog identity is immutable, so a resource which entered the typed movie path remains + // a movie even when it has no live playback. + if (_movieSurfaces.IsKnownMovieResource(resId)) return null; + var asset = _res.ResolveTexture(resId); + var image = asset != null ? Decode(asset) : null; + return asset != null && image != null ? (image, asset.Name, asset.PackedId, false) : null; + } + + public (RgbaImage Image, string Name, int AssetId, bool IsDynamic)? ResolveSurfaceTexture( + int surfaceSlot, long fallbackResourceId) + { + lock (_movieMaskLock) + if (_movieMaskPlaybackBySurface.ContainsKey(surfaceSlot)) + lock (_imageLock) + if (_surfaceImages.TryGetValue(surfaceSlot, out var masked)) + return (masked, $"", + int.MinValue + surfaceSlot, true); + if (_movieSurfaces.TryResolveSurface(surfaceSlot, out var movie) && movie != null) + return (movie.Image, movie.Name, movie.AssetId, true); + if (_movieSurfaces.IsBound(surfaceSlot)) return null; + lock (_imageLock) + if (_surfaceImages.TryGetValue(surfaceSlot, out var surface)) + return (surface, $"", int.MinValue + surfaceSlot, true); + return fallbackResourceId != 0 ? ResolveResIdTexture(fallbackResourceId) : null; + } + + public void ReleaseSurface(int slot) + { + lock (_screenTransitionLock) _renderTargetSnapshots.Remove(slot); + MovieSurfaceRelease movieRelease = _movieSurfaces.ReleaseIfCompleted(slot); + if (movieRelease.Kind == MovieSurfaceReleaseKind.Active) + return; // Static surface setup before 0x21c must not evict an active movie playback. + lock (_imageLock) + { + _surfaceImages.Remove(slot); + _surfaceColorKeys.Remove(slot); + } + lock (_textLock) + { + _surfaceResources.Remove(slot); + foreach (int layoutSlot in _retainedTextLayouts + .Where(pair => pair.Value.Binding.SourceSurfaceSlot == slot) + .Select(pair => pair.Key) + .ToArray()) + { + _retainedTextLayouts.Remove(layoutSlot); + _retainedHistoryLayouts.Remove(layoutSlot); + } + } + _slotDims.Remove(slot); + if (movieRelease.Kind == MovieSurfaceReleaseKind.Released) + { + var binding = movieRelease.Binding; + AbandonMovieMaskPlayback(binding.PlaybackId); + ForgetMovieMaskSurface(binding.SurfaceSlot, binding.PlaybackId); + _timeline?.Event("movie-stop", new() + { + ["resource"] = binding.ResourceId, + ["playback"] = binding.PlaybackId, + ["surface"] = slot, + }); + _main.CallDeferred("StopMovie", binding.PlaybackId); + } + } + + public void ClearRenderTarget(int surfaceSlot) + { + if (surfaceSlot < 0) + { + lock (_backbufferRangeLock) _backbufferClearPending = true; + } + else + { + lock (_imageLock) + { + if (_surfaceImages.TryGetValue(surfaceSlot, out var image)) + System.Array.Clear(image.Pixels); + else if (_slotDims.TryGetValue(surfaceSlot, out var dimensions) + && dimensions.W > 0 && dimensions.H > 0) + _surfaceImages[surfaceSlot] = new RgbaImage( + dimensions.W, dimensions.H, + new byte[checked(dimensions.W * dimensions.H * 4)]); + } + } + _timeline?.Event("render-target-clear", new() { ["surface"] = surfaceSlot }); + } + + private void PublishObjectRangeToSurface( + GfxState gfx, long firstHandle, long count, + IReadOnlyList? sampled = null) + { + int targetSlot = gfx.CurrentRenderTargetSlot; + if (targetSlot < 0 || !_slotDims.TryGetValue(targetSlot, out var dimensions) + || dimensions.W <= 0 || dimensions.H <= 0) + return; + + RgbaImage destination; + lock (_imageLock) + destination = _surfaceImages.TryGetValue(targetSlot, out var current) + ? new RgbaImage(current.Width, current.Height, (byte[])current.Pixels.Clone()) + : new RgbaImage(dimensions.W, dimensions.H, + new byte[checked(dimensions.W * dimensions.H * 4)]); + + IReadOnlyList visible = sampled ?? gfx.SnapshotVisibleObjects(_clock.NowMs); + int rendered = RetainedSurfaceRasterizer.CompositeRange( + destination, visible, firstHandle, count, + item => + { + var raw = gfx.TryGet(item.Handle); + var resolved = raw != null + ? ResolveSurfaceTexture(raw.SourceSlot, item.SurfaceResId) + : ResolveResIdTexture(item.SurfaceResId); + return resolved == null + ? null + : RgbaSurfaceOps.WithColorKey(resolved.Value.Image, item.ColorKey); + }); + + lock (_imageLock) _surfaceImages[targetSlot] = destination; + IReadOnlyList retained = visible + .Where(item => item.Handle >= firstHandle && item.Handle - firstHandle < count) + .ToArray(); + lock (_screenTransitionLock) _renderTargetSnapshots[targetSlot] = retained; + _timeline?.Event("render-target-publish", new() + { + ["surface"] = targetSlot, + ["first"] = firstHandle, + ["count"] = count, + ["objects"] = retained.Count, + ["rendered"] = rendered, + }); + } + + public void ReleaseSurfaceRange(int firstSlot, int count) + { + IReadOnlyList stoppedMovies = _movieSurfaces.ReleaseRange(firstSlot, count); + int end = checked(firstSlot + count); + lock (_screenTransitionLock) + for (int slot = firstSlot; slot < end; slot++) _renderTargetSnapshots.Remove(slot); + lock (_imageLock) + { + for (int slot = firstSlot; slot < end; slot++) + { + _surfaceImages.Remove(slot); + _surfaceColorKeys.Remove(slot); + _slotDims.Remove(slot); + } + } + lock (_textLock) + { + for (int slot = firstSlot; slot < end; slot++) + { + _surfaceResources.Remove(slot); + } + foreach (int layoutSlot in _retainedTextLayouts + .Where(pair => + pair.Value.Binding.SourceSurfaceSlot >= firstSlot + && pair.Value.Binding.SourceSurfaceSlot < end) + .Select(pair => pair.Key) + .ToArray()) + _retainedTextLayouts.Remove(layoutSlot); + _retainedHistoryLayouts.RemoveWhere(layoutSlot => + !_retainedTextLayouts.ContainsKey(layoutSlot)); + } + foreach (MovieSurfaceBinding binding in stoppedMovies) + { + AbandonMovieMaskPlayback(binding.PlaybackId); + ForgetMovieMaskSurface(binding.SurfaceSlot, binding.PlaybackId); + _timeline?.Event("movie-stop", new() + { + ["resource"] = binding.ResourceId, + ["playback"] = binding.PlaybackId, + ["range_release"] = true, + }); + _main.CallDeferred("StopMovie", binding.PlaybackId); + } + _timeline?.Event("surface-range-release", new() { ["first"] = firstSlot, ["count"] = count }); + } + + // Main-thread decoder handoff. Replacing the newest frame mirrors the native texture renderer's + // sample callback: the retained object keeps its surface binding while only the surface pixels change. + private RgbaImage? Decode(AssetEntry asset) + { + lock (_imageLock) + { + if (_images.TryGetValue(asset.PackedId, out var cached)) return cached; + try { return _images[asset.PackedId] = _res.DecodeTexture(asset); } + catch (System.Exception e) + { + Godot.GD.Print($"AGF decode failed {asset.Name}: {e.Message}"); + _images[asset.PackedId] = null; + return null; + } + } + } + + // ---- audio ops (OGG plays natively in Godot) ---- + // BGM is addressed by direct name (BGM{id:D3}.OGG); voice uses the universal packed catalog. +} diff --git a/godot/GodotAdvHost.cs b/godot/GodotAdvHost.cs index 1291dc1..81b0207 100644 --- a/godot/GodotAdvHost.cs +++ b/godot/GodotAdvHost.cs @@ -12,13 +12,6 @@ public sealed partial class GodotAdvHost : IHost private readonly Main _main; private readonly ResourceMap _res; private readonly string _rootScene; - private readonly object _imageLock = new(); - private readonly Dictionary _images = new(); // packed catalog id -> decoded pixels - // Mutable AGE surfaces are published by replacing immutable RgbaImage snapshots, so the compositor - // can safely finish reading an old frame while the VM prepares a copied-rectangle update. - private readonly Dictionary _surfaceImages = new(); - private readonly Dictionary _surfaceColorKeys = new(); - private readonly Dictionary _surfaceResources = new(); // surface slot -> packed catalog id private readonly MovieSurfaceRegistry _movieSurfaces = new(); private sealed record MovieMaskPlayback( GfxState Gfx, MovieMaskTransitionRequest Request, RgbaImage Captured); @@ -26,9 +19,6 @@ public sealed partial class GodotAdvHost : IHost private readonly Dictionary _movieMasksByPlayback = new(); private readonly Dictionary _movieMaskPlaybackBySurface = new(); private readonly string?[] _sfxNames = new string?[10]; // SC0000 native channel subset - // slot -> dimensions of the currently allocated surface. Slot 0 begins as the selected game's - // logical canvas, but op 0x1fa releases it like any other slot; subsequent queries return 0x0. - private readonly Dictionary _slotDims = new(); private readonly int _screenWidth; private readonly int _screenHeight; private readonly Age.Engine.Hosting.FrameClock _clock; @@ -69,187 +59,6 @@ public sealed partial class GodotAdvHost : IHost public Sys4LogicalCanvas LogicalCanvas => new(_screenWidth, _screenHeight); public void ReportWarning(string message) => System.Console.Error.WriteLine(message); - public void FillSurfaceRect(SurfaceRectFill fill) - { - RgbaImage? destination = ResolveSurfacePixels(fill.SurfaceSlot); - if (destination == null && _slotDims.TryGetValue(fill.SurfaceSlot, out var dimensions) - && dimensions.W >= 0 && dimensions.H >= 0) - destination = new RgbaImage(dimensions.W, dimensions.H, - new byte[checked(dimensions.W * dimensions.H * 4)]); - if (destination != null) - { - var updated = new RgbaImage(destination.Width, destination.Height, - (byte[])destination.Pixels.Clone()); - if (RgbaSurfaceOps.FillRect(updated, fill.X, fill.Y, fill.Width, fill.Height, - unchecked((byte)fill.Alpha), fill.Rgb)) - { - lock (_imageLock) _surfaceImages[fill.SurfaceSlot] = updated; - System.Threading.Interlocked.Exchange(ref _presentRequested, 1); - } - } - _timeline?.Event("surface-fill", new() - { - ["surface"] = fill.SurfaceSlot, ["x"] = fill.X, ["y"] = fill.Y, - ["w"] = fill.Width, ["h"] = fill.Height, ["alpha"] = fill.Alpha, ["rgb"] = fill.Rgb, - }); - } - - public bool TraceOps; // --gfx-log: print set-texture/create-texture slot assignments (diagnose slot collisions) - - public void CreateTexture(int slot, int width, int height) - { - lock (_textLock) _surfaceResources.Remove(slot); - int safeWidth = System.Math.Max(0, width); - int safeHeight = System.Math.Max(0, height); - lock (_imageLock) - { - _surfaceImages[slot] = new RgbaImage(safeWidth, safeHeight, - new byte[checked(safeWidth * safeHeight * 4)]); - _surfaceColorKeys.Remove(slot); - } - _slotDims[slot] = (safeWidth, safeHeight); - if (TraceOps) Godot.GD.Print($"[op] create-texture slot={slot} {width}x{height}"); - } - - public void SetTexture(long resourceId, int slot) => SetTexture(resourceId, slot, -1); - - public void SetTexture(long resourceId, int slot, long colorKey) - { - lock (_imageLock) - { - _surfaceImages.Remove(slot); - _surfaceColorKeys[slot] = colorKey; - } - lock (_textLock) - { - _surfaceResources[slot] = resourceId; - } - var asset = _res.ResolveTexture(resourceId); - var image = asset != null ? Decode(asset) : null; - _slotDims[slot] = image != null ? (image.Width, image.Height) : (0, 0); - if (TraceOps) Godot.GD.Print($"[op] set-texture slot={slot} resId=0x{resourceId:x} -> {(asset?.Name ?? "")}"); - } - - // AGF is decoded synchronously on the VM thread so geometry queried immediately afterward sees real dims. - public (int Width, int Height) GetTextureSize(int slot) - => _slotDims.TryGetValue(slot, out var d) ? (d.W, d.H) : (0, 0); - - public RgbaImage? CaptureSurfacePixels(int slot) - { - RgbaImage? image = ResolveSurfacePixels(slot); - return image == null - ? null - : new RgbaImage(image.Width, image.Height, (byte[])image.Pixels.Clone()); - } - - public bool ReplaceSurfacePixels(int slot, RgbaImage image) - { - if (image.Width <= 0 || image.Height <= 0 - || image.Pixels.Length != checked(image.Width * image.Height * 4)) - return false; - lock (_imageLock) - { - _surfaceImages[slot] = - new RgbaImage(image.Width, image.Height, (byte[])image.Pixels.Clone()); - _surfaceColorKeys.Remove(slot); - } - lock (_textLock) - { - _surfaceResources.Remove(slot); - foreach (int layoutSlot in _retainedTextLayouts - .Where(pair => pair.Value.Binding.SourceSurfaceSlot == slot) - .Select(pair => pair.Key) - .ToArray()) - { - _retainedTextLayouts.Remove(layoutSlot); - _retainedHistoryLayouts.Remove(layoutSlot); - } - } - _slotDims[slot] = (image.Width, image.Height); - return true; - } - - // Retained render model: draw-texture updates GfxState (object -> surface bind); Main._Process composites - // the visible objects each frame in ascending-handle order. No immediate blit here. - public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { } - - public void CopySurfaceRect(SurfaceRectCopy copy) - { - RgbaImage? source = ResolveSurfacePixels(copy.SourceSurface); - RgbaImage? destination = ResolveSurfacePixels(copy.DestinationSurface); - if (destination == null && _slotDims.TryGetValue(copy.DestinationSurface, out var dimensions) - && dimensions.W >= 0 && dimensions.H >= 0) - destination = new RgbaImage(dimensions.W, dimensions.H, - new byte[checked(dimensions.W * dimensions.H * 4)]); - if (source == null || destination == null) - { - ReportWarning($"surface copy unresolved source={copy.SourceSurface} destination={copy.DestinationSurface}"); - return; - } - - var updated = new RgbaImage(destination.Width, destination.Height, (byte[])destination.Pixels.Clone()); - if (RgbaSurfaceOps.CopyRect(source, updated, copy.SourceX, copy.SourceY, copy.Width, copy.Height, - copy.DestinationX, copy.DestinationY)) - { - lock (_imageLock) _surfaceImages[copy.DestinationSurface] = updated; - System.Threading.Interlocked.Exchange(ref _presentRequested, 1); - } - _timeline?.Event("surface-copy", new() - { - ["source"] = copy.SourceSurface, ["source_x"] = copy.SourceX, ["source_y"] = copy.SourceY, - ["w"] = copy.Width, ["h"] = copy.Height, ["destination"] = copy.DestinationSurface, - ["destination_x"] = copy.DestinationX, ["destination_y"] = copy.DestinationY, - }); - } - - private RgbaImage? ResolveSurfacePixels(int slot) - { - lock (_imageLock) - if (_surfaceImages.TryGetValue(slot, out var mutable)) return mutable; - long resourceId; - lock (_textLock) - if (!_surfaceResources.TryGetValue(slot, out resourceId)) return null; - var resolved = ResolveResIdTexture(resourceId); - if (resolved == null) return null; - long colorKey; - lock (_imageLock) colorKey = _surfaceColorKeys.GetValueOrDefault(slot, -1); - return RgbaSurfaceOps.WithColorKey(resolved.Value.Image, colorKey); - } - - /// Resolve a gfx surface through universal packed addressing and decode it from the - /// loose-first asset store. - public (RgbaImage Image, string Name, int AssetId, bool IsDynamic)? ResolveResIdTexture(long resId) - { - if (_movieSurfaces.TryResolveResource(resId, out var movie) && movie != null) - return (movie.Image, movie.Name, movie.AssetId, true); - // Movie payloads use the same .AGF extension as still images. Do not misclassify the MPEG program - // stream before its first frame or during the cleanup frame after its surface binding is detached. - // Packed catalog identity is immutable, so a resource which entered the typed movie path remains - // a movie even when it has no live playback. - if (_movieSurfaces.IsKnownMovieResource(resId)) return null; - var asset = _res.ResolveTexture(resId); - var image = asset != null ? Decode(asset) : null; - return asset != null && image != null ? (image, asset.Name, asset.PackedId, false) : null; - } - - public (RgbaImage Image, string Name, int AssetId, bool IsDynamic)? ResolveSurfaceTexture( - int surfaceSlot, long fallbackResourceId) - { - lock (_movieMaskLock) - if (_movieMaskPlaybackBySurface.ContainsKey(surfaceSlot)) - lock (_imageLock) - if (_surfaceImages.TryGetValue(surfaceSlot, out var masked)) - return (masked, $"", - int.MinValue + surfaceSlot, true); - if (_movieSurfaces.TryResolveSurface(surfaceSlot, out var movie) && movie != null) - return (movie.Image, movie.Name, movie.AssetId, true); - if (_movieSurfaces.IsBound(surfaceSlot)) return null; - lock (_imageLock) - if (_surfaceImages.TryGetValue(surfaceSlot, out var surface)) - return (surface, $"", int.MinValue + surfaceSlot, true); - return fallbackResourceId != 0 ? ResolveResIdTexture(fallbackResourceId) : null; - } - public bool IsMovieSurfaceBound(int surfaceSlot) => _movieSurfaces.IsBound(surfaceSlot); public long? PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask) @@ -442,160 +251,6 @@ public sealed partial class GodotAdvHost : IHost } } - public void ReleaseSurface(int slot) - { - lock (_screenTransitionLock) _renderTargetSnapshots.Remove(slot); - MovieSurfaceRelease movieRelease = _movieSurfaces.ReleaseIfCompleted(slot); - if (movieRelease.Kind == MovieSurfaceReleaseKind.Active) - return; // Static surface setup before 0x21c must not evict an active movie playback. - lock (_imageLock) - { - _surfaceImages.Remove(slot); - _surfaceColorKeys.Remove(slot); - } - lock (_textLock) - { - _surfaceResources.Remove(slot); - foreach (int layoutSlot in _retainedTextLayouts - .Where(pair => pair.Value.Binding.SourceSurfaceSlot == slot) - .Select(pair => pair.Key) - .ToArray()) - { - _retainedTextLayouts.Remove(layoutSlot); - _retainedHistoryLayouts.Remove(layoutSlot); - } - } - _slotDims.Remove(slot); - if (movieRelease.Kind == MovieSurfaceReleaseKind.Released) - { - var binding = movieRelease.Binding; - AbandonMovieMaskPlayback(binding.PlaybackId); - ForgetMovieMaskSurface(binding.SurfaceSlot, binding.PlaybackId); - _timeline?.Event("movie-stop", new() - { - ["resource"] = binding.ResourceId, - ["playback"] = binding.PlaybackId, - ["surface"] = slot, - }); - _main.CallDeferred("StopMovie", binding.PlaybackId); - } - } - - public void ClearRenderTarget(int surfaceSlot) - { - if (surfaceSlot < 0) - { - lock (_backbufferRangeLock) _backbufferClearPending = true; - } - else - { - lock (_imageLock) - { - if (_surfaceImages.TryGetValue(surfaceSlot, out var image)) - System.Array.Clear(image.Pixels); - else if (_slotDims.TryGetValue(surfaceSlot, out var dimensions) - && dimensions.W > 0 && dimensions.H > 0) - _surfaceImages[surfaceSlot] = new RgbaImage( - dimensions.W, dimensions.H, - new byte[checked(dimensions.W * dimensions.H * 4)]); - } - } - _timeline?.Event("render-target-clear", new() { ["surface"] = surfaceSlot }); - } - - private void PublishObjectRangeToSurface( - GfxState gfx, long firstHandle, long count, - IReadOnlyList? sampled = null) - { - int targetSlot = gfx.CurrentRenderTargetSlot; - if (targetSlot < 0 || !_slotDims.TryGetValue(targetSlot, out var dimensions) - || dimensions.W <= 0 || dimensions.H <= 0) - return; - - RgbaImage destination; - lock (_imageLock) - destination = _surfaceImages.TryGetValue(targetSlot, out var current) - ? new RgbaImage(current.Width, current.Height, (byte[])current.Pixels.Clone()) - : new RgbaImage(dimensions.W, dimensions.H, - new byte[checked(dimensions.W * dimensions.H * 4)]); - - IReadOnlyList visible = sampled ?? gfx.SnapshotVisibleObjects(_clock.NowMs); - int rendered = RetainedSurfaceRasterizer.CompositeRange( - destination, visible, firstHandle, count, - item => - { - var raw = gfx.TryGet(item.Handle); - var resolved = raw != null - ? ResolveSurfaceTexture(raw.SourceSlot, item.SurfaceResId) - : ResolveResIdTexture(item.SurfaceResId); - return resolved == null - ? null - : RgbaSurfaceOps.WithColorKey(resolved.Value.Image, item.ColorKey); - }); - - lock (_imageLock) _surfaceImages[targetSlot] = destination; - IReadOnlyList retained = visible - .Where(item => item.Handle >= firstHandle && item.Handle - firstHandle < count) - .ToArray(); - lock (_screenTransitionLock) _renderTargetSnapshots[targetSlot] = retained; - _timeline?.Event("render-target-publish", new() - { - ["surface"] = targetSlot, - ["first"] = firstHandle, - ["count"] = count, - ["objects"] = retained.Count, - ["rendered"] = rendered, - }); - } - - public void ReleaseSurfaceRange(int firstSlot, int count) - { - IReadOnlyList stoppedMovies = _movieSurfaces.ReleaseRange(firstSlot, count); - int end = checked(firstSlot + count); - lock (_screenTransitionLock) - for (int slot = firstSlot; slot < end; slot++) _renderTargetSnapshots.Remove(slot); - lock (_imageLock) - { - for (int slot = firstSlot; slot < end; slot++) - { - _surfaceImages.Remove(slot); - _surfaceColorKeys.Remove(slot); - _slotDims.Remove(slot); - } - } - lock (_textLock) - { - for (int slot = firstSlot; slot < end; slot++) - { - _surfaceResources.Remove(slot); - } - foreach (int layoutSlot in _retainedTextLayouts - .Where(pair => - pair.Value.Binding.SourceSurfaceSlot >= firstSlot - && pair.Value.Binding.SourceSurfaceSlot < end) - .Select(pair => pair.Key) - .ToArray()) - _retainedTextLayouts.Remove(layoutSlot); - _retainedHistoryLayouts.RemoveWhere(layoutSlot => - !_retainedTextLayouts.ContainsKey(layoutSlot)); - } - foreach (MovieSurfaceBinding binding in stoppedMovies) - { - AbandonMovieMaskPlayback(binding.PlaybackId); - ForgetMovieMaskSurface(binding.SurfaceSlot, binding.PlaybackId); - _timeline?.Event("movie-stop", new() - { - ["resource"] = binding.ResourceId, - ["playback"] = binding.PlaybackId, - ["range_release"] = true, - }); - _main.CallDeferred("StopMovie", binding.PlaybackId); - } - _timeline?.Event("surface-range-release", new() { ["first"] = firstSlot, ["count"] = count }); - } - - // Main-thread decoder handoff. Replacing the newest frame mirrors the native texture renderer's - // sample callback: the retained object keeps its surface binding while only the surface pixels change. public void PublishMovieFrame(long playbackId, string name, int assetId, RgbaImage frame) { MovieMaskPlayback? maskPlayback; @@ -704,23 +359,6 @@ public sealed partial class GodotAdvHost : IHost _movieMaskPlaybackBySurface.Remove(surfaceSlot); } - private RgbaImage? Decode(AssetEntry asset) - { - lock (_imageLock) - { - if (_images.TryGetValue(asset.PackedId, out var cached)) return cached; - try { return _images[asset.PackedId] = _res.DecodeTexture(asset); } - catch (System.Exception e) - { - Godot.GD.Print($"AGF decode failed {asset.Name}: {e.Message}"); - _images[asset.PackedId] = null; - return null; - } - } - } - - // ---- audio ops (OGG plays natively in Godot) ---- - // BGM is addressed by direct name (BGM{id:D3}.OGG); voice uses the universal packed catalog. public void PlayBgm(long id) => DispatchBgm(id, 1, false);