Prevent failed combat movies from blocking

This commit is contained in:
gamer147
2026-07-21 20:46:10 -04:00
parent 6b58dd156e
commit f53056818d
10 changed files with 84 additions and 37 deletions

View File

@@ -343,19 +343,28 @@ bytes) and `MVB238` (`0x2b94`, 143,364 bytes) both declare 280x352 and fail at
completion, and is stopped by script cleanup. This is decisive backend evidence rather than a resolver, completion, and is stopped by script cleanup. This is decisive backend evidence rather than a resolver,
VFS, signature, or corrupt-asset problem. VFS, signature, or corrupt-asset problem.
The user nevertheless observed the combat presentation stall after that sequence. The existing failure The user nevertheless observed the combat presentation stall after that sequence. Static BTL tracing rules
path calls `NotifyMovieCompleted` immediately, and the successful `movie stopped MVB908` line proves the out `0x23f == -1` as an infinite-loop mechanism by itself. `local 0x44`, the total effect horizon, is first
shared `0x21c` movie wait reached completion at least for the logged sequence. The log contains no VM offset set to `base_time + 1000` at `0x2230`; each effect can only extend that maximum with
or transition-state record after cleanup, so it does not yet prove that DirectShow itself owns the final `effect_start + duration` at `0x2b64..0x2b8b`. A failed duration of -1 can understate that one extension but
stall. One remaining non-native input is concrete: failed graphs make `0x23f` return `-1`, which BTL stores cannot make the callback count negative. After callbacks, `0x2492..0x2515` independently scans movie
in its per-effect duration table at `0x2b31`; a real fallback decoder must instead supply duration and normal surfaces 7..10 through `0x23a`, sleeping 16 ms only while one reports active. The successful
completion. If the stall survives that backend correction, capture the VM/service boundary at the stall and `movie stopped MVB908` line is emitted by the subsequent `0x2518` cleanup, proving that explicit movie wait
treat it as a separate BTL timed-presentation bug. exited in the logged run. The final stall therefore was not localized; it may be after movie cleanup.
The port now nevertheless makes decoder failure safe and deterministic. A valid `0x236` movie whose host
backend cannot initialize is marked completed explicitly and receives stop time 0, meaning an immediate
effect, rather than the former diagnostic -1. Empty movie slots retain native `0x23f == -1`. Successfully
started decoders also carry a completion watchdog: the larger of five seconds or reported stop time plus
two seconds (capped at five minutes), with a 30-second default when timing is unavailable. Expiry forces the
same completed state so a backend that starts but never signals EOF cannot hold `0x21c` indefinitely. If the
reported combat stall survives this guard, capture the VM/service coordinate after `0x2518` and treat it as
a separate BTL timed-presentation bug.
The remaining implementation boundary is the already-planned decoder interface/factory plus a software The remaining implementation boundary is the already-planned decoder interface/factory plus a software
MPEG backend that handles the installed non-16-aligned effects, and preservation of the destination MPEG backend that handles the installed non-16-aligned effects, and preservation of the destination
surface's created dimensions instead of replacing every movie surface with the SC0000-specific 800x600 surface's created dimensions instead of replacing every movie surface with the SC0000-specific 800x600
value. Failure must remain nonblocking and suppress bogus still-AGF fallback. Regressions must cover at value. Failure is now nonblocking and suppresses bogus still-AGF fallback. Regressions must cover at
least a 280x352 effect (`MVB001`), a 400x400 effect (`MVB914`), immediate `0x23f` stop time, RGBA frame least a 280x352 effect (`MVB001`), a 400x400 effect (`MVB914`), immediate `0x23f` stop time, RGBA frame
publication, completion, failure completion, and release. publication, completion, failure completion, and release.

View File

