engine: pin retained glyph text contract
This commit is contained in:
@@ -2507,6 +2507,16 @@ serialization, and `text_history_deserialize@0x456130` restores them on the matc
|
|||||||
`NativeTextHistoryCodec` now implements that appended tail directly against the same live
|
`NativeTextHistoryCodec` now implements that appended tail directly against the same live
|
||||||
`AdvTextHistory` model; it remains independent of shared profile state and `RT.DAT`.
|
`AdvTextHistory` model; it remains independent of shared profile state and `RT.DAT`.
|
||||||
|
|
||||||
|
That tail is specifically the History backlog, not the live ADV layout presentation.
|
||||||
|
`text_history_deserialize` calls `text_history_clear`, reconstructs only the logical index and 0x48-byte
|
||||||
|
History record/string vectors, and does not recreate or replace live layout definitions, layout glyph
|
||||||
|
records, source-surface pixels, selected layout, wait-indicator handles, or glyph-object ranges. The main
|
||||||
|
layout-3 container likewise reloads only flagged asset surfaces; created ADV layout surfaces have no
|
||||||
|
serialized pixels. Full restoration runs the saved scripts from their ordinary entry prologues through
|
||||||
|
`CALLBACK_LOAD.BIN`/`0xae`, so the current page is rebuilt and republished by script control flow.
|
||||||
|
`AdvTextHistory.RestorePersistenceSnapshot` consequently replaces only backlog data and preserves any
|
||||||
|
initialized live layout configuration/bindings. A focused native-tail regression pins that separation.
|
||||||
|
|
||||||
History's display support consists of the ordinary presentation operations: primary/ruby font sizes
|
History's display support consists of the ordinary presentation operations: primary/ruby font sizes
|
||||||
(`0x75/0x197`), font weight (`0x2bd`), colors/effect mode/offset (`0x76/0x77/0x78/0x1a4`), layout origin
|
(`0x75/0x197`), font weight (`0x2bd`), colors/effect mode/offset (`0x76/0x77/0x78/0x1a4`), layout origin
|
||||||
(`0x198`), surface rectangle fill (`0x20b`), message-window alpha (`0x131`), and retained-object presentation
|
(`0x198`), surface rectangle fill (`0x20b`), message-window alpha (`0x131`), and retained-object presentation
|
||||||
@@ -2763,13 +2773,27 @@ record with `gfx_object_bind_draw`. Native SC0000 records `ctx+0x14e9c = 50 ms`:
|
|||||||
at `0x834`, then publishes them one at a time before reaching wait `0x83c`. A click during reveal completes
|
at `0x834`, then publishes them one at a time before reaching wait `0x83c`. A click during reveal completes
|
||||||
the remaining records and is consumed; the next click releases the stable wait.
|
the remaining records and is consumed; the next click releases the stable wait.
|
||||||
|
|
||||||
The record layout is `{publication-chain flag, x, y, width, height}` as five dwords. The publisher binds
|
The record layout is `{publication-chain flag, left, top, right, bottom}` as five dwords. These are native
|
||||||
handle `layout+0x68 + reveal_index` from source surface `layout_slot+0x14`, uses `(x,y,width,height)` as
|
rectangle edges, not width/height: `gfx_object_bind_draw@0x47e870` copies them directly into its source-edge
|
||||||
the source rectangle, and places it at `(layout_origin_x+x, layout_origin_y+y)`. A set first dword makes the
|
record. The publisher binds handle `layout+0x68 + reveal_index` from source surface `layout_slot+0x14` and
|
||||||
service publish the following record in the same tick; otherwise the configured glyph delay controls the
|
places it at `(layout_origin_x+left, layout_origin_y+top)`. A first dword equal to one makes the service
|
||||||
next publication. This gives the port a direct path to ordinary `GfxState` objects: rasterize the complete
|
publish the following record in the same tick; otherwise the configured glyph delay controls the next
|
||||||
layout surface first, then materialize these exact bounded records in handle order rather than adding a
|
publication. This gives the port a direct path to ordinary `GfxState` objects: rasterize the complete layout
|
||||||
second overlay compositor.
|
surface first, then materialize these exact bounded records in handle order rather than adding a second
|
||||||
|
overlay compositor.
|
||||||
|
|
||||||
|
`adv_text_layout_check_overflow@0x45efc0` sets bit 1 only when `right_bound < glyph_right` and bit 2 only
|
||||||
|
when `bottom_bound < glyph_bottom`; equality fits. Vertical overflow terminates the builder. With horizontal
|
||||||
|
wrapping enabled, right overflow inserts a line break before the current glyph except for CP932 `0x8141`,
|
||||||
|
`0x8142`, and `0x8176` (`、`, `。`, `」`), which remain attached to the preceding line even beyond the
|
||||||
|
bound. The line pitch is the positive primary font pixel height plus configured leading.
|
||||||
|
|
||||||
|
Timed `adv_text_publish_next_glyph@0x451220` binds only while the reveal index is below layout `+0x6c`,
|
||||||
|
the configured retained-object capacity; reaching the capacity ends the timed service even if more records
|
||||||
|
exist. SYSTEM4 configures all nine Himegari layouts with `0x1f4` (500) handles. The decoded static dialogue
|
||||||
|
corpus contains 30,057 rows, has a maximum individual show-text string length of 68, and contains no
|
||||||
|
500-character string, so shipped ordinary text does not approach that limit. This is a corpus safety
|
||||||
|
baseline, not permission for a mod-facing renderer to overrun the interval.
|
||||||
|
|
||||||
The port retains the SC0000-visible contract without exposing thousands of individual host glyph objects:
|
The port retains the SC0000-visible contract without exposing thousands of individual host glyph objects:
|
||||||
surface strings remain associated with blank surface slots for later retained-object binding, while ADV
|
surface strings remain associated with blank surface slots for later retained-object binding, while ADV
|
||||||
@@ -3357,8 +3381,11 @@ concrete route relevant here.
|
|||||||
`op_0x20a_publish_adv_text_layout@0x422ce0` first calls
|
`op_0x20a_publish_adv_text_layout@0x422ce0` first calls
|
||||||
`adv_text_publish_layout@0x450c80`. Slot zero selects the text manager's current layout; a nonzero operand
|
`adv_text_publish_layout@0x450c80`. Slot zero selects the text manager's current layout; a nonzero operand
|
||||||
selects that indexed layout. The worker erases the layout's old retained-object range, walks its 20-byte
|
selects that indexed layout. The worker erases the layout's old retained-object range, walks its 20-byte
|
||||||
text records, and rebinds their draw objects. When run-state bit `0x40000000` is active, the opcode also
|
text records, and rebinds their draw objects. It snapshots global reveal index manager `+0x570`, clamps it
|
||||||
calls `adv_text_publish_wait_indicator_frame@0x453120` with the current indicator frame. The latter worker
|
only to the layout's record count, and binds records zero through that index inclusive; unlike timed reveal,
|
||||||
|
this path does not clamp to layout `+0x6c` capacity. Himegari's 500-handle ranges keep the shipped path within
|
||||||
|
the configured interval. When run-state bit `0x40000000` is active, the opcode also calls
|
||||||
|
`adv_text_publish_wait_indicator_frame@0x453120` with the current indicator frame. The latter worker
|
||||||
uses frame `-1` to return/capture the terminal frame, `-2` to erase the indicator object, and a
|
uses frame `-1` to return/capture the terminal frame, `-2` to erase the indicator object, and a
|
||||||
nonnegative frame to choose and bind the corresponding atlas cell. All corpus uses are `0x20a(1)`;
|
nonnegative frame to choose and bind the corresponding atlas cell. All corpus uses are `0x20a(1)`;
|
||||||
`HISTORY.BIN@0x13ab` reaches it through the shared ADV redraw callback.
|
`HISTORY.BIN@0x13ab` reaches it through the shared ADV redraw callback.
|
||||||
@@ -3645,10 +3672,16 @@ cursor record immediately.
|
|||||||
`op_0x1c1_set_adv_text_bounds@0x41f6c0` reads `(layout_slot, right, bottom)` and calls
|
`op_0x1c1_set_adv_text_bounds@0x41f6c0` reads `(layout_slot, right, bottom)` and calls
|
||||||
`adv_text_layout_set_bounds@0x44ff00`, which writes the pair to layout `+0x24/+0x28`. They are absolute
|
`adv_text_layout_set_bounds@0x44ff00`, which writes the pair to layout `+0x24/+0x28`. They are absolute
|
||||||
layout-local overflow coordinates. `adv_text_layout_check_overflow@0x45efc0` compares glyph end x/y
|
layout-local overflow coordinates. `adv_text_layout_check_overflow@0x45efc0` compares glyph end x/y
|
||||||
against those fields and returns horizontal/vertical overflow bits. When a layout resets,
|
against those fields with strict `bound < glyph_edge` tests and returns horizontal/vertical overflow bits.
|
||||||
|
When a layout resets,
|
||||||
`adv_text_layout_reset_cursor_and_bounds_record@0x455070` replaces its retained state with a five-dword
|
`adv_text_layout_reset_cursor_and_bounds_record@0x455070` replaces its retained state with a five-dword
|
||||||
record `{0, reset_x, reset_y, right, bottom}` copied from all four configured fields.
|
record `{0, reset_x, reset_y, right, bottom}` copied from all four configured fields.
|
||||||
|
|
||||||
|
`adv_text_layout_reset@0x455210` then zeroes every pixel in surface `layout_slot+0x14` and erases the
|
||||||
|
complete configured glyph-object interval at layout `+0x68/+0x6c`. History recording is a separate tail:
|
||||||
|
unless suppression bit 31 is set, reset appends the logical boundary and arms the next History record's
|
||||||
|
group-start flag.
|
||||||
|
|
||||||
The corpus supplies a complete consistency check: all nine sites for each opcode are in SYSTEM4 and
|
The corpus supplies a complete consistency check: all nine sites for each opcode are in SYSTEM4 and
|
||||||
alternate after layouts 1–9 are defined and initially reset. Layout 1 configures cursor `(100,47)` and
|
alternate after layouts 1–9 are defined and initially reset. Layout 1 configures cursor `(100,47)` and
|
||||||
bounds `(720,147)`; layouts 2–6 use `(45,42)` and `(645,135)`; layout 7 uses `(53,10)` and `(495,180)`;
|
bounds `(720,147)`; layouts 2–6 use `(45,42)` and `(645,135)`; layout 7 uses `(53,10)` and `(495,180)`;
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
### 0x6e `show-text` (show-text, argc 2)
|
### 0x6e `show-text` (show-text, argc 2)
|
||||||
- **summary:** (layout_slot)(text) - build and publish a retained text run in the selected ADV layout, using the current glyph delay and raster style; slot zero selects the current layout.
|
- **summary:** (layout_slot)(text) - build and publish a retained text run in the selected ADV layout, using the current glyph delay and raster style; slot zero selects the current layout.
|
||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **evidence:** Native op 0x6e enters adv_text_build_glyph_records@0x4576c0, which snapshots the selected layout cursor and active font/color state, rasterizes one record per CP932 glyph, advances the live cursor, and publishes records through adv_text_publish_next_glyph@0x451220 using text-manager charDelay. Native SC0000 traces show a 50-ms ordinary delay and retained layout publication; MAMES/ITMES/SKMES/INFOMES set delay zero for immediate UI descriptions.
|
- **evidence:** Native op 0x6e enters adv_text_build_glyph_records@0x4576c0, which snapshots the selected layout cursor and active font/color state, rasterizes one record per CP932 glyph, advances the live cursor, and publishes records through adv_text_publish_next_glyph@0x451220 using text-manager charDelay. adv_text_layout_check_overflow@0x45efc0 returns independent horizontal/vertical bits from strict right/bottom comparisons; the builder's wrap branch excludes CP932 0x8141/0x8142/0x8176. The publisher passes record edges directly to gfx_object_bind_draw@0x47e870 and stops at layout+0x6c capacity. Native SC0000 traces show a 50-ms ordinary delay and retained layout publication; MAMES/ITMES/SKMES/INFOMES set delay zero for immediate UI descriptions.
|
||||||
|
|
||||||
|
The live 20-byte glyph record is {chain_flag,left,top,right,bottom}; rectangle members are edges. Overflow bits use strict bound<glyph-edge comparisons. With horizontal wrapping enabled, CP932 0x8141/0x8142/0x8176 (、/。/」) remain on the preceding line. Timed publication binds first_handle+reveal_index only while reveal_index is below the configured object capacity.
|
||||||
|
|
||||||
### 0x6f `end-text-line` (end-text-line, argc 1)
|
### 0x6f `end-text-line` (end-text-line, argc 1)
|
||||||
- **summary:** (layout_slot) - reset the selected layout's horizontal cursor and advance vertically by the primary font height plus configured line spacing.
|
- **summary:** (layout_slot) - reset the selected layout's horizontal cursor and advance vertically by the primary font height plus configured line spacing.
|
||||||
@@ -25,9 +27,9 @@
|
|||||||
- **summary:** (layout_slot) - clear/reset an ADV text layout, append a retained-history boundary when recording is enabled, snapshot the current code/text position, and commit pending ReadTextDB records. T1 entries target these structural reset sites.
|
- **summary:** (layout_slot) - clear/reset an ADV text layout, append a retained-history boundary when recording is enabled, snapshot the current code/text position, and commit pending ReadTextDB records. T1 entries target these structural reset sites.
|
||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **depended on by:** 0x79, 0x1c1
|
- **depended on by:** 0x79, 0x1c1
|
||||||
- **evidence:** Corpus: T1 targets op-0x71 records, but the operand is a layout slot (SC0000 uses 1; HISTORY computes 2..6), not an anchor id. Ghidra /v2: op_0x71_handler@0x41e540 calls adv_text_layout_reset@0x455210 with ctx+0x55110; the worker clears the selected layout and appends {slot,current_record_count}/arms group-start unless suppressed. The handler also snapshots (frame_pc-frame_codebase)/4 and text state and calls read_text_db_commit_pending@0x46ae20.
|
- **evidence:** Corpus: T1 targets op-0x71 records, but the operand is a layout slot (SC0000 uses 1; HISTORY computes 2..6), not an anchor id. Ghidra /v2: op_0x71_handler@0x41e540 calls adv_text_layout_reset@0x455210 with ctx+0x55110; adv_text_layout_reset_cursor_and_bounds_record@0x455070 reconstructs the initial five-dword record, then reset zeroes the complete layout surface and calls gfx_object_erase_range(first,capacity). It appends {slot,current_record_count}/arms group-start unless suppressed. The handler also snapshots (frame_pc-frame_codebase)/4 and text state and calls read_text_db_commit_pending@0x46ae20.
|
||||||
|
|
||||||
Port status (2026-07-24): commits the profile-owned ReadTextDB queue, records the current T1 coordinate, and refreshes per-message read-skip state in addition to the retained-layout reset.
|
The reset recreates the initial {0,reset_x,reset_y,right,bottom} layout record, clears every pixel of source surface layout_slot+0x14, and erases the complete configured glyph-object range. Port status (2026-07-24): also commits the profile-owned ReadTextDB queue, records the current T1 coordinate, and refreshes per-message read-skip state.
|
||||||
|
|
||||||
### 0x72 `wait-for-input` (wait-for-input, argc 1)
|
### 0x72 `wait-for-input` (wait-for-input, argc 1)
|
||||||
- **summary:** (layout_slot) - arm the ADV input wait after text reveal completes; activate the configured wait indicator and, while Auto is enabled, arm the appropriate Auto-message timer.
|
- **summary:** (layout_slot) - arm the ADV input wait after text reveal completes; activate the configured wait indicator and, while Auto is enabled, arm the appropriate Auto-message timer.
|
||||||
@@ -167,7 +169,9 @@ The requested CP932 face is copied into the primary LOGFONT lfFaceName and AGE a
|
|||||||
### 0x20a `publish-adv-text-layout` (u00420CE0, argc 1)
|
### 0x20a `publish-adv-text-layout` (u00420CE0, argc 1)
|
||||||
- **summary:** (layout_slot) - republish one ADV text layout and, while active, its current wait-indicator frame.
|
- **summary:** (layout_slot) - republish one ADV text layout and, while active, its current wait-indicator frame.
|
||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **evidence:** Ghidra op_0x20a_publish_adv_text_layout@0x422ce0 passes the requested slot to adv_text_publish_layout@0x450c80, which rebinds the layout's retained text records. If ADV run-state bit 0x40000000 is active, it also calls adv_text_publish_wait_indicator_frame@0x453120 with ctx+0x5f72c. Slot zero resolves the text manager's current layout; the corpus always uses slot one, including HISTORY.BIN@0x13ab in its shared ADV redraw callback.
|
- **evidence:** Ghidra op_0x20a_publish_adv_text_layout@0x422ce0 passes the requested slot to adv_text_publish_layout@0x450c80. That worker reads manager+0x570, clamps against record count, erases layout+0x68/+0x6c, and binds each record's native edges through gfx_object_bind_draw without a capacity check. If ADV run-state bit 0x40000000 is active, the opcode also calls adv_text_publish_wait_indicator_frame@0x453120 with ctx+0x5f72c. Slot zero resolves the text manager's current layout; the corpus always uses slot one, including HISTORY.BIN@0x13ab in its shared ADV redraw callback.
|
||||||
|
|
||||||
|
Republish snapshots the global reveal index, clamps only to the selected layout's record count, erases its configured retained-object interval, and rebinds records zero through the reveal index inclusive from surface layout_slot+0x14. Unlike timed reveal, native republish does not clamp to the configured object capacity; Himegari reserves 500 handles for every layout and shipped ordinary strings remain below that bound.
|
||||||
|
|
||||||
### 0x212 `set-adv-wait-indicator-handle` (set-adv-wait-indicator-handle, argc 2)
|
### 0x212 `set-adv-wait-indicator-handle` (set-adv-wait-indicator-handle, argc 2)
|
||||||
- **summary:** 0x212 (layout_slot)(retained_handle) — set the retained gfx handle used by that ADV layout's animated wait indicator.
|
- **summary:** 0x212 (layout_slot)(retained_handle) — set the retained gfx handle used by that ADV layout's animated wait indicator.
|
||||||
|
|||||||
@@ -566,9 +566,19 @@ primitive, which cannot be made equivalent by choosing another embolden constant
|
|||||||
current semantic coordinates and native-backed checks for SC0000 voiced/unvoiced pages, STUDY/MAMES,
|
current semantic coordinates and native-backed checks for SC0000 voiced/unvoiced pages, STUDY/MAMES,
|
||||||
HISTORY, BUNKI, and save/load restoration. Confirm the remaining implementation-sensitive native details:
|
HISTORY, BUNKI, and save/load restoration. Confirm the remaining implementation-sensitive native details:
|
||||||
overflow/kinsoku behavior, object-range exhaustion, layout reset/republication, and restoration of live
|
overflow/kinsoku behavior, object-range exhaustion, layout reset/republication, and restoration of live
|
||||||
layout surfaces. The known 20-byte glyph record is `{publication-chain flag, x, y, width, height}`;
|
layout surfaces. The 2026-07-30 baseline closes those questions: the 20-byte glyph record is
|
||||||
publication binds `layout_first_handle + reveal_index` from surface `layout_slot + 0x14`, using `(x,y,w,h)`
|
`{publication-chain flag, left, top, right, bottom}`; publication binds
|
||||||
as the source rectangle and `layout_origin + (x,y)` as the destination.
|
`layout_first_handle + reveal_index` from surface `layout_slot + 0x14`, using those four edges as the
|
||||||
|
native source rectangle and `layout_origin + (left,top)` as the destination. Overflow compares glyph
|
||||||
|
right/bottom strictly against the configured bounds. Horizontal wrapping leaves CP932 `、`, `。`, and
|
||||||
|
`」` attached to the preceding line. Timed reveal stops at the configured handle capacity; all nine
|
||||||
|
Himegari layouts reserve 500 handles, while the largest individual static corpus string has 68 characters.
|
||||||
|
Reset clears the complete layout surface and handle interval. Op `0x20a` erases that interval and republishes
|
||||||
|
through the current reveal index, clamping to record count but not capacity. Numbered saves persist the
|
||||||
|
History backlog, not live layout surfaces or glyph records; initialized layout bindings survive history-tail
|
||||||
|
decode and the ordinary saved-frame prologue/republication path reconstructs current-page presentation.
|
||||||
|
`AdvRetainedTextContract` and focused engine tests pin the platform-neutral record, overflow, punctuation,
|
||||||
|
capacity, SYSTEM4 binding, and restore boundaries before presentation changes.
|
||||||
2. **Land the backend-neutral mask and compositor core.** Add request/result records and an
|
2. **Land the backend-neutral mask and compositor core.** Add request/result records and an
|
||||||
`IGlyphMaskRasterizer`-style seam outside the VM. Keep CP932 byte identity available for native-compatible
|
`IGlyphMaskRasterizer`-style seam outside the VM. Keep CP932 byte identity available for native-compatible
|
||||||
requests while permitting an explicit Unicode-capable portable policy for mods. Implement AGE's coverage
|
requests while permitting an explicit Unicode-capable portable policy for mods. Implement AGE's coverage
|
||||||
|
|||||||
55
engine/Age.Engine.Tests/AdvRetainedTextContractTests.cs
Normal file
55
engine/Age.Engine.Tests/AdvRetainedTextContractTests.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using Age.Engine.Model;
|
||||||
|
|
||||||
|
public class AdvRetainedTextContractTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GlyphRecordUsesNativeEdgesAndExactChainFlag()
|
||||||
|
{
|
||||||
|
var chained = new AdvRetainedGlyphRecord(1, 10, 20, 34, 51);
|
||||||
|
var nonChained = chained with { PublicationChainFlag = 2 };
|
||||||
|
|
||||||
|
Assert.Equal((24, 31), (chained.Width, chained.Height));
|
||||||
|
Assert.True(chained.PublishesNextInSameTick);
|
||||||
|
Assert.False(nonChained.PublishesNextInSameTick);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OverflowComparisonsAreStrictAndIndependentPerAxis()
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
AdvTextOverflowFlags.None,
|
||||||
|
AdvRetainedTextContract.CheckOverflow(100, 50, 100, 50));
|
||||||
|
Assert.Equal(
|
||||||
|
AdvTextOverflowFlags.Horizontal,
|
||||||
|
AdvRetainedTextContract.CheckOverflow(100, 50, 101, 50));
|
||||||
|
Assert.Equal(
|
||||||
|
AdvTextOverflowFlags.Vertical,
|
||||||
|
AdvRetainedTextContract.CheckOverflow(100, 50, 100, 51));
|
||||||
|
Assert.Equal(
|
||||||
|
AdvTextOverflowFlags.Horizontal | AdvTextOverflowFlags.Vertical,
|
||||||
|
AdvRetainedTextContract.CheckOverflow(100, 50, 101, 51));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0x8141)] // 、
|
||||||
|
[InlineData(0x8142)] // 。
|
||||||
|
[InlineData(0x8176)] // 」
|
||||||
|
public void NativeClosingPunctuationPreventsHorizontalWrapBefore(int cp932)
|
||||||
|
=> Assert.True(AdvRetainedTextContract.PreventsHorizontalWrapBefore((ushort)cp932));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OtherCp932GlyphsMayWrapNormally()
|
||||||
|
{
|
||||||
|
Assert.False(AdvRetainedTextContract.PreventsHorizontalWrapBefore(0x8140)); // full-width space
|
||||||
|
Assert.False(AdvRetainedTextContract.PreventsHorizontalWrapBefore(0x82a0)); // あ
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TimedPublicationStopsAtConfiguredObjectCapacity()
|
||||||
|
{
|
||||||
|
Assert.True(AdvRetainedTextContract.TimedPublicationHandleExists(0, 500));
|
||||||
|
Assert.True(AdvRetainedTextContract.TimedPublicationHandleExists(499, 500));
|
||||||
|
Assert.False(AdvRetainedTextContract.TimedPublicationHandleExists(500, 500));
|
||||||
|
Assert.False(AdvRetainedTextContract.TimedPublicationHandleExists(-1, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -289,6 +289,12 @@ public class AdvTextOpsTests
|
|||||||
Assert.Equal((10, 10, 250, 368), CursorAndBounds(history.GetLayoutSnapshot(9)));
|
Assert.Equal((10, 10, 250, 368), CursorAndBounds(history.GetLayoutSnapshot(9)));
|
||||||
Assert.Equal(new[] { 1 }, history.LayoutsCoveredByTextObjectErase(0xd6d8, 0x1f4));
|
Assert.Equal(new[] { 1 }, history.LayoutsCoveredByTextObjectErase(0xd6d8, 0x1f4));
|
||||||
Assert.Equal(new[] { 7 }, history.LayoutsCoveredByTextObjectErase(0x7d0, 0x1f4));
|
Assert.Equal(new[] { 7 }, history.LayoutsCoveredByTextObjectErase(0x7d0, 0x1f4));
|
||||||
|
Assert.All(Enumerable.Range(1, 9), slot =>
|
||||||
|
{
|
||||||
|
AdvTextLayoutPresentationBinding binding = history.GetPresentationBinding(slot);
|
||||||
|
Assert.Equal(slot + 0x14, binding.SourceSurfaceSlot);
|
||||||
|
Assert.Equal(0x1f4, binding.ObjectCapacity);
|
||||||
|
});
|
||||||
|
|
||||||
static (int X, int Y, int Right, int Bottom) CursorAndBounds(AdvTextLayoutSnapshot layout)
|
static (int X, int Y, int Right, int Bottom) CursorAndBounds(AdvTextLayoutSnapshot layout)
|
||||||
=> (layout.CursorX, layout.CursorY, layout.Right, layout.Bottom);
|
=> (layout.CursorX, layout.CursorY, layout.Right, layout.Bottom);
|
||||||
|
|||||||
@@ -92,6 +92,34 @@ public class NativeNumberedSaveCodecTests
|
|||||||
Assert.Equal(AdvTextHistoryRecordKind.Metadata, restored.Records[1].Kind);
|
Assert.Equal(AdvTextHistoryRecordKind.Metadata, restored.Records[1].Kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HistoryTailRestorePreservesInitializedLiveLayoutBindings()
|
||||||
|
{
|
||||||
|
var savedHistory = new AdvTextHistory();
|
||||||
|
savedHistory.DefineLayout(1, 400, 120, 75, 340);
|
||||||
|
savedHistory.AppendText(1, 0x123, "保存履歴", AdvTextStyle.Default);
|
||||||
|
|
||||||
|
var liveHistory = new AdvTextHistory();
|
||||||
|
liveHistory.DefineLayout(3, 320, 90, 20, 400);
|
||||||
|
liveHistory.SetResetCursor(3, 45, 42);
|
||||||
|
liveHistory.SetBounds(3, 300, 80);
|
||||||
|
liveHistory.SetWaitIndicatorObjectHandle(3, 0x1234);
|
||||||
|
liveHistory.SetTextObjectRange(3, 0x2000, 500);
|
||||||
|
|
||||||
|
NativeTextHistoryCodec.DecodeInto(NativeTextHistoryCodec.Encode(savedHistory), liveHistory);
|
||||||
|
|
||||||
|
Assert.Equal(savedHistory.Entries, liveHistory.Entries);
|
||||||
|
Assert.Equal(savedHistory.Records.Select(record => record.Text),
|
||||||
|
liveHistory.Records.Select(record => record.Text));
|
||||||
|
Assert.Equal(3, liveHistory.CurrentLayoutSlot);
|
||||||
|
Assert.Equal(
|
||||||
|
new AdvTextLayoutSnapshot(3, 320, 90, 20, 400, 0, 0, 300, 80),
|
||||||
|
liveHistory.GetLayoutSnapshot(3));
|
||||||
|
Assert.Equal(
|
||||||
|
new AdvTextLayoutPresentationBinding(3, 0x17, 0x2000, 500, 0x1234),
|
||||||
|
liveHistory.GetPresentationBinding(3));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DirectoryStorePreservesHistoryTailAfterNativeContainer()
|
public void DirectoryStorePreservesHistoryTailAfterNativeContainer()
|
||||||
{
|
{
|
||||||
|
|||||||
56
engine/Age.Engine/Model/AdvRetainedTextContract.cs
Normal file
56
engine/Age.Engine/Model/AdvRetainedTextContract.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
namespace Age.Engine.Model;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum AdvTextOverflowFlags
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Horizontal = 1,
|
||||||
|
Vertical = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Native AGE's 20-byte live ADV glyph record. Rectangle values are edges, matching the
|
||||||
|
/// retained-object bind ABI; they are not x/y/width/height values.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct AdvRetainedGlyphRecord(
|
||||||
|
int PublicationChainFlag,
|
||||||
|
int Left,
|
||||||
|
int Top,
|
||||||
|
int Right,
|
||||||
|
int Bottom)
|
||||||
|
{
|
||||||
|
public int Width => Right - Left;
|
||||||
|
public int Height => Bottom - Top;
|
||||||
|
public bool PublishesNextInSameTick => PublicationChainFlag == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The script-owned retained-object binding associated with one live ADV layout.</summary>
|
||||||
|
public readonly record struct AdvTextLayoutPresentationBinding(
|
||||||
|
int LayoutSlot,
|
||||||
|
int SourceSurfaceSlot,
|
||||||
|
long FirstObjectHandle,
|
||||||
|
long ObjectCapacity,
|
||||||
|
long WaitIndicatorObjectHandle);
|
||||||
|
|
||||||
|
/// <summary>Platform-neutral rules established from AGE's native retained-glyph workers.</summary>
|
||||||
|
public static class AdvRetainedTextContract
|
||||||
|
{
|
||||||
|
public static AdvTextOverflowFlags CheckOverflow(
|
||||||
|
int rightBound, int bottomBound, int glyphRight, int glyphBottom)
|
||||||
|
{
|
||||||
|
var result = AdvTextOverflowFlags.None;
|
||||||
|
if (rightBound < glyphRight) result |= AdvTextOverflowFlags.Horizontal;
|
||||||
|
if (bottomBound < glyphBottom) result |= AdvTextOverflowFlags.Vertical;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CP932 closing punctuation which AGE keeps on the preceding horizontal line even when the
|
||||||
|
/// current glyph crosses the right bound.
|
||||||
|
/// </summary>
|
||||||
|
public static bool PreventsHorizontalWrapBefore(ushort cp932)
|
||||||
|
=> cp932 is 0x8141 or 0x8142 or 0x8176; // 、 。 」
|
||||||
|
|
||||||
|
public static bool TimedPublicationHandleExists(int revealIndex, long objectCapacity)
|
||||||
|
=> revealIndex >= 0 && revealIndex < objectCapacity;
|
||||||
|
}
|
||||||
@@ -195,6 +195,15 @@ public sealed class AdvTextHistory
|
|||||||
layout.TextObjectRangeCount = count;
|
layout.TextObjectRangeCount = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AdvTextLayoutPresentationBinding GetPresentationBinding(int requestedSlot)
|
||||||
|
{
|
||||||
|
int slot = ResolveLayout(requestedSlot);
|
||||||
|
var layout = GetOrCreateLayout(slot);
|
||||||
|
return new AdvTextLayoutPresentationBinding(
|
||||||
|
slot, checked(slot + 0x14), layout.TextObjectRangeFirst,
|
||||||
|
layout.TextObjectRangeCount, layout.WaitIndicatorObjectHandle);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolve layouts whose complete native retained-glyph handle interval is covered by one op-0x1f7
|
/// Resolve layouts whose complete native retained-glyph handle interval is covered by one op-0x1f7
|
||||||
/// erase. The Label backend collapses those glyph objects into live text runs, so full native range
|
/// erase. The Label backend collapses those glyph objects into live text runs, so full native range
|
||||||
@@ -275,20 +284,9 @@ public sealed class AdvTextHistory
|
|||||||
_entries.Clear();
|
_entries.Clear();
|
||||||
_entries.AddRange(entries);
|
_entries.AddRange(entries);
|
||||||
_pendingGroupStarts.Clear();
|
_pendingGroupStarts.Clear();
|
||||||
_layouts.Clear();
|
// Native text_history_deserialize clears and reconstructs only the history entry/record vectors.
|
||||||
foreach (AdvTextHistoryRecord record in records)
|
// Live layout definitions, selected layout, surfaces, and retained-object bindings remain owned by
|
||||||
{
|
// the initialized text manager and are rebuilt by the script restore path as needed.
|
||||||
var layout = GetOrCreateLayout(record.Layout.Slot);
|
|
||||||
layout.Width = record.Layout.Width;
|
|
||||||
layout.Height = record.Layout.Height;
|
|
||||||
layout.OriginX = record.Layout.OriginX;
|
|
||||||
layout.OriginY = record.Layout.OriginY;
|
|
||||||
layout.CursorX = record.Layout.CursorX;
|
|
||||||
layout.CursorY = record.Layout.CursorY;
|
|
||||||
layout.Right = record.Layout.Right;
|
|
||||||
layout.Bottom = record.Layout.Bottom;
|
|
||||||
}
|
|
||||||
CurrentLayoutSlot = entries.Count > 0 ? entries[^1].LayoutSlot : 0;
|
|
||||||
_navigationAnchorIndex = entries.Count - 1;
|
_navigationAnchorIndex = entries.Count - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -916,11 +916,12 @@ abi_source = "kelebek+decode-validated"
|
|||||||
name = "show-text"
|
name = "show-text"
|
||||||
category = "adv"
|
category = "adv"
|
||||||
summary = "(layout_slot)(text) - build and publish a retained text run in the selected ADV layout, using the current glyph delay and raster style; slot zero selects the current layout."
|
summary = "(layout_slot)(text) - build and publish a retained text run in the selected ADV layout, using the current glyph delay and raster style; slot zero selects the current layout."
|
||||||
|
details = "The live 20-byte glyph record is {chain_flag,left,top,right,bottom}; rectangle members are edges. Overflow bits use strict bound<glyph-edge comparisons. With horizontal wrapping enabled, CP932 0x8141/0x8142/0x8176 (、/。/」) remain on the preceding line. Timed publication binds first_handle+reveal_index only while reveal_index is below the configured object capacity."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = "Native op 0x6e enters adv_text_build_glyph_records@0x4576c0, which snapshots the selected layout cursor and active font/color state, rasterizes one record per CP932 glyph, advances the live cursor, and publishes records through adv_text_publish_next_glyph@0x451220 using text-manager charDelay. Native SC0000 traces show a 50-ms ordinary delay and retained layout publication; MAMES/ITMES/SKMES/INFOMES set delay zero for immediate UI descriptions."
|
evidence = "Native op 0x6e enters adv_text_build_glyph_records@0x4576c0, which snapshots the selected layout cursor and active font/color state, rasterizes one record per CP932 glyph, advances the live cursor, and publishes records through adv_text_publish_next_glyph@0x451220 using text-manager charDelay. adv_text_layout_check_overflow@0x45efc0 returns independent horizontal/vertical bits from strict right/bottom comparisons; the builder's wrap branch excludes CP932 0x8141/0x8142/0x8176. The publisher passes record edges directly to gfx_object_bind_draw@0x47e870 and stops at layout+0x6c capacity. Native SC0000 traces show a 50-ms ordinary delay and retained layout publication; MAMES/ITMES/SKMES/INFOMES set delay zero for immediate UI descriptions."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
@@ -1004,12 +1005,12 @@ abi_source = "kelebek+decode-validated"
|
|||||||
name = "reset-adv-text-layout"
|
name = "reset-adv-text-layout"
|
||||||
category = "adv"
|
category = "adv"
|
||||||
summary = "(layout_slot) - clear/reset an ADV text layout, append a retained-history boundary when recording is enabled, snapshot the current code/text position, and commit pending ReadTextDB records. T1 entries target these structural reset sites."
|
summary = "(layout_slot) - clear/reset an ADV text layout, append a retained-history boundary when recording is enabled, snapshot the current code/text position, and commit pending ReadTextDB records. T1 entries target these structural reset sites."
|
||||||
details = "Port status (2026-07-24): commits the profile-owned ReadTextDB queue, records the current T1 coordinate, and refreshes per-message read-skip state in addition to the retained-layout reset."
|
details = "The reset recreates the initial {0,reset_x,reset_y,right,bottom} layout record, clears every pixel of source surface layout_slot+0x14, and erases the complete configured glyph-object range. Port status (2026-07-24): also commits the profile-owned ReadTextDB queue, records the current T1 coordinate, and refreshes per-message read-skip state."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = "Corpus: T1 targets op-0x71 records, but the operand is a layout slot (SC0000 uses 1; HISTORY computes 2..6), not an anchor id. Ghidra /v2: op_0x71_handler@0x41e540 calls adv_text_layout_reset@0x455210 with ctx+0x55110; the worker clears the selected layout and appends {slot,current_record_count}/arms group-start unless suppressed. The handler also snapshots (frame_pc-frame_codebase)/4 and text state and calls read_text_db_commit_pending@0x46ae20."
|
evidence = "Corpus: T1 targets op-0x71 records, but the operand is a layout slot (SC0000 uses 1; HISTORY computes 2..6), not an anchor id. Ghidra /v2: op_0x71_handler@0x41e540 calls adv_text_layout_reset@0x455210 with ctx+0x55110; adv_text_layout_reset_cursor_and_bounds_record@0x455070 reconstructs the initial five-dword record, then reset zeroes the complete layout surface and calls gfx_object_erase_range(first,capacity). It appends {slot,current_record_count}/arms group-start unless suppressed. The handler also snapshots (frame_pc-frame_codebase)/4 and text state and calls read_text_db_commit_pending@0x46ae20."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
@@ -5156,11 +5157,12 @@ abi_source = "kelebek+decode-validated"
|
|||||||
name = "publish-adv-text-layout"
|
name = "publish-adv-text-layout"
|
||||||
category = "adv"
|
category = "adv"
|
||||||
summary = "(layout_slot) - republish one ADV text layout and, while active, its current wait-indicator frame."
|
summary = "(layout_slot) - republish one ADV text layout and, while active, its current wait-indicator frame."
|
||||||
|
details = "Republish snapshots the global reveal index, clamps only to the selected layout's record count, erases its configured retained-object interval, and rebinds records zero through the reveal index inclusive from surface layout_slot+0x14. Unlike timed reveal, native republish does not clamp to the configured object capacity; Himegari reserves 500 handles for every layout and shipped ordinary strings remain below that bound."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = "Ghidra op_0x20a_publish_adv_text_layout@0x422ce0 passes the requested slot to adv_text_publish_layout@0x450c80, which rebinds the layout's retained text records. If ADV run-state bit 0x40000000 is active, it also calls adv_text_publish_wait_indicator_frame@0x453120 with ctx+0x5f72c. Slot zero resolves the text manager's current layout; the corpus always uses slot one, including HISTORY.BIN@0x13ab in its shared ADV redraw callback."
|
evidence = "Ghidra op_0x20a_publish_adv_text_layout@0x422ce0 passes the requested slot to adv_text_publish_layout@0x450c80. That worker reads manager+0x570, clamps against record count, erases layout+0x68/+0x6c, and binds each record's native edges through gfx_object_bind_draw without a capacity check. If ADV run-state bit 0x40000000 is active, the opcode also calls adv_text_publish_wait_indicator_frame@0x453120 with ctx+0x5f72c. Slot zero resolves the text manager's current layout; the corpus always uses slot one, including HISTORY.BIN@0x13ab in its shared ADV redraw callback."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
|
|||||||
Reference in New Issue
Block a user