Split Godot host surface operations
This commit is contained in:
@@ -150,7 +150,9 @@ full-width text entry.
|
|||||||
history presentation, message-window alpha, and retained wait-indicator configuration/publication.
|
history presentation, message-window alpha, and retained wait-indicator configuration/publication.
|
||||||
`godot/GodotAdvHost.PresentationInput.cs` owns script/presentation synchronization, waits and timing,
|
`godot/GodotAdvHost.PresentationInput.cs` owns script/presentation synchronization, waits and timing,
|
||||||
message-skip/input services, cursor and foreground waits, frame/backbuffer publication, transitions, and
|
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-<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
|
||||||
|
|||||||
@@ -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
|
movie, audio, and retained-text state remains directly accessible through the sealed partial class; runtime
|
||||||
validation remains green.
|
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
|
**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
|
||||||
each domain move.
|
each domain move.
|
||||||
@@ -952,7 +957,7 @@ 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 planned `Main` domains and the first
|
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
|
three `GodotAdvHost` domains isolated, move host movie playback/mask ownership next, then continue one existing
|
||||||
at a time while preserving public types, commands, and generated output.
|
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.
|
||||||
|
|||||||
375
godot/GodotAdvHost.Surfaces.cs
Normal file
375
godot/GodotAdvHost.Surfaces.cs
Normal file
@@ -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<int, RgbaImage?> _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<int, RgbaImage> _surfaceImages = new();
|
||||||
|
private readonly Dictionary<int, long> _surfaceColorKeys = new();
|
||||||
|
private readonly Dictionary<int, long> _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<int, (int W, int H)> _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 ?? "<none>")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Resolve a gfx surface through universal packed addressing and decode it from the
|
||||||
|
/// loose-first asset store.</summary>
|
||||||
|
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, $"<movie-mask:{surfaceSlot}>",
|
||||||
|
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, $"<surface:{surfaceSlot}>", 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<RenderObject>? 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<RenderObject> 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<RenderObject> 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<MovieSurfaceBinding> 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.
|
||||||
|
}
|
||||||
@@ -12,13 +12,6 @@ public sealed partial class GodotAdvHost : IHost
|
|||||||
private readonly Main _main;
|
private readonly Main _main;
|
||||||
private readonly ResourceMap _res;
|
private readonly ResourceMap _res;
|
||||||
private readonly string _rootScene;
|
private readonly string _rootScene;
|
||||||
private readonly object _imageLock = new();
|
|
||||||
private readonly Dictionary<int, RgbaImage?> _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<int, RgbaImage> _surfaceImages = new();
|
|
||||||
private readonly Dictionary<int, long> _surfaceColorKeys = new();
|
|
||||||
private readonly Dictionary<int, long> _surfaceResources = new(); // surface slot -> packed catalog id
|
|
||||||
private readonly MovieSurfaceRegistry _movieSurfaces = new();
|
private readonly MovieSurfaceRegistry _movieSurfaces = new();
|
||||||
private sealed record MovieMaskPlayback(
|
private sealed record MovieMaskPlayback(
|
||||||
GfxState Gfx, MovieMaskTransitionRequest Request, RgbaImage Captured);
|
GfxState Gfx, MovieMaskTransitionRequest Request, RgbaImage Captured);
|
||||||
@@ -26,9 +19,6 @@ public sealed partial class GodotAdvHost : IHost
|
|||||||
private readonly Dictionary<long, MovieMaskPlayback> _movieMasksByPlayback = new();
|
private readonly Dictionary<long, MovieMaskPlayback> _movieMasksByPlayback = new();
|
||||||
private readonly Dictionary<int, long> _movieMaskPlaybackBySurface = new();
|
private readonly Dictionary<int, long> _movieMaskPlaybackBySurface = new();
|
||||||
private readonly string?[] _sfxNames = new string?[10]; // SC0000 native channel subset
|
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<int, (int W, int H)> _slotDims = new();
|
|
||||||
private readonly int _screenWidth;
|
private readonly int _screenWidth;
|
||||||
private readonly int _screenHeight;
|
private readonly int _screenHeight;
|
||||||
private readonly Age.Engine.Hosting.FrameClock _clock;
|
private readonly Age.Engine.Hosting.FrameClock _clock;
|
||||||
@@ -69,187 +59,6 @@ public sealed partial class GodotAdvHost : IHost
|
|||||||
public Sys4LogicalCanvas LogicalCanvas => new(_screenWidth, _screenHeight);
|
public Sys4LogicalCanvas LogicalCanvas => new(_screenWidth, _screenHeight);
|
||||||
public void ReportWarning(string message) => System.Console.Error.WriteLine(message);
|
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 ?? "<none>")}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Resolve a gfx surface through universal packed addressing and decode it from the
|
|
||||||
/// loose-first asset store.</summary>
|
|
||||||
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, $"<movie-mask:{surfaceSlot}>",
|
|
||||||
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, $"<surface:{surfaceSlot}>", int.MinValue + surfaceSlot, true);
|
|
||||||
return fallbackResourceId != 0 ? ResolveResIdTexture(fallbackResourceId) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsMovieSurfaceBound(int surfaceSlot) => _movieSurfaces.IsBound(surfaceSlot);
|
public bool IsMovieSurfaceBound(int surfaceSlot) => _movieSurfaces.IsBound(surfaceSlot);
|
||||||
|
|
||||||
public long? PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask)
|
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<RenderObject>? 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<RenderObject> 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<RenderObject> 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<MovieSurfaceBinding> 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)
|
public void PublishMovieFrame(long playbackId, string name, int assetId, RgbaImage frame)
|
||||||
{
|
{
|
||||||
MovieMaskPlayback? maskPlayback;
|
MovieMaskPlayback? maskPlayback;
|
||||||
@@ -704,23 +359,6 @@ public sealed partial class GodotAdvHost : IHost
|
|||||||
_movieMaskPlaybackBySurface.Remove(surfaceSlot);
|
_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)
|
public void PlayBgm(long id)
|
||||||
=> DispatchBgm(id, 1, false);
|
=> DispatchBgm(id, 1, false);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user