@@ -1268,11 +1268,14 @@ initialization returns. It applies native seconds-to-milliseconds truncation and
`IHost.PlayMovieToSurface` into a movie-surface record owned by `GfxState`. Decoder construction moved from `IHost.PlayMovieToSurface` into a movie-surface record owned by `GfxState`. Decoder construction moved from
the deferred Godot callback to the VM-side synchronous open boundary; the ready decoder is staged in a the deferred Godot callback to the VM-side synchronous open boundary; the ready decoder is staged in a
thread-safe pending registry and adopted by the main thread before frame sampling, preserving asynchronous thread-safe pending registry and adopted by the main thread before frame sampling, preserving asynchronous
presentation. `0x23f` silently returns -1 for an empty movie slot. If the movie record exists but DirectShow presentation. `0x23f` silently returns -1 for an empty movie slot. Native ignores the HRESULT for a non-null
returned an error, non-finite value, or value outside native signed-32-bit range, the port emits movie's stop-time query and has no meaningful contract for the port-only case where the installed host
`movie stop-time unavailable ...; returning -1` and returns -1. This is the chosen safe substitute for decoder cannot construct a graph for a shipped asset. The port therefore models backend failure as an
native's ignored-HRESULT/uninitialized-output edge case. A normal Game Start through SC0000 was manually explicitly completed movie with stop time 0. This gives BTL an immediate-effect duration rather than feeding
validated without the warning, confirming that the installed movie's ordinary metadata path succeeds. its timeline -1. Started decoders also have a duration-based completion watchdog (minimum five seconds,
reported stop time plus two seconds, maximum five minutes; 30 seconds without timing) so missing EOF cannot
hold the shared `0x21c` wait indefinitely. A normal Game Start through SC0000 was manually validated with
ordinary positive timing.
### Grey-background root cause — slot collision + tint-strength (2026-07-08, gfx-log) ### Grey-background root cause — slot collision + tint-strength (2026-07-08, gfx-log)

View File

