Render DEBUGMAP tiled field surfaces

This commit is contained in:
gamer147
2026-07-21 13:02:02 -04:00
parent d5f69853d5
commit ff39c7f3f9
14 changed files with 465 additions and 112 deletions

View File

@@ -86,6 +86,16 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
remains correct for ordinary SC texture/voice ids. ROOM voice `0x3365`, for example, resolves as raw
`EUA0016.OGG`; treating it only as a ROOM-local id produces no asset because ROOM owns no SC range.
**Explicit raw texture loader (identified and implemented 2026-07-21).** Opcode `0x249` is the
unambiguous packed raw-id texture path even while a scene section is active. It shares `0x1f9`'s surface
replacement, AGF decode, and RGB colorkey contract, but passes native surface mode 1 and does not apply
the executing frame's section base. FIELD uses `0x32da..0x32dd`, the universal SYS4INI indexes for
`SO005.AGF`, `SO007.AGF`, `SO008A.AGF`, and `SO007A.AGF`, to populate map-sheet surfaces `0x3e..0x41`.
The port therefore forwards those ids directly to `ResolveRawTexture`; it must not run them through
`ResolveTextureResourceId` first. Native mode 1 is a large-image wrapper which tiles the same decoded
logical pixels over ordinary child textures; it is not a different AGF/spritesheet interpretation or
blend rule. The port's contiguous CPU image is therefore equivalent for rendering purposes.
*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
hunt for the selector (thought it was native scene state; even tried reading `G[0x62424]` live — the

View File

@@ -941,6 +941,61 @@ Separate scale/rotation/translation state and timing are implemented. Anchor sem
cyclic wrapping, 2D projection, and affine raster coverage have focused native-oracle tests. Native D3D9 filtering
and render-target command execution remain separate fidelity work.
#### Raw mode-1 surface load — opcode `0x249` (2026-07-21)
`op_0x249_load_raw_texture_surface@0x424b20` has ABI
`(universal_packed_catalog_id, surface_slot, RGB_colorkey)`. Its release, movie detach, indexed-asset open,
colorkey conversion, failure exception, and stream cleanup are the same as `0x1f9`. The only native loader
difference is the last argument to `gfx_surface_load_asset@0x477c40`: `0x1f9` passes mode 0, while `0x249`
passes mode 1. `gfx_surface_decode_and_create@0x474e90` selects a base 0x450-byte texture object for mode 0
and a derived 0x460-byte texture object for mode 1. A successful mode-0 load records the resource id in its
device-reload record; mode 1 records `-1`.
The derived class is specifically a **tiled large-image surface**, not an alternate pixel format,
spritesheet interpretation, or blend mode. `gfx_tiled_surface_create@0x432ff0` divides the logical image into
`ceil(width / DAT_005b15b0) × ceil(height / DAT_005b15b0)` ordinary mode-0 child surfaces.
`gfx_tiled_surface_upload_agf@0x431a10` decodes indexed 1/4/8-bpp and 24/32-bpp input and uploads each tile;
`gfx_tiled_surface_blit@0x4316b0` divides any requested logical source rectangle across the intersecting
children and adjusts their destinations. A CPU compositor can therefore keep one contiguous decoded RGBA
image without losing the mode-1 behavior relevant to Himegari.
The corpus makes the addressing and first gameplay consequence concrete. FIELD calls `0x249` with raw ids
`0x32da..0x32dd`, which are SYS4INI entries `SO005`, `SO007`, `SO008A`, and `SO007A`, into slots
`0x3e..0x41`. `DRAWMAP.BIN` then creates the dungeon tile objects almost entirely from slots `0x3e` and
`0x3f`. A skipped `0x249` therefore leaves the surrounding UI operational but the central map black. After
implementing it, the first DEBUGMAP retest exposed a separate host blit error: FIELD intentionally binds a
zero-width/zero-height SO005 prototype object, while the port expanded zero dimensions to the full texture.
Native `gfx_object_blit_d3d9@0x4774c0` clips the explicit source rectangle and returns when
`right<=left || bottom<=top`; mode-1's tiled blit likewise visits no children for an empty rectangle. The
port now preserves that empty draw rather than leaking the complete SO005 sheet.
#### Selected retained-object range transform — opcodes `0x229`/`0x22a`/`0x22c`/`0x22d` (2026-07-21)
`0x229` was formerly misclassified as a second per-object position setter. Native
`op_0x229_set_gfx_range_transform@0x423700` instead resets an embedded gfx-object record at retained-gfx
owner `+0x428`, writes `(first_handle,count)` to owner `+0x420/+0x424`, and writes operands 3..5 as that
embedded object's anchor at owner `+0x440..+0x448`. The actual per-object direct-position opcode remains
`0x22f`.
On every render frame, `gfx_range_transform_sample_frame@0x476df0` samples the embedded object's ordinary
scale/rotation/translation channels into owner matrix `+0xb5b4`. `gfx_object_composite@0x47f650`
post-multiplies an object's normal matrix by this shared matrix only when its handle is in
`[first_handle, first_handle+count)`. The sibling setters are:
- `0x22a`: current scale, three integer percentages divided by 100;
- `0x22b`: current axis-angle rotation (present in the native dispatch table, zero Himegari corpus calls);
- `0x22c`: current translation in pixels;
- `0x22d`: delayed/duration scale target, using the embedded object's ordinary one-shot scale channel;
- `0x22e`: delayed/duration axis-angle target (native-dispatch-only, zero Himegari corpus calls).
FIELD's camera helper selects handles `[1,50000)`, anchors the transform at the current camera world
coordinate `(G[0x767e],G[0x767f])`, sets translation to `(400-camera_x,300-camera_y,0)`, and applies the
zoom percentage from `G[0xccc09]`. Thus the map layer is centered/scaled while handles `>=50000`—the dungeon
UI—remain screen-fixed. FIELD's sole `0x22d` call animates a zoom over 300 ms. LOOK reuses the immediate
camera helper. Across the corpus, `0x229` occurs 693 times in 309 scripts: 590 all-zero disables, 101
identity-range selections, and the two FIELD/LOOK camera selections. Correcting the contract therefore
removes spurious object-zero mutations without changing established ADV output.
### Blend & transparency — colorkey + `0x202`/`0x203` color/alpha (2026-07-08)
Reversed for graphics slice A (spec `docs/superpowers/specs/2026-07-08-blend-transparency-design.md`;
@@ -1048,7 +1103,7 @@ annotated in Ghidra, saved.
| op | handler / worker | semantics |
|---|---|---|
| `0x22f` | `gfx_op_0x22f_set_position_anim` → `gfx_worker_set_translation` | set object **position** (translation vec `obj+0x5d4`); base transform, not a ping-pong channel |
| `0x229` | `gfx_op_0x229_set_position` (`FUN_00472bb0`+`FUN_00472be0`) | set object **position/geometry** immediately (`obj+0x420/0x424` + vec `obj+0x440..0x448`) |
| `0x229` | `op_0x229_set_gfx_range_transform` → `gfx_range_transform_reset` / `select_handles` / `set_anchor` | reset/select the shared **retained-object range transform**; not a per-object position setter (superseded finding above) |
| `0x239` | `gfx_op_0x239_set_srcrect_cell` → `gfx_worker_set_srcrect_cell` | one-shot **spritesheet-cell** channel: delay/duration `obj+0x48/+0x5c`, total frames/columns `obj+0x238/+0x23c`, target frame `obj+0x234` |
| `0x231` | `gfx_op_0x231_anim_srcrect` → `gfx_worker_anim_srcrect` | looping **spritesheet-cell** channel: milliseconds per frame `obj+0x230`, total frames `obj+0x238`, columns `obj+0x23c`; row-major and wraps, not ping-pong |
| `0x232` | `gfx_op_0x232_anim_color` → `gfx_worker_anim_color` | **animate color**: bit2 active, period `obj+0x220`, target `obj+0x240` → interpolator COLOR channel (ping-pong). Negative alpha/RGB preserve corresponding bytes from static color `obj+0x60`; alpha >255 clamps. Distinct from one-shot `0x202`/static `0x203` |

View File

@@ -594,9 +594,25 @@ Implemented through IHost.PlayModalMovieToSurface. ResourceMap.ResolveRawMovie d
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2 handler gfx_op_0x228_query_position@0x42a3a0 calls gfx_object_query_translation_target@0x47cdd0. The worker copies the complete 0xb5-dword object record, passes copied obj+0x17c to matrix4_decompose_affine@0x48d7c8, and returns its translation outputs; the decomposition reads matrix elements +0x30/+0x34/+0x38, corresponding to obj+0x1ac/+0x1b0/+0x1b4. SC0000 AE001H queries this before each 0x220 leg. C# regression covers targets (40,-20), (50,-80), (130,-100), plus the missing-object output-preservation path.
### 0x229 `u004219E0` (u004219E0, argc 5)
- **summary:** 0x229 set-position2 (handle)(op2)(x)(y)(z): set object position/geometry directly (FUN_00472bb0/be0). C# VM: sets V24. See docs/engine-re.md §SC0000 anim cluster.
- **grounding:** source=kelebek, confidence=low
### 0x229 `set-gfx-range-transform` (set-gfx-range-transform, argc 5)
- **summary:** (first_handle)(count)(anchor_x)(anchor_y)(anchor_z) — reset and select the retained-gfx range transform applied after each ordinary object matrix for handles in [first, first+count), then set its anchor/pivot. A zero count disables it.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x229_set_gfx_range_transform@0x423700 first calls gfx_range_transform_reset@0x472b80, then writes operands 1/2 to retained-gfx owner+0x420/+0x424 and operands 3..5 to the embedded transform object's anchor at owner+0x440..+0x448. gfx_object_composite@0x47f650 post-multiplies the sampled owner+0xb5b4 matrix only for handles in that selected range. Corpus: 693 calls/309 scripts; 590 disable with all zeroes, 101 select from handle 1 with a script-computed count, and FIELD/LOOK supply camera anchors. This supersedes the former incorrect per-object-position interpretation; per-object direct position is 0x22f.
### 0x22a `set-gfx-range-scale-current` (set-gfx-range-scale-current, argc 3)
- **summary:** (scale_x_percent)(scale_y_percent)(scale_z_percent) — immediately replace the selected retained-gfx range transform's current scale matrix.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x22a_set_gfx_range_scale_current@0x4237b0 divides all three operands by 100 and calls gfx_range_transform_set_scale_current@0x472c10, which builds owner+0x494. FIELD and LOOK each call it once after 0x229/0x22c; FIELD's zoom percent is G[0xccc09].
### 0x22c `set-gfx-range-translation-current` (set-gfx-range-translation-current, argc 3)
- **summary:** (translate_x)(translate_y)(translate_z) — immediately replace the selected retained-gfx range transform's current translation matrix.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x22c_set_gfx_range_translation_current@0x423900 passes the three integer operands as floats to gfx_range_transform_set_translation_current@0x472d00, which builds owner+0x594. FIELD computes (400-camera_x, 300-camera_y, 0), making the selected map anchor land at screen center; LOOK uses the same camera helper.
### 0x22d `set-gfx-range-scale-target` (set-gfx-range-scale-target, argc 5)
- **summary:** (delay_ms)(duration_ms)(scale_x_percent)(scale_y_percent)(scale_z_percent) — animate the selected retained-gfx range transform's scale from its current matrix to the target.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x22d_set_gfx_range_scale_target@0x423990 divides operands 3..5 by 100 and calls gfx_range_transform_set_scale_target@0x472d50. The worker arms the embedded transform object's ordinary scale channel (delay obj+0x3c, duration +0x50, target matrix +0xac), which gfx_range_transform_sample_frame@0x476df0 samples before range composition. FIELD has the sole corpus call, a 300 ms camera zoom.
### 0x22f `u00421DD0` (u00421DD0, argc 5)
- **summary:** 0x22f set-position (handle)(op2)(x)(y)(z): set the object base position (direct transform, not ping-pong). Worker gfx_worker_set_translation @0x472e90. C# VM: sets V24. See docs/engine-re.md §SC0000 anim cluster.
@@ -658,6 +674,11 @@ The setter get-or-creates the object and writes the complete operand. During ret
- **depends on:** 0x242
- **evidence:** Ghidra handler 0x4182d0: if !(ctx+0x51b80 & 2), set retained-gfx owner+0xb55c (EngineCtx+0x51b70)=1 and zero owner+0xb564/+0xb568. gfx_object_apply_transform_channels treats force value 1 as immediate completion unless obj+0x2d0 bit 0 is set. SC0000 label_1235a calls it before present-frame.
### 0x249 `load-raw-texture-surface` (load-raw-texture-surface, argc 3)
- **summary:** Load an AGF by universal packed SYS4INI/AAI catalog id into a retained surface slot using native surface mode 1 and the same RGB colorkey contract as set-texture (0x1f9).
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x249_load_raw_texture_surface@0x424b20 is instruction-length 7 and is contract-identical to gfx_op_0x1f9_load_surface through release, asset_open_indexed_entry, RGB colorkey conversion, load failure, and cleanup. Its mode-1 gfx_surface_mode1_ctor selects a tiled large-image wrapper: gfx_tiled_surface_create@0x432ff0 splits the logical dimensions into DAT_005b15b0-sized ordinary mode-0 child textures; gfx_tiled_surface_upload_agf@0x431a10 decodes and uploads each region; gfx_tiled_surface_blit@0x4316b0 subdivides a requested logical source rectangle across those tiles. It is not a spritesheet interpretation or alternate blend mode, so the port's contiguous CPU image is behaviorally equivalent. Corpus literals are universal raw indexes, including FIELD 0x32da..0x32dd -> SO005/SO007/SO008A/SO007A, and therefore bypass scene-section normalization.
## input
### 0x86 `set-cursor-resource` (u0041B210, argc 1)
@@ -1181,18 +1202,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x22a `u00421A90` (u00421A90, argc 3)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x22c `u00421BD0` (u00421BD0, argc 3)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x22d `u00421C60` (u00421C60, argc 5)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x230 `u00421E70` (u00421E70, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
@@ -1221,10 +1230,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x249 `u00422EB0` (u00422EB0, argc 3)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x24d `u00422E90` (u00422E90, argc 12)
- **summary:** —
- **grounding:** source=kelebek, confidence=low

View File

@@ -383,6 +383,39 @@ requires them.
Completion evidence combines original-game observation, executed-opcode/call traces, visible map/UI output,
and before/after global-state comparisons for the action.
### DEBUGMAP field-entry result (2026-07-21; manually validated)
The shipped `DEBUGMAP.BIN` path is useful for the first bounded field slice, but it is not treated as a
replacement for the natural campaign path. It performs substantial script-authored setup itself: it creates
the test units, writes stage id `0xa5` to `G[0x4dfbc]`, fills the field-mode globals, writes system-flow
request `G[0]=3`, and returns so SYSTEM4 enters `FIELD.BIN`. Runs launched from TITLE also retain the real
SYSTEM4/INIT tables. A future discrepancy in party, inventory, stage, or progression state may still be an
unpublished debug-level prerequisite; do not invent a seed unless its missing producer is proven.
The first observed field discrepancy was a black central map while the surrounding field UI and minimap
input remained alive. `DRAWMAP.BIN` is 23/23 opcodes handled and `RENDERMAP.BIN` is 31/31. FIELD's missing
opcode `0x249` loads universal raw map sheets `0x32da..0x32dd`
(`SO005`/`SO007`/`SO008A`/`SO007A`) into surfaces `0x3e..0x41`, after which DRAWMAP binds its generated tile
objects to those surfaces. Skipping the loader left valid retained objects pointing at empty surfaces.
The first retest after adding `0x249` showed the complete SO005 sheet enlarged over a grey field. The native
mode-1 class is now fully identified as a large-image tiled wrapper over the same decoded pixels, ruling out
a special spritesheet or blend interpretation. Two independent presentation gaps caused the retest:
- native treats a zero-area draw-texture source rectangle as an empty draw; the port incorrectly expanded
it to the entire source image, exposing FIELD's intentionally invisible SO005 prototype object;
- FIELD's camera depends on the shared retained-object range transform. Corrected `0x229` selects the map
handle range and anchor (it is not a per-object position opcode), while newly implemented `0x22a`,
`0x22c`, and `0x22d` apply immediate zoom, immediate translation, and animated zoom to that range without
moving the surrounding UI.
The related native `0x22b`/`0x22e` range-rotation setters have zero Himegari corpus calls and need no runtime
implementation yet. The remaining `DRAWMINIMAP` gap is `0x207` (eight calls) and is confined to minimap
work; FIELD's other 14 static gaps do not produce the main terrain layer. Installed-asset decode,
range-isolation/animation, VM dispatch, full engine tests, and the threaded Godot selftest pass. Manual
acceptance confirms that DEBUGMAP now displays the dungeon map correctly; the earlier full-sheet overlay is
gone and the field presentation remains operational after the camera-transform correction.
## Later Phase B breadth
Once the natural spine and first gameplay loop are trustworthy, broaden in independent tracks: