Land SYSTEM4-rooted Godot boot
This commit is contained in:
@@ -75,13 +75,13 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
|
||||
AGE game with the same container — **the "scope" was just which SYS4INI section the scene lives in.**
|
||||
(The old `play-bgm 5→BGM006` validation point was a mis-attribution — the real game plays BGM005.)
|
||||
|
||||
**System/global-id exception (identified 2026-07-10; not yet implemented).** Some SYSTEM4 loads use
|
||||
**System/global-id exception (identified 2026-07-10; implemented).** Some SYSTEM4 loads use
|
||||
the SYS4INI record's universal `raw_index` directly, including the two `@` placeholder records, rather
|
||||
than a scene-local manifest index. `SYSTEM4.BIN` writes `G[0x69b]=0x337e`, then
|
||||
`set-texture(G[0x69b], slot=0x11)`. SYS4INI `raw_index 0x337e` is `DATA1/SO001.AGF`, the shared
|
||||
800×300 RGBA system-chrome sheet. The filtered `files[]` list omits placeholders, so treating `0x337e`
|
||||
as a `files[]` position currently mis-resolves it to `SETROUTE.BIN`. This path needs a distinct
|
||||
`raw_index → entry` lookup; the scene-manifest rule above remains correct for SC texture/voice ids.
|
||||
800×300 RGBA system-chrome sheet. `ResourceMap.ResolveTexture` therefore tries the active script's
|
||||
scene manifest first and then the distinct universal raw-id lookup; the scene-manifest rule above
|
||||
remains correct for ordinary SC texture/voice ids.
|
||||
|
||||
*How we got here (condensed):* first confirmed `resId == file_number` via Frida load-order correlation
|
||||
for SC0000's opening, but `file_number` is not globally unique so a per-scene "scope" was needed. A long
|
||||
@@ -104,16 +104,18 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
|
||||
model approximated the game's immediate-mode blit-onto-slot-0 canvas. See `docs/phase-a-slice-plan.md`
|
||||
(A2b section) for the implementation history and current retained-object model.
|
||||
|
||||
**Current system-chrome shortcut/gap (confirmed 2026-07-10).** `Main --boot` executes only
|
||||
`INITCONFIG/INIT2/INIT` through `CaptureHost` and copies their globals into the scene VM; it does not
|
||||
replay SYSTEM4's graphics side effects through `GodotAdvHost`. `convert_agf.py --scene SC0000` also
|
||||
converts only SC0000's manifest, so `build/textures/SO001.BMP` is absent. `CALLBACK_WINDOW.BIN` expects
|
||||
slot `0x11` to already contain SO001, draws the 800×227 textbox from `(0,0)`, and crops the lower-right
|
||||
buttons from the same sheet. In the port slot 17 is unpopulated, so retained handle `0xd2f0` resolves as
|
||||
a colored surfaceless object and the compositor draws the observed opaque black fill. A temporary decode
|
||||
verified SO001 is 800×300, 32-bpp, with substantial per-pixel alpha; the current rasterizer already
|
||||
consumes source alpha. The missing prerequisites are system-asset resolution/conversion and retained
|
||||
slot initialization, not new textbox drawing or button interaction.
|
||||
**System chrome ownership (resolved 2026-07-20).** The normal Godot entry now runs SYSTEM4 as the live
|
||||
root, so its SO000/SO001 loads, ADV-layout definitions, and retained slot initialization execute through
|
||||
`GodotAdvHost` before TITLE and child scenes. Asset lookup decodes VFS-owned AGF bytes directly and does
|
||||
not depend on a scene-limited converted BMP. `CALLBACK_WINDOW.BIN` consequently inherits slot `0x11`
|
||||
with SO001 and can crop its textbox/buttons normally. The explicit `--scene SC0000 --boot` diagnostic
|
||||
still injects the same known inherited layout and surface state because it intentionally bypasses
|
||||
SYSTEM4; that shortcut is no longer the shipped/default route.
|
||||
|
||||
The active manifest is frame-local, not fixed to the root scene: every VM call frame brackets host work
|
||||
with its script context. `set-texture` resolves the local id at load time and retains the normalized raw
|
||||
catalog id in the graphics surface, so that surface remains stable after a nested helper returns or a
|
||||
sibling script becomes active. Voice, SFX, and movie calls likewise resolve against the executing frame.
|
||||
4. **Audio.** **✅ WIRED (2026-07-06), VFS bytes complete (2026-07-11).** `IHost.PlayBgm/PlayVoice` +
|
||||
VM dispatch (`play-bgm` 0xbf / `play-voice` 0xc4, both argc 1); `ResourceMap.ReadAudio` opens the
|
||||
resolved catalog entry through `IAssetStore`; `GodotAdvHost` passes the bytes to `Main`'s players
|
||||
|
||||
@@ -245,9 +245,9 @@ optionally calls `LOGO.BIN` and `OP.BIN`, calls the one-op `INIT.BIN`, and enter
|
||||
not a thin handle seed: it calls 23 data initializers in order (`EBINIT`, `CNINIT`, `ITINIT`, `SKINIT`,
|
||||
`ILINIT`, `AFINIT`, `TRINIT`, `MAINIT`, `ALINIT`, `CDINIT2`, `MPINIT`, `LAINIT`, `OBINIT`, `STINIT2`,
|
||||
`RTINIT`, `CGINIT`, `SPINIT`, `CTINIT`, `CVINIT`, `CIINIT`, `VIINIT`, `SCINIT`, `BTANINIT2`) and then
|
||||
`TUNE.BIN`. The CLI `play --boot` nine-script list is therefore only a partial diagnostic approximation;
|
||||
the Godot `--boot` path reaches the complete list indirectly because it executes `INIT2` with call-script
|
||||
resolution enabled.
|
||||
`TUNE.BIN`. The CLI `play --boot` nine-script list is therefore only a partial diagnostic approximation.
|
||||
Godot now runs SYSTEM4 itself by default, so this complete sequence and its host-visible side effects execute
|
||||
in one VM; Godot `--boot` remains only for an explicit direct-scene diagnostic such as `--scene SC0000`.
|
||||
|
||||
An existing native operand trace identifies every observed heap codebase by a 100% match against its static
|
||||
instruction-offset set. The captured New Game route is:
|
||||
@@ -262,6 +262,11 @@ SYS4INI id `0x22` (`SC0000.BIN`) when the mapping is zero, and executes computed
|
||||
Thus normal scenes remain nested script frames under SYSTEM4 and return to it; the port should keep one
|
||||
VM/host session rooted at SYSTEM4 rather than replace top-level VMs based on a host-invented scene result.
|
||||
|
||||
**Port landing (2026-07-20).** The no-argument Godot path is rooted at SYSTEM4 and renders TITLE without
|
||||
manual inherited-layout or surface injection. A real-script integration test drives the same input callback
|
||||
lifecycle through TITLE and GAMESTART and observes SYSTEM4 enter SC0000 with `G[0]=1`, `G[0x699]=0x22`,
|
||||
and script-produced `G[0x6c1]=1`. Direct `--scene` launches retain the old bootstrap strictly as a diagnostic.
|
||||
|
||||
`tools/frida/capture_script_loads.py` hooks `script_frame_load_resource@0x40e980` and reads its third stack
|
||||
argument (the raw packed resource id) for direct name resolution. It is attach-only: attempting to gate the
|
||||
installed executable at process start with this loader hook produced Protection Error 45 and no records.
|
||||
@@ -852,18 +857,26 @@ Ghidra functions renamed + plate-commented, saved).
|
||||
(`EngineCtx+0x51b64`), not op `0x238`.
|
||||
- **`0x203` (`gfx_op_0x203_worker_set_color` `0x47e9b0`)**: sets a **static** color/alpha `obj+0x60`, no anim
|
||||
bit. Immediate per-object modulation.
|
||||
- **Blit** (`gfx_object_blit_d3d9` `0x4774c0`): selects a **blend mode** (`local_2c`: 0 opaque, 1 alpha
|
||||
`SRCALPHA/INVSRCALPHA`, 2/3 additive/special for glow/flash) and passes a modulation color/alpha to the
|
||||
device draw. Slice A ports the **alpha** path (fades); additive (glow) is deferred (its `local_2c` source
|
||||
field is `obj+0x30`, the value written by op `0x203`. Mode 1 uses packed ARGB alpha as opacity and RGB
|
||||
as multiplicative D3D modulation. For a **textured mode-0** object, preserved `0xffffffff` is opaque
|
||||
- **Blit** (`gfx_object_blit_d3d9` `0x4774c0`): selects a **blend mode** from `obj+0x30`, the value written
|
||||
by op `0x203`, and passes a modulation color/alpha to the device draw. Correcting the D3D9 constants:
|
||||
mode 1 writes `SRCBLEND=SRCALPHA` (5) and `DESTBLEND=ONE` (2), so it is additive glow—not ordinary
|
||||
`SRCALPHA/INVSRCALPHA`. Mode 2 conditionally writes `ONE/ZERO` for a selected render target; mode 3 adds
|
||||
the subtract blend operation to the mode-1 factors. The port implements mode-1 additive composition,
|
||||
where packed alpha scales source contribution and packed RGB multiplicatively modulates it. For a
|
||||
**textured mode-0** object, preserved `0xffffffff` is opaque
|
||||
identity, not a request to replace the texture with white; the packed alpha byte is therefore not a
|
||||
generic tint-strength control. Modes 2/3 remain separately scoped beyond the completed mode-1 path, and
|
||||
generic tint-strength control. Mode 3 remains separately scoped beyond the completed mode-1 path, and
|
||||
surfaceless mode-0 fills remain a distinct consumer case.
|
||||
|
||||
**TITLE SO022 additive proof (2026-07-20).** TITLE loads type-1 8-bpp `SO022.AGF` with no alpha plane and
|
||||
no color key, binds two 140×140 spritesheet objects, and calls `0x203(handle,1,255,0xffffff)` for both.
|
||||
Ordinary alpha composition therefore produces opaque black squares around the blue flames. Native mode-1
|
||||
`SRCALPHA/ONE` makes black contribute zero; the implemented additive raster path removes the rectangles
|
||||
while preserving the animated glow, verified across a windowed SYSTEM4/TITLE capture.
|
||||
|
||||
**SC0000 third-CG white-screen and missing-glow fix (2026-07-11).** The page containing
|
||||
`大役を担ったのは…` reaches the intended EV052DA image and both animated AE001D layers. A synchronized
|
||||
Godot capture proved AE001D is correctly alpha-bearing and drawn in mode 1 at only 6-8% object opacity;
|
||||
Godot capture proved AE001D is correctly alpha-bearing and drawn in mode 1 at only a 6-8% additive source scale;
|
||||
it was not the white wall. During the preceding `0x223` EV052CA→EV052DA crossfade, base handle `0xcb2a`
|
||||
renders EV052DA in mode 2 with identity modulation. When the transition ends, `0x203@0x12478` restores that
|
||||
same textured object to mode 0 with negative color operands, preserving `0xffffffff`. Native keeps EV052DA
|
||||
@@ -908,8 +921,8 @@ alpha/RGB target operands independently preserve their bytes from current `+0x60
|
||||
|
||||
The port now carries current and target separately and samples them from the unified `FrameClock`; an op
|
||||
`0x203` static write after `0x202` therefore becomes the ramp's current value rather than overwriting its
|
||||
target. Mode 0 retains the established CG/tint/fill behavior; mode 1 now uses native alpha opacity plus RGB
|
||||
modulation. `draw-string 0x204`/`0x7a` remains a separate dependency.
|
||||
target. Mode 0 retains the established CG/tint/fill behavior; mode 1 uses native additive composition with
|
||||
ARGB alpha as the source scale plus RGB modulation. `draw-string 0x204`/`0x7a` remains a separate dependency.
|
||||
|
||||
**ADV chrome correction (2026-07-11).** Mode 0 cannot be classified from the final packed color alone.
|
||||
Static `0x203(mode=0, alpha=0, rgb=white)` remains the established opaque/no-tint CG initializer, but a
|
||||
@@ -1004,13 +1017,13 @@ AE001H, exactly matching the reported white pulse. Follow-up native dataflow clo
|
||||
fresh objects initialize static color `obj+0x60` to `0xffffffff`; the interpolator samples that temporary
|
||||
color toward target `obj+0x240`, then `gfx_object_composite` passes the sample and the unchanged blend
|
||||
selector `obj+0x30` to `gfx_object_blit_d3d9`. Mode 0 enables no alpha blending and uses RGB only as vertex
|
||||
modulation, while mode 1 enables SRCALPHA/INVSRCALPHA. AE001H therefore cycles
|
||||
modulation, while mode 1 enables SRCALPHA/ONE additive composition. AE001H therefore cycles
|
||||
`0xffffffff ↔ 0xe0ffffff`: identity RGB throughout, with alpha intentionally inert in mode 0, so native has
|
||||
no visible pulse. The faithful fix is now fully bounded: initialize/resolve static color correctly, sample
|
||||
packed ARGB, and consume it through the existing mode-specific blend path instead of converting animated
|
||||
alpha into tint strength. The port now initializes static color to native identity `0xffffffff`, resolves
|
||||
negative operands in `SetColorAnimResolved`, samples packed ARGB before blend selection, and feeds it through
|
||||
the existing mode-specific path. Exact AE001H, mode-0 RGB-modulation, and mode-1 alpha regressions cover the
|
||||
the existing mode-specific path. Exact AE001H, mode-0 RGB-modulation, and mode-1 additive regressions cover the
|
||||
contract; the white pulse is removed without suppressing the scripted channel.
|
||||
|
||||
**Resolved 2026-07-20:** `0x236` is the movie-to-retained-surface path described below. `0x242` is the
|
||||
|
||||
@@ -387,9 +387,9 @@ The handler clears the map embedded at retained-gfx owner+0x408, resets its coun
|
||||
- **evidence:** Ghidra handler 0x4228d0 packs operands 4/5 and calls worker 0x47ea00(handle,delay,duration,packed). Consumer 0x472f00: shared start +0x34; color delay/duration +0x38/+0x4c; current/target +0x60/+0x64; frame clock retained-gfx owner+0xb550 (EngineCtx+0x51b64); bytewise integer LERP; natural or owner+0xb55c (EngineCtx+0x51b70) forced completion. /v2 annotated and saved 2026-07-10.
|
||||
|
||||
### 0x203 `gfx-draw-color` (gfx-draw-color, argc 4)
|
||||
- **summary:** 0x203 (handle)(mode)(alpha)(color) — worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60; the handler's ctx+0x53d88 write is the generic 9-dword instruction length. Negative alpha/RGB preserve current static bytes. Mode 0 is the opaque textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength); mode 1 is SRCALPHA/INVSRCALPHA with ARGB alpha opacity and multiplicative RGB modulation; mode 2 is the 0x223 transition-source identity path. Surfaceless mode-0 fill consumption remains a distinct case.
|
||||
- **summary:** 0x203 (handle)(mode)(alpha)(color) — worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60; the handler's ctx+0x53d88 write is the generic 9-dword instruction length. Negative alpha/RGB preserve current static bytes. Mode 0 is the default textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength). Mode 1 is SRCALPHA/ONE additive glow with ARGB alpha scaling the source contribution and RGB providing multiplicative modulation. Mode 2 conditionally forces ONE/ZERO for the selected render target and is used by 0x223 transition sources; mode 3 selects a subtractive special path. Surfaceless mode-0 fill consumption remains distinct.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra handler 0x4229a0; negative operands read current obj+0x60, then worker 0x47e9b0 stores op2 at obj+0x30 and ARGB at +0x60. gfx_object_composite call-site 0x47f78f passes +0x30/+0x60 directly to gfx_object_blit_d3d9; mode 1 sets D3DRS SRCALPHA/INVSRCALPHA and the packed color is the device draw modulation. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white. SC0000 page 14 adds the mode-0 endpoint proof: after the EV052CA->EV052DA 0x223 crossfade, 0x203@0x12478 restores the base CG to mode 0 with preserved 0xffffffff; native keeps EV052DA visible while the port's tint-strength interpretation turns every texel white.
|
||||
- **evidence:** Ghidra handler 0x4229a0; negative operands read current obj+0x60, then worker 0x47e9b0 stores op2 at obj+0x30 and ARGB at +0x60. gfx_object_composite call-site 0x47f78f passes +0x30/+0x60 directly to gfx_object_blit_d3d9. Correct D3D9 constants at 0x4774c0 prove mode 1 writes SRCBLEND=5/SRCALPHA and DESTBLEND=2/ONE, not INVSRCALPHA. TITLE draws two opaque-black, no-colorkey SO022 flame sprites with mode 1; additive composition removes black and preserves the blue glow, verified in a windowed capture. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white. SC0000 page 14 adds the mode-0 endpoint proof: after the EV052CA->EV052DA 0x223 crossfade, 0x203@0x12478 restores the base CG to mode 0 with preserved 0xffffffff; native keeps EV052DA visible while the port's tint-strength interpretation turns every texel white.
|
||||
|
||||
### 0x208 `get-texture-size` (get-texture-size, argc 3)
|
||||
- **summary:** 0x208 (slot)(out_w)(out_h) — writes the loaded texture's width/height into two output globals; keystone for bytecode-computed sprite/bg geometry (SC0000 label_12649)
|
||||
@@ -522,9 +522,9 @@ This is a target-pixel operation, not retained-object teardown. It invokes IDire
|
||||
- **evidence:** Native /v2 decompile: worker stores period at obj+0x230, frame_count at +0x238, columns at +0x23c. Interpolator computes ((now-start)/period)%frame_count, then offsets both source-rect X bounds by rect_width*(frame%columns) and Y bounds by rect_height*(frame/columns). SC0000 uses (100,8,4) with AE001H's eight 200x200 cells in a 4x2 800x400 sheet.
|
||||
|
||||
### 0x232 `u00421EF0` (u00421EF0, argc 4)
|
||||
- **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 opaque blending (animated alpha is inert; RGB is vertex modulation), while mode 1 consumes ARGB alpha as opacity. 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.
|
||||
- **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.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: gfx_op_0x232_anim_color@0x423c30 resolves sentinels then calls gfx_worker_anim_color@0x47ef50; gfx_object_anim_interpolate@0x473ed0 samples static obj+0x60 toward target obj+0x240 into a temporary packed color; gfx_object_composite@0x47f650 passes that color plus unchanged selector obj+0x30 to gfx_object_blit_d3d9@0x4774c0. Blit mode 0 enables no alpha blend and passes RGB as modulation; mode 1 enables SRCALPHA/INVSRCALPHA. gfx_object_init_default@0x472810 initializes obj+0x60=0xffffffff. SC0000 0x1a0e (handle,1200,224,-1) is therefore 0xffffffff<->0xe0ffffff with inert alpha and identity RGB: no visible pulse. C# regressions cover exact AE001H visual invariance, negative-RGB preservation, mode-0 RGB modulation, and mode-1 alpha opacity.
|
||||
- **evidence:** Ghidra /v2: gfx_op_0x232_anim_color@0x423c30 resolves sentinels then calls gfx_worker_anim_color@0x47ef50; gfx_object_anim_interpolate@0x473ed0 samples static obj+0x60 toward target obj+0x240 into a temporary packed color; gfx_object_composite@0x47f650 passes that color plus unchanged selector obj+0x30 to gfx_object_blit_d3d9@0x4774c0. Blit mode 0 leaves the default path and passes RGB as modulation; mode 1 sets SRCALPHA/ONE additive composition. gfx_object_init_default@0x472810 initializes obj+0x60=0xffffffff. SC0000 0x1a0e (handle,1200,224,-1) is therefore 0xffffffff<->0xe0ffffff with inert alpha and identity RGB in mode 0: no visible pulse. C# regressions cover exact AE001H visual invariance, negative-RGB preservation, mode-0 RGB modulation, and mode-1 additive scaling.
|
||||
|
||||
### 0x234 `anim-start` (anim-start, argc 5)
|
||||
- **summary:** (handle)(period_ms)(axis_x)(axis_y)(axis_z) — configure cyclic rotation. Worker stores period obj+0x228, start obj+0x214=0, and float axis obj+0x244; each frame uses integer degrees floor(((now-start)%period)*360/period). gfx_object_composite right-multiplies this separately anchored transform after the one-shot scale/rotation/translation product, so cyclic rotation also rotates the translation vector.
|
||||
|
||||
@@ -90,6 +90,15 @@ coverage is 100% for all 23 data initializers, CALCARR, and TUNE; the remaining
|
||||
SYSTEM4-rooted path visible and interactive in Godot, then investigate only the gaps actually reached on
|
||||
that route instead of treating every static gap as a prerequisite.
|
||||
|
||||
**Godot root landing (2026-07-20).** The no-argument Godot/run-godot path now starts SYSTEM4 directly and
|
||||
does not apply the direct-SC0000 layout/surface bootstrap or the diagnostic `--boot` prefix. A windowed run
|
||||
reaches and renders TITLE using SYSTEM4-owned retained state. A real-script integration test drives TITLE's
|
||||
Game Start input, GAMESTART's release-gated default selection, and proves the same VM enters SC0000 through
|
||||
SYSTEM4's computed resource id `G[0x699]=0x22`; `G[0]=1` and the script-produced ADV-chrome flag
|
||||
`G[0x6c1]=1` are present at that boundary. `--scene SC0000 --boot` remains available only as the explicit
|
||||
single-scene diagnostic harness. This lands the boot/title/New Game entry half of B1–B3; proving a completed
|
||||
scene return plus boundary cleanup still belongs to B1 completion.
|
||||
|
||||
## Stage B1 — Persistent session and scene coordinator
|
||||
|
||||
Replace the single-SC0000-root assumption with an application-owned session that runs SYSTEM4 as its root.
|
||||
|
||||
@@ -132,21 +132,24 @@ at 2.5M lines). All observe-only → parity preserved; all on `run`/`play`/`swee
|
||||
- **Godot** accepts **`--trace-histogram <file>`** — profile the **real** run (headless flow diverges because
|
||||
`wait-for-input` is a no-op there; the real run to page 1 is ~562 steps with **0** sleeps vs headless's 2M
|
||||
steps / 493k sleeps). Dumped when the scene ends or the window closes. e.g.
|
||||
`godot --path godot -- --boot --shot out/p1.png --trace-histogram out/hist.txt`.
|
||||
`godot --path godot -- --scene SC0000 --boot --shot out/p1.png --trace-histogram out/hist.txt`.
|
||||
|
||||
**Godot frontend** (`S:/Godot/Godot_v4.7…`; project = `godot/`). Toolchain: `godot --headless --path godot
|
||||
--import` → `dotnet build godot/Himegari.csproj` → `godot [--headless] --path godot [-- <userargs>]`.
|
||||
Plays the real bytecode with call-script execution on (subroutines run live). `--headless` can't render
|
||||
texture ops (no GPU context) — run windowed for real scenes. User args (after `--`):
|
||||
- `--scene <NAME>` — which scene to play (default `SC0000`), e.g. `--scene SC0240` (executes 29 nested subroutines).
|
||||
Plays the real bytecode with call-script execution on (subroutines run live). A no-argument launch, including
|
||||
`run-godot.ps1`, starts the persistent `SYSTEM4.BIN` root and reaches TITLE naturally. `--headless` can't
|
||||
render texture ops (no GPU context) — run windowed for real scenes. User args (after `--`):
|
||||
- `--scene <NAME>` — override the default `SYSTEM4` root with a direct diagnostic scene, e.g. `--scene SC0240` (executes 29 nested subroutines).
|
||||
- `--selftest` — headless; runs a **synthesized** scene through the thread/suspend/`CallDeferred` plumbing and asserts it matches a live headless run (full handling; no vm0/frozen golden). Exits.
|
||||
- `--seed 0xADDR=VAL` (repeatable) — seed initial global state, e.g. `--seed 0xa57=1` unlocks Lily's form-A voiced dialogue.
|
||||
- `--boot` — run SYSTEM4's state prefix (`INITCONFIG/INIT2/INIT`) via `GameSession` before the scene, so scene-assumed boot state (chiefly INIT2's gfx handle array) is present. **Needed for the gfx CGs to render** (without it the opening event CGs collapse/drift). e.g. `godot --path godot -- --boot`.
|
||||
- `--boot` — direct-scene diagnostic only: with `--scene <non-SYSTEM4>`, run the old
|
||||
`INITCONFIG/INIT2/INIT` state prefix before that isolated scene. The normal SYSTEM4-rooted launch neither
|
||||
needs nor applies it. e.g. `godot --path godot -- --scene SC0000 --boot`.
|
||||
- `--shot <png> [--shot-page N]` — capture page N to a PNG then quit (dev screenshot). At scene end it also prints the call-scripts executed as nested frames.
|
||||
- `--shot-sequence <dir> [--frames N]` — dump one PNG per rendered frame (`frame_0000.png…`, default N=180 ≈ 3s @60fps) then quit, auto-advancing past input waits. Verifies time-based retained effects and publication boundaries as distinct frames, which a single `--shot` cannot. CPU/IO-heavy by design (a PNG every frame); a dev diagnostic, not a normal run. e.g. `godot --path godot -- --boot --shot-sequence out/seq --frames 300`.
|
||||
- `--shot-sequence <dir> [--frames N]` — dump one PNG per rendered frame (`frame_0000.png…`, default N=180 ≈ 3s @60fps) then quit, auto-advancing past input waits. Verifies time-based retained effects and publication boundaries as distinct frames, which a single `--shot` cannot. CPU/IO-heavy by design (a PNG every frame); a dev diagnostic, not a normal run. e.g. `godot --path godot -- --scene SC0000 --boot --shot-sequence out/seq --frames 300`.
|
||||
- `--sleep-scale <f>` — multiply every explicit `sleep` (op 0xc8) duration by `f` (default 1.0). This stretches only script-authored sleep holds; it does not slow ordinary opcode bursts or replace `0x20c`/`0x21c` presentation pacing. Debug-only; leave at 1.0 for real playback.
|
||||
- `--speed <f>` — scale sleeps and retained presentation clocks without throttling ordinary opcode bursts or auto-advancing input waits. Values 0.05–8 are accepted; `--speed 0.25` is useful for transform inspection, while 1.0 is normal playback.
|
||||
- `--gfx-log <file>` — **compositor + op diagnostic** (the tool that root-caused the grey background). Logs, per rendered frame, only the objects whose draw outcome **CHANGED** (drawn↔skip↔gone, resId, resolved file, `slot`, `src`/`dst`, `op`acity, `tintStr`ength) — quiet until something actually changes, so the exact frame a layer drops out (and why) stands out. Also traces every `set-texture`/`create-texture` **slot assignment** (via `GodotAdvHost.TraceOps`). Works live or with `--shot-sequence`. Use it before theorising about layering/blend/geometry: it showed the grey BG = the slot-selecting globals resolving to 0 → every texture collapsing into slot 0 (see engine-re.md §"Grey-background root cause"). e.g. `godot --path godot -- --boot --gfx-log out/gfx.log` then click to the bad page.
|
||||
- `--gfx-log <file>` — **compositor + op diagnostic** (the tool that root-caused the grey background). Logs, per rendered frame, only the objects whose draw outcome **CHANGED** (drawn↔skip↔gone, resId, resolved file, `slot`, `src`/`dst`, `op`acity, `tintStr`ength) — quiet until something actually changes, so the exact frame a layer drops out (and why) stands out. Also traces every `set-texture`/`create-texture` **slot assignment** (via `GodotAdvHost.TraceOps`). Works live or with `--shot-sequence`. Use it before theorising about layering/blend/geometry: it showed the grey BG = the slot-selecting globals resolving to 0 → every texture collapsing into slot 0 (see engine-re.md §"Grey-background root cause"). e.g. `godot --path godot -- --scene SC0000 --boot --gfx-log out/gfx.log` then click to the bad page.
|
||||
Matrix-channel outcomes also include `base`, `anchor`, projected `dst`, sampled `scale`/`trans`, and
|
||||
one-shot-plus-cyclic `rot`ation angles. Active op-`0x202` outcomes include packed `color=current->target`
|
||||
and `colorProgress`, synchronized with the same frame/clock in `--timeline-log`. Parent directories are
|
||||
@@ -155,7 +158,7 @@ texture ops (no GPU context) — run windowed for real scenes. User args (after
|
||||
- `--timeline-log <jsonl>` — diagnostic-only synchronized event stream for a real Godot run. Records every
|
||||
executed script byte offset/opcode, virtual time/frame, VM state changes (`running`, `sleep`, `input-wait`,
|
||||
`halted`), BGM events, and changed visible-object compositor outcomes in one ordered JSONL file. Combine with
|
||||
`--boot --shot-sequence ... --gfx-log ...` to distinguish control-flow stalls from retained-object/compositor
|
||||
`--scene SC0000 --boot --shot-sequence ... --gfx-log ...` to distinguish control-flow stalls from retained-object/compositor
|
||||
failures at an exact bytecode boundary. Relative output paths are project-relative (`godot/`).
|
||||
|
||||
**Godot page locator:** every normal run recreates `build/page-map-<SCENE>.jsonl`, adding one record per
|
||||
|
||||
@@ -9,16 +9,19 @@ public class CallScriptTests
|
||||
{
|
||||
// Opcodes (from build/opcodes.json): exit=0x2, call-script=0x3(argc1), mov=0x55(argc2).
|
||||
// Operand types: imm=0, global-int=3, local-int=9.
|
||||
private const uint OP_EXIT = 0x2, OP_CALLSCRIPT = 0x3, OP_MOV = 0x55;
|
||||
private const uint OP_EXIT = 0x2, OP_CALLSCRIPT = 0x3, OP_MOV = 0x55, OP_SETTEXTURE = 0x1f9;
|
||||
|
||||
private sealed class NullHost : IHost
|
||||
private class NullHost : IHost
|
||||
{
|
||||
public virtual void EnterScriptContext(string scriptName) { }
|
||||
public virtual void ExitScriptContext() { }
|
||||
public virtual long ResolveTextureResourceId(long resourceId) => resourceId;
|
||||
public void ShowText(int o, string t) { }
|
||||
public void WaitForInput() { }
|
||||
public void Sleep(long duration) { }
|
||||
public void FrameYield() { }
|
||||
public void CreateTexture(int s, int w, int h) { }
|
||||
public void SetTexture(long r, int s) { }
|
||||
public virtual void SetTexture(long r, int s) { }
|
||||
public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { }
|
||||
public (int Width, int Height) GetTextureSize(int s) => (0, 0);
|
||||
public void PlayBgm(long id) { }
|
||||
@@ -32,6 +35,29 @@ public class CallScriptTests
|
||||
public Script? GetById(long id) => _m.TryGetValue(id, out var s) ? s : null;
|
||||
}
|
||||
|
||||
private sealed class ContextHost : NullHost
|
||||
{
|
||||
private readonly Stack<string> _contexts = new();
|
||||
public List<string> Events { get; } = new();
|
||||
public List<(long ResourceId, int Slot)> Textures { get; } = new();
|
||||
|
||||
public override void EnterScriptContext(string scriptName)
|
||||
{
|
||||
_contexts.Push(scriptName);
|
||||
Events.Add($"enter:{scriptName}");
|
||||
}
|
||||
|
||||
public override void ExitScriptContext()
|
||||
{
|
||||
Events.Add($"exit:{_contexts.Pop()}");
|
||||
}
|
||||
|
||||
public override long ResolveTextureResourceId(long resourceId)
|
||||
=> resourceId + (_contexts.Peek() == "CALLEE" ? 700 : 70);
|
||||
|
||||
public override void SetTexture(long resourceId, int slot) => Textures.Add((resourceId, slot));
|
||||
}
|
||||
|
||||
// Build a Script from raw dwords via the real loader (guarantees identical decode).
|
||||
private static Script Asm(OpcodeTable t, string name, params uint[] body)
|
||||
{
|
||||
@@ -105,4 +131,25 @@ public class CallScriptTests
|
||||
vm.Run();
|
||||
Assert.Equal(1, vm.Globals[0x20]); // caller's local 0 unchanged by callee's local 0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptLocalTextureIdsFollowTheActiveNestedFrame()
|
||||
{
|
||||
var t = Table();
|
||||
var callee = Asm(t, "CALLEE",
|
||||
OP_SETTEXTURE, 0, 7, 0, 2, 0, uint.MaxValue,
|
||||
OP_EXIT);
|
||||
var caller = Asm(t, "CALLER",
|
||||
OP_SETTEXTURE, 0, 7, 0, 1, 0, uint.MaxValue,
|
||||
OP_CALLSCRIPT, 0, 5,
|
||||
OP_SETTEXTURE, 0, 7, 0, 3, 0, uint.MaxValue,
|
||||
OP_EXIT);
|
||||
var host = new ContextHost();
|
||||
var vm = new VirtualMachine(caller, t, host, null, new MapProvider(new() { [5] = callee }));
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(new[] { (77L, 1), (707L, 2), (77L, 3) }, host.Textures);
|
||||
Assert.Equal(new[] { "enter:CALLER", "enter:CALLEE", "exit:CALLEE", "exit:CALLER" }, host.Events);
|
||||
}
|
||||
}
|
||||
|
||||
111
engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs
Normal file
111
engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using Age.Engine.Diagnostics;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
using Xunit;
|
||||
|
||||
public class NaturalBootIntegrationTests
|
||||
{
|
||||
private sealed class ReachedSc0000Exception : Exception { }
|
||||
|
||||
private sealed class StopAtSc0000Sink : ITraceSink
|
||||
{
|
||||
public readonly List<string> Entered = new();
|
||||
public Action<string>? OnEnter;
|
||||
public bool TracingSteps => false;
|
||||
|
||||
public void Emit(in TraceEvent e)
|
||||
{
|
||||
if (e.Kind == TraceEventKind.FrameEnter && e.Name != null)
|
||||
{
|
||||
Entered.Add(e.Name);
|
||||
OnEnter?.Invoke(e.Name);
|
||||
if (e.Name.Equals("SC0000.BIN", StringComparison.OrdinalIgnoreCase))
|
||||
throw new ReachedSc0000Exception();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NewGameInputHost : RecordingHost
|
||||
{
|
||||
public VirtualMachine Vm = null!;
|
||||
private long _now;
|
||||
public int TitlePollSleeps;
|
||||
private bool _inGameStart;
|
||||
private int _gameStartPollSleeps;
|
||||
public override long InputClockMilliseconds => _now;
|
||||
|
||||
public void BeginGameStart()
|
||||
{
|
||||
_inGameStart = true;
|
||||
_gameStartPollSleeps = 0;
|
||||
Vm.UpdateMouseButtonState(0x1, false);
|
||||
Vm.UpdateInputCallbackState(4, false);
|
||||
Vm.QueueInputCallback(10);
|
||||
}
|
||||
|
||||
public override void Sleep(long duration)
|
||||
{
|
||||
base.Sleep(duration);
|
||||
_now += Math.Max(1, duration);
|
||||
if (duration > 1) return;
|
||||
|
||||
// TITLE's first menu entry is Game Start. Hold the pointer over its native 800x600
|
||||
// rectangle, then provide one complete primary-button edge to its raw input callback.
|
||||
int polls = _inGameStart ? ++_gameStartPollSleeps : ++TitlePollSleeps;
|
||||
int cycle = polls % 200;
|
||||
if (_inGameStart && polls <= 200) return;
|
||||
if (cycle == 1)
|
||||
{
|
||||
int input = _inGameStart && polls <= 400 ? 0 : 4;
|
||||
if (!_inGameStart)
|
||||
{
|
||||
Vm.UpdatePointer(400, 300);
|
||||
Vm.UpdateMouseButtonState(0x1, true);
|
||||
}
|
||||
Vm.UpdateInputCallbackState(input, true);
|
||||
}
|
||||
// TITLE polls its registered mouse callback every 50 ms and activates on the release edge.
|
||||
// Keep the button down through one callback, then release it before the next.
|
||||
else if (cycle == 120)
|
||||
{
|
||||
int input = _inGameStart && polls <= 400 ? 0 : 4;
|
||||
if (!_inGameStart) Vm.UpdateMouseButtonState(0x1, false);
|
||||
Vm.UpdateInputCallbackState(input, false);
|
||||
Vm.QueueInputCallback(10); // native/main-thread release callback unlocks the menu input gate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void System4Root_NewGameSelectionNaturallyCallsSc0000()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var scripts = Sys4ScriptProvider.Load(table);
|
||||
var host = new NewGameInputHost();
|
||||
var sink = new StopAtSc0000Sink();
|
||||
var vm = new VirtualMachine(scripts.RequireByName("SYSTEM4.BIN"), table, host,
|
||||
new VmOptions(MaxSteps: 5_000_000), scripts, sink);
|
||||
host.Vm = vm;
|
||||
sink.OnEnter = name =>
|
||||
{
|
||||
if (name.Equals("GAMESTART.BIN", StringComparison.OrdinalIgnoreCase)) host.BeginGameStart();
|
||||
};
|
||||
|
||||
var exception = Record.Exception(() => vm.Run());
|
||||
Assert.True(exception is ReachedSc0000Exception,
|
||||
$"halt={vm.HaltReason}; title_sleeps={host.TitlePollSleeps}; entered={string.Join(",", sink.Entered)}");
|
||||
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
"SYSTEM4.BIN", "INITCONFIG.BIN", "INIT2.BIN",
|
||||
}, sink.Entered.Take(3));
|
||||
Assert.Contains("TITLE.BIN", sink.Entered);
|
||||
Assert.Contains("GAMESTART.BIN", sink.Entered);
|
||||
Assert.Contains("UNITECH.BIN", sink.Entered);
|
||||
Assert.Contains("CALCARR.BIN", sink.Entered);
|
||||
Assert.Equal("SC0000.BIN", sink.Entered[^1]);
|
||||
Assert.Equal(1, vm.Globals.GetValueOrDefault(0));
|
||||
Assert.Equal(0x22, vm.Globals.GetValueOrDefault(0x699));
|
||||
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x6c1));
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ public class RenderObjectBlendTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mode1_UsesArgbAlphaAsOpacityAndRgbAsMultiplicativeModulation()
|
||||
public void Mode1_UsesAdditiveBlendWithArgbSourceScaleAndRgbModulation()
|
||||
{
|
||||
var g = WithVisibleObject(0x100, resId: 5, colorKey: -1);
|
||||
g.SetStaticObjectColorResolved(0x100, 1, 0x40, 0x80ff40);
|
||||
@@ -103,7 +103,7 @@ public class RenderObjectBlendTests
|
||||
Assert.Equal(0, ro.TintStrength);
|
||||
Assert.Equal(0x80ff40, ro.Tint);
|
||||
Assert.True(ro.MultiplyTint);
|
||||
Assert.Equal(BlendKind.Alpha, ro.Blend);
|
||||
Assert.Equal(BlendKind.Additive, ro.Blend);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -99,6 +99,22 @@ public class SoftwareAffineRasterizerTests
|
||||
Assert.Equal(new byte[4], hidden);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BlitRgba_AdditiveMakesBlackTransparentAndAddsScaledColor()
|
||||
{
|
||||
var identity = new Affine2D(1, 0, 0, 1, 0, 0);
|
||||
byte[] background = { 40, 50, 60, 255 };
|
||||
byte[] black = { 0, 0, 0, 255 };
|
||||
SoftwareAffineRasterizer.BlitRgba(background, 1, 1, black, 1, 1, 0, 0, 1, 1,
|
||||
identity, 0xffffff, 0, 1, multiplyTint: true, blend: BlendKind.Additive);
|
||||
Assert.Equal(new byte[] { 40, 50, 60, 255 }, background);
|
||||
|
||||
byte[] blueGlow = { 16, 32, 200, 128 };
|
||||
SoftwareAffineRasterizer.BlitRgba(background, 1, 1, blueGlow, 1, 1, 0, 0, 1, 1,
|
||||
identity, 0xffffff, 0, 0.5f, multiplyTint: true, blend: BlendKind.Additive);
|
||||
Assert.Equal(new byte[] { 43, 57, 109, 255 }, background);
|
||||
}
|
||||
|
||||
// Pre-fast-path affine algorithm retained here as an independent differential oracle.
|
||||
private static void ReferenceBlit(byte[] dst, int dstW, int dstH, byte[] src, int srcW,
|
||||
int srcX, int srcY, int width, int height, Affine2D transform,
|
||||
|
||||
@@ -15,6 +15,11 @@ public readonly record struct SurfaceRectFill(
|
||||
|
||||
public interface IHost
|
||||
{
|
||||
// Script-local resource ids resolve against the currently executing frame's SYS4INI section.
|
||||
// Interactive hosts track this stack; headless hosts may keep the no-op/default identity behavior.
|
||||
void EnterScriptContext(string scriptName) { }
|
||||
void ExitScriptContext() { }
|
||||
long ResolveTextureResourceId(long resourceId) => resourceId;
|
||||
void ShowText(int offset, string text);
|
||||
// Native ADV text subsystem: op 0x7a updates the selected layout's last 20-byte cursor record;
|
||||
// op 0x204 rasterizes a string into a numbered surface before 0x1fb binds that surface.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace Age.Engine.Model;
|
||||
|
||||
/// <summary>How an object's surface composites onto the canvas. Opaque = straight copy; Alpha = source-alpha
|
||||
/// blend (fades). Additive (glow/flash, native blit mode 2/3) is a documented seam — NOT implemented in the
|
||||
/// blend/transparency slice; see docs/superpowers/specs/2026-07-08-blend-transparency-design.md.</summary>
|
||||
/// blend (fades); Additive = native SRCALPHA/ONE glow composition.</summary>
|
||||
public enum BlendKind { Opaque, Alpha, Additive }
|
||||
|
||||
@@ -665,10 +665,15 @@ public sealed class GfxState
|
||||
{
|
||||
var (a, r, g, b) = BlendMath.UnpackArgb(sampledColor);
|
||||
tint = ((long)r << 16) | ((long)g << 8) | (long)b;
|
||||
if (o.StaticColorMode == 1 || (o.StaticColorMode == 0 && o.OneShotColorBlend))
|
||||
if (o.StaticColorMode == 1)
|
||||
{
|
||||
// Native mode 1 enables SRCALPHA/ONE additive blending and passes packed ARGB as
|
||||
// D3D modulation. Black therefore contributes nothing (TITLE's SO022 flames),
|
||||
// while the high byte scales the additive source contribution.
|
||||
alpha = a; strength = 0; blend = BlendKind.Additive; multiplyTint = true;
|
||||
}
|
||||
else if (o.StaticColorMode == 0 && o.OneShotColorBlend)
|
||||
{
|
||||
// Native mode 1 enables SRCALPHA/INVSRCALPHA and passes packed ARGB as D3D
|
||||
// modulation. Its high byte is opacity, not mode-0 tint/fill strength.
|
||||
alpha = a; strength = 0; blend = BlendKind.Alpha; multiplyTint = true;
|
||||
}
|
||||
else if (o.StaticColorMode == 2)
|
||||
|
||||
@@ -5,7 +5,8 @@ public static class SoftwareAffineRasterizer
|
||||
{
|
||||
public static void BlitRgba(byte[] dst, int dstW, int dstH, byte[] src, int srcW, int srcH,
|
||||
int srcX, int srcY, int width, int height, Affine2D localToDest,
|
||||
long tint, float tintStrength, float opacity, bool multiplyTint = false)
|
||||
long tint, float tintStrength, float opacity, bool multiplyTint = false,
|
||||
BlendKind blend = BlendKind.Alpha)
|
||||
{
|
||||
if (width <= 0 || height <= 0) return;
|
||||
int istr = (int)(System.Math.Clamp(tintStrength, 0f, 1f) * 255);
|
||||
@@ -15,7 +16,7 @@ public static class SoftwareAffineRasterizer
|
||||
if (TryIntegerTranslation(localToDest, out int tx, out int ty))
|
||||
{
|
||||
BlitTranslated(dst, dstW, dstH, src, srcW, srcX, srcY, width, height,
|
||||
tx, ty, tr, tg, tb, istr, ia, multiplyTint);
|
||||
tx, ty, tr, tg, tb, istr, ia, multiplyTint, blend);
|
||||
return;
|
||||
}
|
||||
if (!localToDest.TryInverse(out var inv)) return;
|
||||
@@ -30,7 +31,7 @@ public static class SoftwareAffineRasterizer
|
||||
int sr=multiplyTint ? src[si]*tr/255 : (src[si]*(255-istr)+tr*istr)/255;
|
||||
int sg=multiplyTint ? src[si+1]*tg/255 : (src[si+1]*(255-istr)+tg*istr)/255;
|
||||
int sb=multiplyTint ? src[si+2]*tb/255 : (src[si+2]*(255-istr)+tb*istr)/255;
|
||||
Blend(dst,di,sr,sg,sb,sa);
|
||||
Blend(dst,di,sr,sg,sb,sa,blend);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +68,8 @@ public static class SoftwareAffineRasterizer
|
||||
|
||||
private static void BlitTranslated(byte[] dst, int dstW, int dstH, byte[] src, int srcW,
|
||||
int srcX, int srcY, int width, int height, int tx, int ty,
|
||||
int tr, int tg, int tb, int istr, int ia, bool multiplyTint)
|
||||
int tr, int tg, int tb, int istr, int ia, bool multiplyTint,
|
||||
BlendKind blend)
|
||||
{
|
||||
int x0 = System.Math.Max(0, tx), y0 = System.Math.Max(0, ty);
|
||||
int x1 = (int)System.Math.Min(dstW, (long)tx + width);
|
||||
@@ -84,7 +86,7 @@ public static class SoftwareAffineRasterizer
|
||||
int sr = multiplyTint ? src[si] * tr / 255 : (src[si] * (255 - istr) + tr * istr) / 255;
|
||||
int sg = multiplyTint ? src[si + 1] * tg / 255 : (src[si + 1] * (255 - istr) + tg * istr) / 255;
|
||||
int sb = multiplyTint ? src[si + 2] * tb / 255 : (src[si + 2] * (255 - istr) + tb * istr) / 255;
|
||||
Blend(dst, di, sr, sg, sb, sa);
|
||||
Blend(dst, di, sr, sg, sb, sa, blend);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +110,12 @@ public static class SoftwareAffineRasterizer
|
||||
x1=System.Math.Min(dw,(int)System.Math.Ceiling(System.Math.Max(System.Math.Max(a.X,b.X),System.Math.Max(c.X,d.X))));
|
||||
y1=System.Math.Min(dh,(int)System.Math.Ceiling(System.Math.Max(System.Math.Max(a.Y,b.Y),System.Math.Max(c.Y,d.Y))));
|
||||
}
|
||||
private static void Blend(byte[] d,int i,int r,int g,int b,int a){
|
||||
private static void Blend(byte[] d,int i,int r,int g,int b,int a,BlendKind blend=BlendKind.Alpha){
|
||||
if(blend==BlendKind.Additive){
|
||||
d[i]=(byte)System.Math.Min(255,d[i]+r*a/255);d[i+1]=(byte)System.Math.Min(255,d[i+1]+g*a/255);
|
||||
d[i+2]=(byte)System.Math.Min(255,d[i+2]+b*a/255);d[i+3]=(byte)System.Math.Min(255,d[i+3]+a);
|
||||
return;
|
||||
}
|
||||
d[i]=(byte)((r*a+d[i]*(255-a))/255);d[i+1]=(byte)((g*a+d[i+1]*(255-a))/255);
|
||||
d[i+2]=(byte)((b*a+d[i+2]*(255-a))/255);d[i+3]=(byte)System.Math.Min(255,d[i+3]+a);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,14 @@ public sealed class ResourceMap
|
||||
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase) ? entry : null;
|
||||
}
|
||||
|
||||
/// <summary>Resolve an already-normalized raw catalog id without applying a scene section base.</summary>
|
||||
public AssetEntry? ResolveRawTexture(long rawId)
|
||||
{
|
||||
var entry = _catalog.ResolveRaw(rawId);
|
||||
return entry is { IsPlaceholder: false } &&
|
||||
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase) ? entry : null;
|
||||
}
|
||||
|
||||
/// <summary>Decode an AGF directly from loose-first VFS bytes.</summary>
|
||||
public RgbaImage DecodeTexture(AssetEntry entry) => AgfDecoder.Decode(_store, entry);
|
||||
|
||||
|
||||
@@ -341,6 +341,11 @@ public sealed class VirtualMachine
|
||||
previousRawInputFrame = _rawInputFrame;
|
||||
}
|
||||
var prev = _cur; _cur = frame; _depth++;
|
||||
bool hostContextEntered = false;
|
||||
try
|
||||
{
|
||||
_host.EnterScriptContext(frame.Script.Name);
|
||||
hostContextEntered = true;
|
||||
_sink.Emit(TraceEvent.FrameEnter(frame.Script.Name, _depth, cause, callId));
|
||||
var outcome = FrameOutcome.RanOff;
|
||||
int pc = frame.Pc;
|
||||
@@ -356,6 +361,10 @@ public sealed class VirtualMachine
|
||||
pc = next;
|
||||
}
|
||||
_sink.Emit(TraceEvent.FrameExit(frame.Script.Name, _depth, outcome.ToString()));
|
||||
return outcome;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_interactiveLock)
|
||||
{
|
||||
if (cause == FrameCause.CallScript)
|
||||
@@ -365,8 +374,15 @@ public sealed class VirtualMachine
|
||||
_interactiveFrame = null;
|
||||
if (ReferenceEquals(_rawInputFrame, frame)) _rawInputFrame = previousRawInputFrame;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (hostContextEntered) _host.ExitScriptContext();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cur = prev; _depth--;
|
||||
return outcome;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool ServiceHotspotCallback()
|
||||
@@ -951,12 +967,17 @@ public sealed class VirtualMachine
|
||||
Gfx.ClearSurface((int)Read(a[0]));
|
||||
_host.CreateTexture((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2])); return pc + 1;
|
||||
case "set-texture": // 0x1f9 (resId)(slot)(colorkey) — load a file into the slot's surface
|
||||
{
|
||||
long requestedResourceId = Read(a[0]);
|
||||
long resolvedResourceId = _host.ResolveTextureResourceId(requestedResourceId);
|
||||
if (_diagSetTexture) // AGE_DIAG_SETTEX: log the SLOT operand source (literal vs which global) — grey-BG slot dig
|
||||
System.Console.Error.WriteLine($"[settex] resId=0x{Read(a[0]):x} slot={(int)Read(a[1])} " +
|
||||
System.Console.Error.WriteLine($"[settex] resId=0x{requestedResourceId:x}->0x{resolvedResourceId:x} slot={(int)Read(a[1])} " +
|
||||
$"slotOp=(type={a[1].Type} val=0x{a[1].Value:x}){(a[1].Type == 3 ? $" G[0x{a[1].Value:x}]" : "")}");
|
||||
_host.ReleaseSurface((int)Read(a[1]));
|
||||
Gfx.SetSurface((int)Read(a[1]), Read(a[0]), a.Count > 2 ? Read(a[2]) : 0);
|
||||
_host.SetTexture(Read(a[0]), (int)Read(a[1])); return pc + 1; // host still tracks dims for get-texture-size
|
||||
Gfx.SetSurface((int)Read(a[1]), resolvedResourceId, a.Count > 2 ? Read(a[2]) : 0);
|
||||
_host.SetTexture(resolvedResourceId, (int)Read(a[1]));
|
||||
return pc + 1; // host still tracks dims for get-texture-size
|
||||
}
|
||||
case "draw-texture": // 0x1fb (handle)(slot)(srcX)(srcY)(w)(h)(dstX)(dstY) — bind object -> surface + rect + pos
|
||||
Gfx.BindDraw(Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]),
|
||||
(int)Read(a[4]), (int)Read(a[5]), (int)Read(a[6]), (int)Read(a[7]));
|
||||
|
||||
@@ -12,10 +12,12 @@ public sealed class GodotAdvHost : IHost
|
||||
{
|
||||
private readonly Main _main;
|
||||
private readonly ResourceMap _res;
|
||||
private readonly string _scene; // e.g. "SC0000" — for section_base
|
||||
private readonly string _rootScene;
|
||||
private readonly object _scriptContextLock = new();
|
||||
private readonly Stack<string> _scriptContexts = new();
|
||||
private readonly object _imageLock = new();
|
||||
private readonly Dictionary<int, RgbaImage?> _images = new(); // raw catalog id -> decoded pixels
|
||||
private readonly Dictionary<int, long> _surfaceResources = new(); // surface slot -> scene/raw resource id
|
||||
private readonly Dictionary<int, long> _surfaceResources = new(); // surface slot -> normalized raw catalog id
|
||||
private readonly Dictionary<long, (RgbaImage Image, string Name, int RawIndex)> _movieFrames = new();
|
||||
private readonly Dictionary<int, long> _movieBySurface = new();
|
||||
private readonly HashSet<long> _completedMovies = new();
|
||||
@@ -59,10 +61,33 @@ public sealed class GodotAdvHost : IHost
|
||||
public GodotAdvHost(Main main, ResourceMap res, string scene, Age.Engine.Hosting.FrameClock clock,
|
||||
PageLocatorState locator, GodotTimelineLog? timeline = null)
|
||||
{
|
||||
_main = main; _res = res; _scene = scene; _clock = clock;
|
||||
_main = main; _res = res; _rootScene = scene; _clock = clock;
|
||||
_locator = locator; _timeline = timeline;
|
||||
}
|
||||
|
||||
private string CurrentScene
|
||||
{
|
||||
get { lock (_scriptContextLock) return _scriptContexts.TryPeek(out var scene) ? scene : _rootScene; }
|
||||
}
|
||||
|
||||
public void EnterScriptContext(string scriptName)
|
||||
{
|
||||
string scene = System.IO.Path.GetFileNameWithoutExtension(scriptName).ToUpperInvariant();
|
||||
lock (_scriptContextLock) _scriptContexts.Push(scene);
|
||||
_timeline?.Event("script-context-enter", new() { ["scene"] = scene });
|
||||
}
|
||||
|
||||
public void ExitScriptContext()
|
||||
{
|
||||
string? scene = null;
|
||||
lock (_scriptContextLock)
|
||||
if (_scriptContexts.TryPop(out var popped)) scene = popped;
|
||||
if (scene != null) _timeline?.Event("script-context-exit", new() { ["scene"] = scene });
|
||||
}
|
||||
|
||||
public long ResolveTextureResourceId(long resourceId)
|
||||
=> _res.ResolveTexture(CurrentScene, resourceId)?.RawIndex ?? resourceId;
|
||||
|
||||
public void ShowText(int offset, string text)
|
||||
{
|
||||
Captured.Add((offset, text));
|
||||
@@ -225,7 +250,7 @@ public sealed class GodotAdvHost : IHost
|
||||
if (!_waitIndicators.TryGetValue(_activeWaitLayout, out config)) return null;
|
||||
if (!_surfaceResources.TryGetValue(config.SurfaceSlot, out resourceId)) return null;
|
||||
}
|
||||
var asset = _res.ResolveTexture(_scene, resourceId);
|
||||
var asset = _res.ResolveRawTexture(resourceId);
|
||||
var image = asset != null ? Decode(asset) : null;
|
||||
if (asset == null || image == null || config.CellWidth <= 0 || config.CellHeight <= 0) return null;
|
||||
int frames = System.Math.Max(1, config.TerminalFrame + 1);
|
||||
@@ -510,7 +535,7 @@ public sealed class GodotAdvHost : IHost
|
||||
_surfaceText.Remove(slot);
|
||||
_surfaceResources[slot] = resourceId;
|
||||
}
|
||||
var asset = _res.ResolveTexture(_scene, resourceId);
|
||||
var asset = _res.ResolveRawTexture(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>")}");
|
||||
@@ -531,15 +556,16 @@ public sealed class GodotAdvHost : IHost
|
||||
lock (_imageLock)
|
||||
if (_movieFrames.TryGetValue(resId, out var movie))
|
||||
return (movie.Image, movie.Name, movie.RawIndex, true);
|
||||
var asset = _res.ResolveTexture(_scene, resId);
|
||||
var asset = _res.ResolveRawTexture(resId);
|
||||
var image = asset != null ? Decode(asset) : null;
|
||||
return asset != null && image != null ? (image, asset.Name, asset.RawIndex, false) : null;
|
||||
}
|
||||
|
||||
public void PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask)
|
||||
{
|
||||
var asset = _res.Resolve(_scene, resourceId);
|
||||
if (asset == null) { Godot.GD.Print($"movie unresolved {_scene}:0x{resourceId:x}"); return; }
|
||||
string scene = CurrentScene;
|
||||
var asset = _res.Resolve(scene, resourceId);
|
||||
if (asset == null) { Godot.GD.Print($"movie unresolved {scene}:0x{resourceId:x}"); return; }
|
||||
try
|
||||
{
|
||||
var movie = _res.ReadMovie(asset);
|
||||
@@ -689,7 +715,7 @@ public sealed class GodotAdvHost : IHost
|
||||
|
||||
public void PlayVoice(long id, int playbackVariant)
|
||||
{
|
||||
var asset = _res.Resolve(_scene, id);
|
||||
var asset = _res.Resolve(CurrentScene, id);
|
||||
var audio = asset != null ? LoadAudio(asset) : null;
|
||||
_timeline?.Event("voice", new() { ["id"] = id, ["file"] = audio?.Name,
|
||||
["playback_variant"] = playbackVariant });
|
||||
@@ -722,7 +748,7 @@ public sealed class GodotAdvHost : IHost
|
||||
public void LoadSoundEffect(long resourceId, int channel)
|
||||
{
|
||||
if ((uint)channel >= (uint)_sfxNames.Length) return;
|
||||
var asset = _res.Resolve(_scene, resourceId);
|
||||
var asset = _res.Resolve(CurrentScene, resourceId);
|
||||
var audio = asset != null ? LoadAudio(asset) : null;
|
||||
_sfxNames[channel] = audio?.Name;
|
||||
_timeline?.Event("sfx-load", new() { ["resource"] = resourceId, ["channel"] = channel,
|
||||
|
||||
@@ -153,8 +153,8 @@ public partial class Main : Godot.Control
|
||||
|
||||
var userArgs = OS.GetCmdlineUserArgs();
|
||||
_selftest = System.Array.IndexOf(userArgs, "--selftest") >= 0;
|
||||
bool boot = System.Array.IndexOf(userArgs, "--boot") >= 0; // run SYSTEM4's state prefix first
|
||||
string scene = "SC0000"; // --scene <NAME>: which scene to play (default SC0000)
|
||||
bool boot = System.Array.IndexOf(userArgs, "--boot") >= 0; // diagnostic prefix for direct-scene runs
|
||||
string scene = "SYSTEM4"; // natural persistent root; --scene keeps direct diagnostics
|
||||
var seeds = new List<(int Addr, long Val)>(); // --seed 0xADDR=VAL (repeatable) — initial global state
|
||||
double sleepScale = 1.0; // --sleep-scale <f>: scale explicit op-0xc8 holds
|
||||
double speed = 1.0; // --speed <f>: sleeps + retained presentation clocks
|
||||
@@ -200,6 +200,8 @@ public partial class Main : Godot.Control
|
||||
Sys4ScriptProvider? scripts = null;
|
||||
if (_selftest) (script, provider) = BuildSelfTestScene(table);
|
||||
else { scripts = Sys4ScriptProvider.Load(table); script = scripts.RequireByName(scene + ".BIN"); provider = scripts; }
|
||||
bool directSceneHarness = !_selftest
|
||||
&& !scene.Equals("SYSTEM4", System.StringComparison.OrdinalIgnoreCase);
|
||||
if (_timelineLogPath != null) _timeline = new GodotTimelineLog(_timelineLogPath);
|
||||
if (!_selftest && pageMapPath == null)
|
||||
pageMapPath = System.IO.Path.Combine(Paths.Build, $"page-map-{scene.ToUpperInvariant()}.jsonl");
|
||||
@@ -220,12 +222,12 @@ public partial class Main : Godot.Control
|
||||
// single-scene harness starts after that prefix, so carry forward its exact script-owned state
|
||||
// alongside the inherited SO000/SO001 state below. Full Phase-B SYSTEM4 replay will replace this
|
||||
// bootstrap as one unit; HISTORY depends on the 650x150 dimensions of layouts 2..6 for clipping.
|
||||
if (!_selftest)
|
||||
if (directSceneHarness)
|
||||
AdvTextLayoutBootstrap.ApplyLeadingDefinitionsAndResets(
|
||||
scripts!.RequireByName("SYSTEM4.BIN"), table, _vm.TextHistory);
|
||||
// SYSTEM4 loads the shared SO001 chrome sheet into surface slot 17 before any scene runs.
|
||||
// Seed that inherited retained-surface state without replaying the entrypoint's unrelated UI flow.
|
||||
if (!_selftest && resources.ResolveName("SO001.AGF") is { } systemChrome)
|
||||
if (directSceneHarness && resources.ResolveName("SO001.AGF") is { } systemChrome)
|
||||
{
|
||||
_host.SetTexture(systemChrome.RawIndex, 0x11);
|
||||
_vm.Gfx.SetSurface(0x11, systemChrome.RawIndex, 0);
|
||||
@@ -233,7 +235,7 @@ public partial class Main : Godot.Control
|
||||
// SYSTEM4 also loads SO000 and configures op 0x73 before entering scene code. The Phase-A
|
||||
// single-scene harness does not replay those graphics side effects, so inject their exact state
|
||||
// alongside the existing SO001 bootstrap until Phase B runs the complete SYSTEM4 entrypoint.
|
||||
if (!_selftest && resources.ResolveName("SO000.AGF") is { } waitIndicator)
|
||||
if (directSceneHarness && resources.ResolveName("SO000.AGF") is { } waitIndicator)
|
||||
{
|
||||
_host.SetTexture(waitIndicator.RawIndex, 0x0c);
|
||||
_vm.Gfx.SetSurface(0x0c, waitIndicator.RawIndex, 0xff00);
|
||||
@@ -242,7 +244,7 @@ public partial class Main : Godot.Control
|
||||
}
|
||||
// --boot: run SYSTEM4's state prefix (INITCONFIG/INIT2/INIT) so the scene sees boot state — chiefly
|
||||
// INIT2's gfx handle array 0x62455.. (skips the UI scripts LOGO/OP/TITLE). State carries via globals.
|
||||
if (boot && !_selftest)
|
||||
if (boot && directSceneHarness)
|
||||
{
|
||||
var session = new GameSession();
|
||||
foreach (var b in new[] { "INITCONFIG.BIN", "INIT2.BIN", "INIT.BIN" })
|
||||
@@ -254,13 +256,13 @@ public partial class Main : Godot.Control
|
||||
// The native SYSTEM4 UI boot enables standard ADV chrome after the data-only *INIT prefix above.
|
||||
// Without this inherited value the visible SO001 strip is still drawn, but every ADV script skips
|
||||
// its five pointer rectangles and registers only the off-screen keyboard/pad records.
|
||||
if (!_selftest)
|
||||
if (directSceneHarness)
|
||||
{
|
||||
_vm.Globals[0x6c1] = 1;
|
||||
// The native ADV scheduler supplies this Hide Window permission outside script-visible
|
||||
// writes. Every ADV scene gates its HIDEWIN call on it after opcode 0x199 re-entry.
|
||||
_vm.Globals[0x62425] = 1;
|
||||
}
|
||||
// The native ADV scheduler supplies this Hide Window permission outside script-visible writes.
|
||||
// It is an engine service default, not a scene bootstrap, and remains present for either entry path.
|
||||
if (!_selftest) _vm.Globals[0x62425] = 1;
|
||||
// Native AGE owns this transient secondary-SFX channel outside script-visible writes.
|
||||
// The matching SC0000 trace has value 4 at 0xc31; seed only this proven profile/slice.
|
||||
if (!_selftest && scene.Equals("SC0000", System.StringComparison.OrdinalIgnoreCase))
|
||||
@@ -526,7 +528,7 @@ public partial class Main : Godot.Control
|
||||
else
|
||||
{
|
||||
BlitLayer(texture.Value.Image, texture.Value.AssetId, v.ColorKey, v.Tint, strength, v.SrcX, v.SrcY, v.W, v.H,
|
||||
localToDest, opacity, v.MultiplyTint, texture.Value.IsDynamic);
|
||||
localToDest, opacity, v.MultiplyTint, texture.Value.IsDynamic, v.Blend);
|
||||
var raw = _vm.Gfx.TryGet(v.Handle);
|
||||
outcome = $"slot={raw?.SourceSlot} DRAWN resId=0x{v.SurfaceResId:x} {texture.Value.Name} " +
|
||||
$"src=({v.SrcX},{v.SrcY} {v.W}x{v.H}) base=({v.DstX},{v.DstY}) " +
|
||||
@@ -710,7 +712,7 @@ public partial class Main : Godot.Control
|
||||
if (texture == null) continue;
|
||||
BlitLayer(texture.Value.Image, texture.Value.AssetId, source.ColorKey, source.Tint, source.TintStrength / 255f,
|
||||
source.SrcX, source.SrcY, source.W, source.H, affine, opacity, source.MultiplyTint,
|
||||
texture.Value.IsDynamic);
|
||||
texture.Value.IsDynamic, source.Blend);
|
||||
}
|
||||
drawn++;
|
||||
}
|
||||
@@ -749,11 +751,11 @@ public partial class Main : Godot.Control
|
||||
|
||||
// Blit one object's surface rect. Static source pixels are cached per (assetId, colorKey): on first use, texels
|
||||
// matching the surface colorkey are made transparent (native bakes the key at load — engine-re.md §Blend).
|
||||
// Mode 0 uses tintStrength to LERP texel RGB toward tint. Mode 1 sets multiplyTint and uses packed RGB as
|
||||
// multiplicative modulation while alpha is object opacity.
|
||||
// Mode 0 uses tintStrength to LERP texel RGB toward tint. Mode 1 uses packed RGB modulation and
|
||||
// SRCALPHA/ONE additive composition; black source pixels therefore contribute nothing.
|
||||
private void BlitLayer(RgbaImage decoded, int assetId, long colorKey, long tint, float tintStrength, int srcX, int srcY, int w, int h,
|
||||
Age.Engine.Model.Affine2D localToDest, float alpha = 1f, bool multiplyTint = false,
|
||||
bool dynamic = false)
|
||||
bool dynamic = false, BlendKind blend = BlendKind.Alpha)
|
||||
{
|
||||
var cacheKey = (assetId, colorKey);
|
||||
int sourceWidth, sourceHeight;
|
||||
@@ -796,7 +798,7 @@ public partial class Main : Godot.Control
|
||||
if (sw <= 0 || sh <= 0) return;
|
||||
Age.Engine.Model.SoftwareAffineRasterizer.BlitRgba(
|
||||
_screenPixels, ScreenWidth, ScreenHeight, sourcePixels, sourceWidth, sourceHeight,
|
||||
srcX, srcY, sw, sh, localToDest, tint, tintStrength, alpha, multiplyTint);
|
||||
srcX, srcY, sw, sh, localToDest, tint, tintStrength, alpha, multiplyTint, blend);
|
||||
}
|
||||
|
||||
private void FillAffineQuad(int w, int h, Age.Engine.Model.Affine2D localToDest, long tint, float alpha)
|
||||
|
||||
@@ -4843,12 +4843,12 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "gfx-draw-color"
|
||||
category = "draw"
|
||||
summary = "0x203 (handle)(mode)(alpha)(color) — worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60; the handler's ctx+0x53d88 write is the generic 9-dword instruction length. Negative alpha/RGB preserve current static bytes. Mode 0 is the opaque textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength); mode 1 is SRCALPHA/INVSRCALPHA with ARGB alpha opacity and multiplicative RGB modulation; mode 2 is the 0x223 transition-source identity path. Surfaceless mode-0 fill consumption remains a distinct case."
|
||||
summary = "0x203 (handle)(mode)(alpha)(color) — worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60; the handler's ctx+0x53d88 write is the generic 9-dword instruction length. Negative alpha/RGB preserve current static bytes. Mode 0 is the default textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength). Mode 1 is SRCALPHA/ONE additive glow with ARGB alpha scaling the source contribution and RGB providing multiplicative modulation. Mode 2 conditionally forces ONE/ZERO for the selected render target and is used by 0x223 transition sources; mode 3 selects a subtractive special path. Surfaceless mode-0 fill consumption remains distinct."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "Ghidra handler 0x4229a0; negative operands read current obj+0x60, then worker 0x47e9b0 stores op2 at obj+0x30 and ARGB at +0x60. gfx_object_composite call-site 0x47f78f passes +0x30/+0x60 directly to gfx_object_blit_d3d9; mode 1 sets D3DRS SRCALPHA/INVSRCALPHA and the packed color is the device draw modulation. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white. SC0000 page 14 adds the mode-0 endpoint proof: after the EV052CA->EV052DA 0x223 crossfade, 0x203@0x12478 restores the base CG to mode 0 with preserved 0xffffffff; native keeps EV052DA visible while the port's tint-strength interpretation turns every texel white."
|
||||
evidence = "Ghidra handler 0x4229a0; negative operands read current obj+0x60, then worker 0x47e9b0 stores op2 at obj+0x30 and ARGB at +0x60. gfx_object_composite call-site 0x47f78f passes +0x30/+0x60 directly to gfx_object_blit_d3d9. Correct D3D9 constants at 0x4774c0 prove mode 1 writes SRCBLEND=5/SRCALPHA and DESTBLEND=2/ONE, not INVSRCALPHA. TITLE draws two opaque-black, no-colorkey SO022 flame sprites with mode 1; additive composition removes black and preserves the blue glow, verified in a windowed capture. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white. SC0000 page 14 adds the mode-0 endpoint proof: after the EV052CA->EV052DA 0x223 crossfade, 0x203@0x12478 restores the base CG to mode 0 with preserved 0xffffffff; native keeps EV052DA visible while the port's tint-strength interpretation turns every texel white."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
@@ -6048,12 +6048,12 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "u00421EF0"
|
||||
category = "draw"
|
||||
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 opaque blending (animated alpha is inert; RGB is vertex modulation), while mode 1 consumes ARGB alpha as opacity. 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."
|
||||
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."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "Ghidra /v2: gfx_op_0x232_anim_color@0x423c30 resolves sentinels then calls gfx_worker_anim_color@0x47ef50; gfx_object_anim_interpolate@0x473ed0 samples static obj+0x60 toward target obj+0x240 into a temporary packed color; gfx_object_composite@0x47f650 passes that color plus unchanged selector obj+0x30 to gfx_object_blit_d3d9@0x4774c0. Blit mode 0 enables no alpha blend and passes RGB as modulation; mode 1 enables SRCALPHA/INVSRCALPHA. gfx_object_init_default@0x472810 initializes obj+0x60=0xffffffff. SC0000 0x1a0e (handle,1200,224,-1) is therefore 0xffffffff<->0xe0ffffff with inert alpha and identity RGB: no visible pulse. C# regressions cover exact AE001H visual invariance, negative-RGB preservation, mode-0 RGB modulation, and mode-1 alpha opacity."
|
||||
evidence = "Ghidra /v2: gfx_op_0x232_anim_color@0x423c30 resolves sentinels then calls gfx_worker_anim_color@0x47ef50; gfx_object_anim_interpolate@0x473ed0 samples static obj+0x60 toward target obj+0x240 into a temporary packed color; gfx_object_composite@0x47f650 passes that color plus unchanged selector obj+0x30 to gfx_object_blit_d3d9@0x4774c0. Blit mode 0 leaves the default path and passes RGB as modulation; mode 1 sets SRCALPHA/ONE additive composition. gfx_object_init_default@0x472810 initializes obj+0x60=0xffffffff. SC0000 0x1a0e (handle,1200,224,-1) is therefore 0xffffffff<->0xe0ffffff with inert alpha and identity RGB in mode 0: no visible pulse. C# regressions cover exact AE001H visual invariance, negative-RGB preservation, mode-0 RGB modulation, and mode-1 additive scaling."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
|
||||
Reference in New Issue
Block a user