@@ -786,11 +786,11 @@ First erase digit_capacity objects beginning at base_handle. Then split value by
For each fixed slot in [42,1000), the handler stops/releases the movie-to-texture object at ctx+0x52bd4[slot], then invokes the ordinary retained-gfx surface-release worker. Protected/externally owned slots may be retained by the worker's per-slot guard. This is the resource half of the common 0x1f6/0x23d full-reset sequence. For each fixed slot in [42,1000), the handler stops/releases the movie-to-texture object at ctx+0x52bd4[slot], then invokes the ordinary retained-gfx surface-release worker. Protected/externally owned slots may be retained by the worker's per-slot guard. This is the resource half of the common 0x1f6/0x23d full-reset sequence.
### 0x23f `query-surface-stop-time-ms` (u00422930, argc 2) ### 0x23f `query-surface-stop-time-ms` (u00422930, argc 2)
- **summary:** (out_stop_time_ms)(surface_slot) — query the DirectShow stop position retained by a loaded movie surface, convert seconds to integer milliseconds by truncating toward zero, and write -1 when the movie slot is empty. The port retains this metadata during 0x236 graph initialization; unavailable metadata emits a warning and also returns -1. - **summary:** (out_stop_time_ms)(surface_slot) — query the DirectShow stop position retained by a loaded movie surface, convert seconds to integer milliseconds by truncating toward zero, and write -1 when the movie slot is empty. Port-only host decoder failure is modeled as an explicitly completed, zero-duration movie.
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x23f_query_surface_stop_time_ms@0x42a520 indexes EngineCtx surface array operand 2, returns -1 for a null slot, otherwise dereferences movie+0x414 IMediaPosition and calls vtable+0x28 get_StopTime. The adjacent op 0x23e uses the same interface at vtable+0x24 get_CurrentPosition; movie op 0x245 uses +0x20 put_CurrentPosition, independently confirming the documented vtable layout. The returned seconds are multiplied by g_dMillisecondsPerSecond@0x5713e8 (double 1000.0) and truncated by crt_ftol2_sse_truncate@0x550850 before vm_operand_write(1). All 23 Himegari sites in 17 scripts are associated with a preceding op 0x236 movie load to the queried surface. FIELD divides one result by 16 and adds 1 to build a 16 ms callback schedule; another path clamps the result to 600 ms before DRAWVOL. - **evidence:** Ghidra /v2: op_0x23f_query_surface_stop_time_ms@0x42a520 indexes EngineCtx surface array operand 2, returns -1 for a null slot, otherwise dereferences movie+0x414 IMediaPosition and calls vtable+0x28 get_StopTime. The adjacent op 0x23e uses the same interface at vtable+0x24 get_CurrentPosition; movie op 0x245 uses +0x20 put_CurrentPosition, independently confirming the documented vtable layout. The returned seconds are multiplied by g_dMillisecondsPerSecond@0x5713e8 (double 1000.0) and truncated by crt_ftol2_sse_truncate@0x550850 before vm_operand_write(1). All 23 Himegari sites in 17 scripts are associated with a preceding op 0x236 movie load to the queried surface. FIELD divides one result by 16 and adds 1 to build a 16 ms callback schedule; another path clamps the result to 600 ms before DRAWVOL.
The surface object's +0x414 member is IMediaPosition. Its vtable +0x28 entry is get_StopTime (after IUnknown, IDispatch, get_Duration, put_CurrentPosition, and get_CurrentPosition), returning a REFTIME double in seconds. Native multiplies by the double constant 1000.0 and calls the compiler float-to-integer helper, whose SSE2 and x87 paths both truncate toward zero. It does not inspect the getter HRESULT. For a valid graph the default stop time normally equals media duration, which explains duration-style consumers, but the exact ABI is stop position rather than get_Duration. The handler only queries state; it does not yield or alter playback. Port safety extension: a modeled movie surface whose decoder cannot supply a finite signed-32-bit stop time reports a warning and returns -1 instead of reproducing native's uninitialized-output failure path. The surface object's +0x414 member is IMediaPosition. Its vtable +0x28 entry is get_StopTime (after IUnknown, IDispatch, get_Duration, put_CurrentPosition, and get_CurrentPosition), returning a REFTIME double in seconds. Native multiplies by the double constant 1000.0 and calls the compiler float-to-integer helper, whose SSE2 and x87 paths both truncate toward zero. It does not inspect the getter HRESULT. For a valid graph the default stop time normally equals media duration, which explains duration-style consumers, but the exact ABI is stop position rather than get_Duration. The handler only queries state; it does not yield or alter playback. Native has no meaningful answer for a port host that cannot build a graph for a valid shipped MPEG. Port safety extension: op 0x236 normalizes missing host metadata to stop time 0 and marks decoder failure completed, while a truly empty movie slot still returns -1.
### 0x242 `set-object-animation-detached` (set-object-animation-detached, argc 2) ### 0x242 `set-object-animation-detached` (set-object-animation-detached, argc 2)
- **summary:** Replace the retained object's animation-control word at obj+0x2d0. Bit 0 detaches finite one-shot channels from blocking presentation and protects them from 0x243 forced completion until they finish naturally. - **summary:** Replace the retained object's animation-control word at obj+0x2d0. Bit 0 detaches finite one-shot channels from blocking presentation and protects them from 0x243 forced completion until they finish naturally.

View File

@@ -577,12 +577,14 @@ resolution; VM surface state and Godot caches retain the full selector. Focused
texture/voice ids and append-pack identity; 302 engine tests, a zero-warning Godot build, and threaded texture/voice ids and append-pack identity; 302 engine tests, a zero-warning Godot build, and threaded
selftest pass. Manual combat acceptance confirms the split: 400x400 MVB908 plays and completes, while selftest pass. Manual combat acceptance confirms the split: 400x400 MVB908 plays and completes, while
280x352 MVB961/MVB238 resolve correctly but DirectShow rejects their graph connection with `0x80040217`. 280x352 MVB961/MVB238 resolve correctly but DirectShow rejects their graph connection with `0x80040217`.
The user observed a stall after the sequence; failed movies are marked complete and the successful movie's The user observed a stall after the sequence. Static BTL tracing proves failed `0x23f == -1` cannot create
stop log proves the shared movie wait resumed, but failed `0x23f` duration remains `-1` and the final stalled an infinite callback count: the total horizon starts at `base_time + 1000`, and the explicit post-callback
VM/service coordinate was not logged. **NEXT:** replace the decoder boundary with a software MPEG path that loop polls `0x23a` until surfaces 7..10 are inactive. MVB908's stop log occurs after that loop. As a bounded
returns real duration/frames for 280x352 effects and regress failure as nonblocking. If the stall remains, safety correction, decoder failure now becomes an explicitly completed zero-duration effect, while started
capture it as a separate BTL timed-presentation issue. Preserve created destination dimensions and failed movies have a stop-time-based completion watchdog so a missing EOF cannot hold `0x21c` forever. **NEXT:**
movie identity through the backend change. manually recheck the same exchange. If it still stalls, capture the VM/service coordinate after BTL cleanup
as a separate timed-presentation issue; otherwise proceed to the software MPEG backend for real 280x352
duration/frames. Preserve created destination dimensions and failed-movie identity through that backend change.
**Mutable-surface fill/blend regression corrected.** The first visual recheck exposed BUNKI's menu interior **Mutable-surface fill/blend regression corrected.** The first visual recheck exposed BUNKI's menu interior
as transparent. SYSTEM4 creates 800x600 surface 3 and fills it opaque white through `0x20b`; the metadata-only as transparent. SYSTEM4 creates 800x600 surface 3 and fills it opaque white through `0x20b`; the metadata-only

View File

@@ -171,13 +171,14 @@ public class MovieOpcodeTests
} }
[Fact] [Fact]
public void QueryMovieStopTimeWarnsAndReturnsMinusOneWhenMetadataIsUnavailable() public void LoadedMovieWithoutBackendMetadataUsesImmediateZeroDuration()
{ {
var table = OpcodeTableJson.Load(Paths.OpcodesJson); var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var script = ScriptAssembler.Assemble(table, "MISSING-MOVIE-TIME", new List<(int, Operand[])> var script = ScriptAssembler.Assemble(table, "MISSING-MOVIE-TIME", new List<(int, Operand[])>
{ {
(0x236, new[] { new Operand(0, 0x33), new Operand(0, 5), new Operand(0, 2), new Operand(0, 0) }), (0x236, new[] { new Operand(0, 0x33), new Operand(0, 5), new Operand(0, 2), new Operand(0, 0) }),
(0x23f, new[] { new Operand(3, 0x1234), new Operand(0, 5) }), (0x23f, new[] { new Operand(3, 0x1234), new Operand(0, 5) }),
(0x23a, new[] { new Operand(3, 0x1235), new Operand(0, 5) }),
(0x2, System.Array.Empty<Operand>()), (0x2, System.Array.Empty<Operand>()),
}, System.Array.Empty<string>()); }, System.Array.Empty<string>());
var host = new RecordingHost(); var host = new RecordingHost();
@@ -185,10 +186,11 @@ public class MovieOpcodeTests
vm.Run(); vm.Run();
Assert.Equal(-1, vm.Globals[0x1234]); Assert.Equal(0, vm.Globals[0x1234]);
string warning = Assert.Single(host.Warnings); Assert.True(vm.Gfx.TryGetMovieStopTime(5, out long? retained));
Assert.Contains("movie stop-time unavailable MISSING-MOVIE-TIME@", warning); Assert.Equal(0, retained);
Assert.Contains("surface=5; returning -1", warning); Assert.Equal(0, vm.Globals[0x1235]);
Assert.Empty(host.Warnings);
} }
[Fact] [Fact]

View File

@@ -1684,7 +1684,10 @@ public sealed class VirtualMachine
// The native CMovieToTexture renderer replaces the pixels of the already-created surface. // The native CMovieToTexture renderer replaces the pixels of the already-created surface.
// Retain the same resource binding so the compositor resolves live movie frames for its objects. // Retain the same resource binding so the compositor resolves live movie frames for its objects.
Gfx.SetSurface(surfaceSlot, resourceId, 0); Gfx.SetSurface(surfaceSlot, resourceId, 0);
Gfx.SetMovieStopTime(surfaceSlot, stopTimeMs); // Native never encounters a missing system decoder for shipped assets. If a host backend
// cannot initialize one, model the valid movie as completing immediately: BTL feeds this
// value into its effect timeline, where zero is a safe duration and -1 is not meaningful.
Gfx.SetMovieStopTime(surfaceSlot, stopTimeMs ?? 0);
return pc + 1; // native cmd size 9 resumes at the next instruction; playback is asynchronous return pc + 1; // native cmd size 9 resumes at the next instruction; playback is asynchronous
} }
// ---- gfx command-buffer ops (VM-internal GfxState; docs/engine-re.md op-contract table) ---- // ---- gfx command-buffer ops (VM-internal GfxState; docs/engine-re.md op-contract table) ----

View File

@@ -897,8 +897,9 @@ public sealed class GodotAdvHost : IHost
string scene = CurrentScene; string scene = CurrentScene;
var asset = _res.ResolveMovie(resourceId); var asset = _res.ResolveMovie(resourceId);
if (asset == null) { Godot.GD.Print($"movie unresolved {scene}:0x{resourceId:x}"); return null; } if (asset == null) { Godot.GD.Print($"movie unresolved {scene}:0x{resourceId:x}"); return null; }
return StartMovie(asset, resourceId, surfaceSlot, movieFlags, syncMask, modal: false, StartMovie(asset, resourceId, surfaceSlot, movieFlags, syncMask, modal: false,
out long? stopTimeMs) ? stopTimeMs : null; out long? stopTimeMs);
return stopTimeMs ?? 0;
} }
public bool IsMovieSurfaceActive(int surfaceSlot) public bool IsMovieSurfaceActive(int surfaceSlot)
@@ -972,7 +973,14 @@ public sealed class GodotAdvHost : IHost
["resource"] = resourceId, ["surface"] = surfaceSlot, ["file"] = movie.Name, ["resource"] = resourceId, ["surface"] = surfaceSlot, ["file"] = movie.Name,
["flags"] = movieFlags, ["sync_mask"] = syncMask, ["modal"] = modal, ["flags"] = movieFlags, ["sync_mask"] = syncMask, ["modal"] = modal,
}); });
return _main.TryPlayMovie(movie.Bytes, movie.Name, resourceId, asset.PackedId, out stopTimeMs); bool started = _main.TryPlayMovie(movie.Bytes, movie.Name, resourceId, asset.PackedId,
out stopTimeMs);
if (!started)
{
stopTimeMs = 0;
NotifyMovieCompleted(resourceId);
}
return started;
} }
catch (System.Exception e) catch (System.Exception e)
{ {
@@ -984,6 +992,8 @@ public sealed class GodotAdvHost : IHost
_completedMovies.Remove(resourceId); _completedMovies.Remove(resourceId);
} }
_slotDims.Remove(surfaceSlot); _slotDims.Remove(surfaceSlot);
stopTimeMs = 0;
NotifyMovieCompleted(resourceId);
Godot.GD.Print($"movie read failed {asset.Name}: {e.Message}"); Godot.GD.Print($"movie read failed {asset.Name}: {e.Message}");
return false; return false;
} }

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Text.Json; using System.Text.Json;
@@ -56,6 +57,7 @@ public partial class Main : Godot.Control
// Presentation ownership transfers here; _Process adopts staged decoders before sampling frames. // Presentation ownership transfers here; _Process adopts staged decoders before sampling frames.
private readonly System.Collections.Concurrent.ConcurrentDictionary<long, MovieRuntime> _pendingMovies = new(); private readonly System.Collections.Concurrent.ConcurrentDictionary<long, MovieRuntime> _pendingMovies = new();
private readonly System.Collections.Generic.HashSet<long> _movieFrameSeen = new(); private readonly System.Collections.Generic.HashSet<long> _movieFrameSeen = new();
private readonly System.Collections.Generic.HashSet<long> _movieCompletionNotified = new();
private GodotTraceSink _trace = null!; private GodotTraceSink _trace = null!;
private PageLocatorState _locator = null!; private PageLocatorState _locator = null!;
private bool _locatorHudVisible; private bool _locatorHudVisible;
@@ -1190,7 +1192,6 @@ public partial class Main : Godot.Control
catch (System.Exception e) catch (System.Exception e)
{ {
GD.Print($"movie decode failed {assetName}: {e.Message}"); GD.Print($"movie decode failed {assetName}: {e.Message}");
_host.NotifyMovieCompleted(resourceId); // release a pending 0x21c boundary on deterministic load failure
return false; return false;
} }
} }
@@ -1202,6 +1203,7 @@ public partial class Main : Godot.Control
if (!_pendingMovies.TryRemove(resourceId, out var movie)) continue; if (!_pendingMovies.TryRemove(resourceId, out var movie)) continue;
if (_movies.Remove(resourceId, out var prior)) prior.Decoder.Dispose(); if (_movies.Remove(resourceId, out var prior)) prior.Decoder.Dispose();
_movies[resourceId] = movie; _movies[resourceId] = movie;
_movieCompletionNotified.Remove(resourceId);
GD.Print($"movie started {movie.Name} ({movie.Decoder.StopTimeMs?.ToString() ?? "unknown"} ms from VFS)"); GD.Print($"movie started {movie.Name} ({movie.Decoder.StopTimeMs?.ToString() ?? "unknown"} ms from VFS)");
} }
} }
@@ -1217,7 +1219,14 @@ public partial class Main : Godot.Control
if (_movieFrameSeen.Add(resourceId)) if (_movieFrameSeen.Add(resourceId))
GD.Print($"movie first frame {movie.Name}: {frame.Width}x{frame.Height} RGBA8 at render frame {_timelineFrame}"); GD.Print($"movie first frame {movie.Name}: {frame.Width}x{frame.Height} RGBA8 at render frame {_timelineFrame}");
} }
if (movie.Decoder.IsCompleted) _host.NotifyMovieCompleted(resourceId); bool watchdogExpired = Stopwatch.GetElapsedTime(movie.StartedAtTimestamp).TotalMilliseconds
>= movie.WatchdogMs;
if ((movie.Decoder.IsCompleted || watchdogExpired) && _movieCompletionNotified.Add(resourceId))
{
if (watchdogExpired && !movie.Decoder.IsCompleted)
GD.Print($"movie completion watchdog {movie.Name}: forcing completion after {movie.WatchdogMs} ms");
_host.NotifyMovieCompleted(resourceId);
}
} }
} }
@@ -1230,9 +1239,18 @@ public partial class Main : Godot.Control
GD.Print($"movie stopped {movie.Name} at render frame {_timelineFrame}"); GD.Print($"movie stopped {movie.Name} at render frame {_timelineFrame}");
} }
_movieFrameSeen.Remove(resourceId); _movieFrameSeen.Remove(resourceId);
_movieCompletionNotified.Remove(resourceId);
} }
private sealed record MovieRuntime(string Name, int AssetId, DirectShowMovieDecoder Decoder); private sealed record MovieRuntime(string Name, int AssetId, DirectShowMovieDecoder Decoder,
long StartedAtTimestamp, long WatchdogMs)
{
public MovieRuntime(string name, int assetId, DirectShowMovieDecoder decoder)
: this(name, assetId, decoder, Stopwatch.GetTimestamp(),
decoder.StopTimeMs is >= 0 and var stopTime
? System.Math.Clamp(stopTime + 2000, 5000, 300000)
: 30000) { }
}
public void AppendLine(string text) => _text.Text += text + "\n"; public void AppendLine(string text) => _text.Text += text + "\n";
public void PageBreak() public void PageBreak()

View File

@@ -75,7 +75,7 @@ INFERRED: dict[int, dict] = {
0x232: dict(name='animate-gfx-color-loop', category='draw', noop=False, confidence='high', source='investigation', summary='0x232 anim-color (handle)(period)(alpha)(color): ping-pong the temporary packed ARGB passed to the normal object blit. Handler resolves negative alpha/RGB from static color obj+0x60 and clamps alpha above 255. Blend selector obj+0x30 is unchanged: mode 0 keeps default blending (animated alpha is inert; RGB is vertex modulation), while mode 1 uses sampled ARGB alpha as the SRCALPHA scale for additive composition. Fresh static color is 0xffffffff. The C# VM resolves sentinels and consumes sampled ARGB through the unchanged mode-specific path. See docs/engine-re.md §SC0000 anim cluster.'), 0x232: dict(name='animate-gfx-color-loop', category='draw', noop=False, confidence='high', source='investigation', summary='0x232 anim-color (handle)(period)(alpha)(color): ping-pong the temporary packed ARGB passed to the normal object blit. Handler resolves negative alpha/RGB from static color obj+0x60 and clamps alpha above 255. Blend selector obj+0x30 is unchanged: mode 0 keeps default blending (animated alpha is inert; RGB is vertex modulation), while mode 1 uses sampled ARGB alpha as the SRCALPHA scale for additive composition. Fresh static color is 0xffffffff. The C# VM resolves sentinels and consumes sampled ARGB through the unchanged mode-specific path. See docs/engine-re.md §SC0000 anim cluster.'),
0x239: dict(name='animate-gfx-srcrect-target', category='draw', noop=False, confidence='high', source='investigation', summary='(handle)(delay_ms)(duration_ms)(frame_count)(column_count)(target_frame) — one-shot row-major source-rectangle cell channel. Worker gfx_worker_set_srcrect_cell @0x47ed90 stores timing at obj+0x48/+0x5c, layout at +0x238/+0x23c, and target at +0x234. C# currently retains the endpoint cell immediately.'), 0x239: dict(name='animate-gfx-srcrect-target', category='draw', noop=False, confidence='high', source='investigation', summary='(handle)(delay_ms)(duration_ms)(frame_count)(column_count)(target_frame) — one-shot row-major source-rectangle cell channel. Worker gfx_worker_set_srcrect_cell @0x47ed90 stores timing at obj+0x48/+0x5c, layout at +0x238/+0x23c, and target at +0x234. C# currently retains the endpoint cell immediately.'),
0x23b: dict(name='draw-decimal-glyphs', category='draw', noop=False, confidence='high', source='investigation', summary='Draw an integer as decimal glyph objects from a style registered by opcode 0x13a.'), 0x23b: dict(name='draw-decimal-glyphs', category='draw', noop=False, confidence='high', source='investigation', summary='Draw an integer as decimal glyph objects from a style registered by opcode 0x13a.'),
0x23f: dict(name='query-surface-stop-time-ms', category='draw', noop=False, confidence='high', source='investigation', summary='(out_stop_time_ms)(surface_slot) — query the DirectShow stop position retained by a loaded movie surface, convert seconds to integer milliseconds by truncating toward zero, and write -1 when the movie slot is empty. The port retains this metadata during 0x236 graph initialization; unavailable metadata emits a warning and also returns -1.'), 0x23f: dict(name='query-surface-stop-time-ms', category='draw', noop=False, confidence='high', source='investigation', summary='(out_stop_time_ms)(surface_slot) — query the DirectShow stop position retained by a loaded movie surface, convert seconds to integer milliseconds by truncating toward zero, and write -1 when the movie slot is empty. Port-only host decoder failure is modeled as an explicitly completed, zero-duration movie.'),
0x258: dict(name='decl?', category='marker', noop=True, confidence='low', source='harness', summary='2 imm; runs in a chain right after script-entry 0x259, enumerating ids — prologue declaration/registration?'), 0x258: dict(name='decl?', category='marker', noop=True, confidence='low', source='harness', summary='2 imm; runs in a chain right after script-entry 0x259, enumerating ids — prologue declaration/registration?'),
0x259: dict(name='script-entry', category='marker', noop=True, confidence='low', source='harness', summary='zero-arg; the first instruction of a script (offset 0), opens the decl chain that 0x258 continues — script/prologue entry marker, structural'), 0x259: dict(name='script-entry', category='marker', noop=True, confidence='low', source='harness', summary='zero-arg; the first instruction of a script (offset 0), opens the decl chain that 0x258 continues — script/prologue entry marker, structural'),
0x2c5: dict(name='byte-string-length', category='compute', noop=False, confidence='high', source='investigation', summary="Write the resolved NUL-terminated engine string's raw byte length."), 0x2c5: dict(name='byte-string-length', category='compute', noop=False, confidence='high', source='investigation', summary="Write the resolved NUL-terminated engine string's raw byte length."),

View File

@@ -6396,12 +6396,12 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "query-surface-stop-time-ms" name = "query-surface-stop-time-ms"
category = "draw" category = "draw"
summary = "(out_stop_time_ms)(surface_slot) — query the DirectShow stop position retained by a loaded movie surface, convert seconds to integer milliseconds by truncating toward zero, and write -1 when the movie slot is empty. The port retains this metadata during 0x236 graph initialization; unavailable metadata emits a warning and also returns -1." summary = "(out_stop_time_ms)(surface_slot) — query the DirectShow stop position retained by a loaded movie surface, convert seconds to integer milliseconds by truncating toward zero, and write -1 when the movie slot is empty. Port-only host decoder failure is modeled as an explicitly completed, zero-duration movie."
noop_headless = false noop_headless = false
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"
depends_on = [] depends_on = []
details = "The surface object's +0x414 member is IMediaPosition. Its vtable +0x28 entry is get_StopTime (after IUnknown, IDispatch, get_Duration, put_CurrentPosition, and get_CurrentPosition), returning a REFTIME double in seconds. Native multiplies by the double constant 1000.0 and calls the compiler float-to-integer helper, whose SSE2 and x87 paths both truncate toward zero. It does not inspect the getter HRESULT. For a valid graph the default stop time normally equals media duration, which explains duration-style consumers, but the exact ABI is stop position rather than get_Duration. The handler only queries state; it does not yield or alter playback. Port safety extension: a modeled movie surface whose decoder cannot supply a finite signed-32-bit stop time reports a warning and returns -1 instead of reproducing native's uninitialized-output failure path." details = "The surface object's +0x414 member is IMediaPosition. Its vtable +0x28 entry is get_StopTime (after IUnknown, IDispatch, get_Duration, put_CurrentPosition, and get_CurrentPosition), returning a REFTIME double in seconds. Native multiplies by the double constant 1000.0 and calls the compiler float-to-integer helper, whose SSE2 and x87 paths both truncate toward zero. It does not inspect the getter HRESULT. For a valid graph the default stop time normally equals media duration, which explains duration-style consumers, but the exact ABI is stop position rather than get_Duration. The handler only queries state; it does not yield or alter playback. Native has no meaningful answer for a port host that cannot build a graph for a valid shipped MPEG. Port safety extension: op 0x236 normalizes missing host metadata to stop time 0 and marks decoder failure completed, while a truly empty movie slot still returns -1."
evidence = "Ghidra /v2: op_0x23f_query_surface_stop_time_ms@0x42a520 indexes EngineCtx surface array operand 2, returns -1 for a null slot, otherwise dereferences movie+0x414 IMediaPosition and calls vtable+0x28 get_StopTime. The adjacent op 0x23e uses the same interface at vtable+0x24 get_CurrentPosition; movie op 0x245 uses +0x20 put_CurrentPosition, independently confirming the documented vtable layout. The returned seconds are multiplied by g_dMillisecondsPerSecond@0x5713e8 (double 1000.0) and truncated by crt_ftol2_sse_truncate@0x550850 before vm_operand_write(1). All 23 Himegari sites in 17 scripts are associated with a preceding op 0x236 movie load to the queried surface. FIELD divides one result by 16 and adds 1 to build a 16 ms callback schedule; another path clamps the result to 600 ms before DRAWVOL." evidence = "Ghidra /v2: op_0x23f_query_surface_stop_time_ms@0x42a520 indexes EngineCtx surface array operand 2, returns -1 for a null slot, otherwise dereferences movie+0x414 IMediaPosition and calls vtable+0x28 get_StopTime. The adjacent op 0x23e uses the same interface at vtable+0x24 get_CurrentPosition; movie op 0x245 uses +0x20 put_CurrentPosition, independently confirming the documented vtable layout. The returned seconds are multiplied by g_dMillisecondsPerSecond@0x5713e8 (double 1000.0) and truncated by crt_ftol2_sse_truncate@0x550850 before vm_operand_write(1). All 23 Himegari sites in 17 scripts are associated with a preceding op 0x236 movie load to the queried surface. FIELD divides one result by 16 and adds 1 to build a 16 ms callback schedule; another path clamps the result to 600 ms before DRAWVOL."
[[opcode.semantics.args]] [[opcode.semantics.args]]