Document native glyph raster backlog

This commit is contained in:
gamer147
2026-07-28 16:56:07 -04:00
parent 69c60ded2e
commit 2dcf0b1f85
5 changed files with 105 additions and 15 deletions

View File

@@ -2522,17 +2522,52 @@ horizontal agreement for regular 24px Mincho and regular 16px Gothic. GDI also p
measurements with the native negative half-height `lfWidth` and `lfWidth=0`; no horizontal scaling is measurements with the native negative half-height `lfWidth` and `lfWidth=0`; no horizontal scaling is
needed for those profiles on this installation. needed for those profiles on this installation.
The old `VariationEmbolden=1.2` was not a good weight-700 approximation: the sample glyph `` had The first numeric pass selected `VariationEmbolden=0.53` plus one pixel of glyph spacing because its raw
coverage sum 6,651 versus native 4,615 (+44%), while complete strings remained as much as 9 pixels Godot atlas sum was close to GDI's raw 4-bit coverage sum and it bounded the six complete-string errors
narrower than native. `VariationEmbolden=0.53` plus one pixel of glyph spacing yields coverage 4,600 to 0..3 pixels. A subsequent direct native/port SC0000 screenshot comparison proved that cross-API sum
(-0.3%) and bounds all six complete-string errors to 0..3 pixels. Godot reports a 25px/17px line box was not a visually comparable quantity. For `――かつて、戦いがあった。`, both screenshots have the
for the native 24px/16px profiles, so presentation subtracts that one-pixel backend excess from the same viewport-relative top (`y=478`) and bright horizontal bounds (`x=101..410`, 310px), confirming
script's extra line spacing, retaining AGE's requested total line advance. The calibration is applied placement and effective advance. Native nevertheless has 1,656 bright neutral pixels versus the port's
per bold font variation and through the common live/History/surface style path. 1,145 (+45%), 1,296 near-white pixels versus 713 (+82%), a 23px rather than 22px bright box, and roughly
twice the gray edge population. The port is materially underweight and softer despite requesting the
correct Mincho face.
Mode 3 still does not reproduce AGE's exact sampled coverage at each ellipse angle. Exact per-pixel The discrepancy is not an unknown value to tune. The native and port pipelines are behaviorally
parity beyond the calibrated profiles requires a dedicated glyph backend. Portable configurable face different:
substitutions/defaults are intentionally deferred to the broader runtime configuration design.
1. `adv_text_manager_construct@0x456570` creates an information DC with
`CreateICA("DISPLAY",NULL,NULL,NULL)`. AGE selects the `CreateFontIndirectA` result for its complete
`LOGFONTA` into this display IC. For SC0000 that state is height `-24`, width `-12`, weight `700`,
`DEFAULT_CHARSET`, default precision/quality/pitch, and ` 明朝`. Windows GDI therefore owns font
mapping, grid fitting, and weight-700 synthesis.
2. `text_raster_string_cached@0x45b600` calls `GetGlyphOutlineA` with identity `MAT2` and
`GGO_GRAY4_BITMAP`. It retains GDI's `GLYPHMETRICS`, uses `gmCellIncX` as the per-glyph advance, and
consumes the returned 17-level coverage mask (`0..16`) directly.
3. `text_blit_glyph_bitmap@0x458c80` converts coverage `c` to integer alpha `c*255/16`. On a 32-bit
surface it takes `max(destination alpha, glyph alpha)` and interpolates each RGB channel as
`(dst*(255-alpha)+src*alpha)/255`.
4. Mode 3 does not invoke a font-renderer outline. Its step is
`360/(sqrt(effect_x²+effect_y²)*8)`, with nearest-integer ellipse coordinates. Radius `(1,1)` makes
twelve calls covering the eight neighboring offsets: each cardinal offset is visited twice and each
diagonal once, followed by one primary-color glyph blit at the unshifted position.
The port does none of those raster operations. It loads the same TTC into Godot/FreeType with grayscale
antialiasing, light hinting, automatic subpixel positioning, embedded bitmaps disabled, and otherwise
default `FontFile` policy. Bold is the unrelated `FontVariation.VariationEmbolden=0.53` operation plus
one pixel of artificial glyph spacing; mode 3 is Godot `Label`'s `outline_size=1`. Thus the family,
nominal size, placement, and total width can agree while stems, serifs, white-core coverage, and edge
pixels differ. The screenshot is useful evidence that the difference is visible, but it is not the
behavioral source.
The fidelity correction is consequently a decoded glyph-mask backend, not screenshot-driven embolden
calibration. A Windows reference implementation can call the same GDI APIs with the decoded `LOGFONTA`
and reproduce AGE's integer compositor. A portable backend must expose the same mask/metrics/compositing
contract while explicitly defining its font-substitution and rasterizer policy; FreeType output should
be treated as that backend's result, not claimed to be GDI-equivalent. Godot's one-pixel line-box
compensation remains independently supported by the matching viewport placement. Portable configurable
face substitutions/defaults remain deferred to the broader runtime configuration design. Implementation
is explicitly backlogged until gameplay settles; the scoped architecture and acceptance gates live in
`docs/remake-architecture-and-roadmap.md` under “AGE-exact glyph-mask text renderer.”
#### ADV wait indicator -- ops `0x73` / `0x72` (2026-07-11) #### ADV wait indicator -- ops `0x73` / `0x72` (2026-07-11)

View File

@@ -3374,7 +3374,14 @@ self-test checks the calibrated variation and both 24px/16px spacing examples.
Validation: engine **431/431**, zero-warning Godot build, and threaded `SELFTEST OK` with Validation: engine **431/431**, zero-warning Godot build, and threaded `SELFTEST OK` with
`font-calibration=ok`. The probes were deliberately disposable; the canonical measurements live in `font-calibration=ok`. The probes were deliberately disposable; the canonical measurements live in
`docs/engine-re.md` rather than adding a Windows-only runtime/tool dependency. User visual acceptance `docs/engine-re.md` rather than adding a Windows-only runtime/tool dependency. User visual acceptance
passed on 2026-07-28. was provisional on 2026-07-28. A later same-page native/port comparison retained exact placement and
horizontal bounds but found the port 45% lower in bright-pixel population with much softer edges. The
raw GDI/Godot coverage-sum comparison was therefore not a valid equivalence oracle. Follow-up Ghidra
work established that AGE uses a GDI-synthesized weight-700 `GGO_GRAY4_BITMAP` and its own integer
glyph/outline compositor, whereas the slice uses FreeType embolden and Godot's outline primitive.
The required fidelity follow-up is implementation of that decoded mask contract; screenshots remain
validation evidence, not the source of rendering behavior. That work is deferred until gameplay settles
and is scoped in `docs/remake-architecture-and-roadmap.md`; it is not the next Phase A/B slice.
## Persistence native-format reconnaissance complete (2026-07-24) ## Persistence native-format reconnaissance complete (2026-07-24)

View File

@@ -31,7 +31,7 @@ or replaced before claiming portable exports.
| AGE movie decode (`0x236` scene movies; `0x20f` modal LOGO/OP/ED) | `FfmpegMovieDecoder` is the sole factory over the project-owned `native/age_movie_ffmpeg` ABI | Windows-x64 passes the complete 213-payload installed video/audio corpus gate plus audible LOGO/OP/CHAPTER playback | Add target-specific native builds and export packaging | | AGE movie decode (`0x236` scene movies; `0x20f` modal LOGO/OP/ED) | `FfmpegMovieDecoder` is the sole factory over the project-owned `native/age_movie_ffmpeg` ABI | Windows-x64 passes the complete 213-payload installed video/audio corpus gate plus audible LOGO/OP/CHAPTER playback | Add target-specific native builds and export packaging |
| Movie integration | Each surface owns a unique playback-instance id; `MovieRuntime` owns `IMovieDecoder` from an injected factory; video-only streams use monotonic pacing while audio-bearing streams use the Godot output clock | Concurrent/restarted uses of one asset have independent frame/audio/completion/teardown state; managed code is no longer Windows-annotated, while only the win-x64 native bundle exists today | Add Linux/macOS native builds and smoke gates | | Movie integration | Each surface owns a unique playback-instance id; `MovieRuntime` owns `IMovieDecoder` from an injected factory; video-only streams use monotonic pacing while audio-bearing streams use the Godot output clock | Concurrent/restarted uses of one asset have independent frame/audio/completion/teardown state; managed code is no longer Windows-annotated, while only the win-x64 native bundle exists today | Add Linux/macOS native builds and smoke gates |
| Movie audio | ABI v2 returns timestamped stereo float PCM; bounded managed buffering feeds a per-playback Godot `AudioStreamGenerator` and routes native movie flags to engine buses | All 29 installed audio-bearing streams decode with signal; synchronized LOGO/OP/CHAPTER playback is audibly accepted | Treat absent, distorted, or unsynchronized audio from an audio-bearing movie as a runtime bug | | Movie audio | ABI v2 returns timestamped stereo float PCM; bounded managed buffering feeds a per-playback Godot `AudioStreamGenerator` and routes native movie flags to engine buses | All 29 installed audio-bearing streams decode with signal; synchronized LOGO/OP/CHAPTER playback is audibly accepted | Treat absent, distorted, or unsynchronized audio from an audio-bearing movie as a runtime bug |
| ADV font discovery/raster fidelity | Opcode `0x1a5` reaches presentation; `godot/Main.cs` loads Windows ` 明朝`/` ゴシック` from their known TTC files when available, otherwise uses the existing Japanese-font/default fallback; native mode 1 maps to one Godot shadow glyph and shipped `(1,1)` mode 3 maps to a one-pixel outline | On the reference Windows install, regular 24px Mincho/16px Gothic advances match GDI exactly; calibrated bold coverage is within 0.3%, six-sample string drift is at most 3px, and backend line-box excess is compensated. Other platforms normally lack the proprietary faces, so substitute metrics remain profile-dependent | Add configurable default and requested-face substitutions as part of the broader runtime configuration design; for portable distribution bundle licensed metric-compatible faces or define profile substitutions, then decide whether broader exact AGE grayscale rasterization warrants a custom glyph backend | | ADV font discovery/raster fidelity | Opcode `0x1a5` reaches presentation; `godot/Main.cs` loads Windows ` 明朝`/` ゴシック` from their known TTC files when available, otherwise uses the existing Japanese-font/default fallback. AGE actually uses a display information DC, weight-700 `LOGFONTA`, `GGO_GRAY4_BITMAP`, and its own integer glyph/outline compositor; the current `Label` backend substitutes FreeType embolden and a Godot outline | On the reference Windows install, regular advances and direct SC0000 placement/bounds agree, but the behaviorally different provisional bold path has 45% fewer bright pixels and much softer edge coverage. Other platforms normally lack the proprietary faces, so substitute metrics remain profile-dependent | Deferred until gameplay settles: implement the Phase-E decoded mask/metrics/compositor backlog in `docs/remake-architecture-and-roadmap.md`, with a shipped GDI reference backend on Windows and an explicitly defined portable rasterizer/substitution policy |
| Filesystem semantics | Several filename and containment comparisons use `OrdinalIgnoreCase`; installed assets are conventionally uppercase | Needs validation on case-sensitive filesystems; may hide casing or containment mistakes | Add Linux/macOS tests with mixed-case synthetic roots and use filesystem-appropriate containment rules | | Filesystem semantics | Several filename and containment comparisons use `OrdinalIgnoreCase`; installed assets are conventionally uppercase | Needs validation on case-sensitive filesystems; may hide casing or containment mistakes | Add Linux/macOS tests with mixed-case synthetic roots and use filesystem-appropriate containment rules |
| Save/profile storage | Managed `DirectoryNativeDatStore` under Godot `user://SAVE`; native S3SD/S4SD/S3RT files and 24-bit BMP thumbnails | No Win32 path API at runtime; port saves remain isolated from the original installation | Validate replace/flush, case, permissions, and interrupted-write behavior on each export target | | Save/profile storage | Managed `DirectoryNativeDatStore` under Godot `user://SAVE`; native S3SD/S4SD/S3RT files and 24-bit BMP thumbnails | No Win32 path API at runtime; port saves remain isolated from the original installation | Validate replace/flush, case, permissions, and interrupted-write behavior on each export target |
| Install/repository discovery | `engine/Age.Engine/Sys4/Paths.cs` finds `age-reimpl` above `AppContext.BaseDirectory` and assumes the current workspace sibling layout | Suitable for development, not packaged exports on any OS | Replace runtime discovery with a user-selected game root/profile; retain repository paths only for developer tools/tests | | Install/repository discovery | `engine/Age.Engine/Sys4/Paths.cs` finds `age-reimpl` above `AppContext.BaseDirectory` and assumes the current workspace sibling layout | Suitable for development, not packaged exports on any OS | Replace runtime discovery with a user-selected game root/profile; retain repository paths only for developer tools/tests |

View File

@@ -518,6 +518,53 @@ requirements.
- Enhancements the VM unlocks: higher/wide resolution, faster text, QoL, save-anywhere, new-content - Enhancements the VM unlocks: higher/wide resolution, faster text, QoL, save-anywhere, new-content
mods. Modding docs + tools. Save/UX polish. mods. Modding docs + tools. Save/UX polish.
#### Deferred backlog — AGE-exact glyph-mask text renderer
**Scheduling:** defer this effort until the natural gameplay spine, first dungeon loop, and combat/system
presentation are settled. The current Godot `Label` backend is readable, respects the script-requested
Mincho/Gothic family when available, and matches SC0000's placement and effective line width. Its remaining
weight, hinting, and outline-pixel differences do not block gameplay work. Resume this backlog early only if
the approximation causes a legibility, layout, input, or gameplay-presentation defect rather than a cosmetic
native-parity difference.
**Behavioral source:** implement the native contract decoded in `docs/engine-re.md`; screenshots are
integration/regression evidence, not tuning inputs. AGE selects an exact `LOGFONTA` into a
`CreateICA("DISPLAY")` information DC, obtains per-glyph metrics and 0..16 coverage through
`GetGlyphOutlineA(GGO_GRAY4_BITMAP)`, and applies its own integer coverage, outline-sampling, and surface
compositor. Godot currently substitutes FreeType `FontFile`/`FontVariation` masks and the `Label` outline
primitive, which cannot be made equivalent by choosing another embolden constant.
**Target architecture:**
1. Add a platform-neutral glyph request/result contract. The request carries the authored face, height,
derived width, weight, character/code point, and raster policy. The result carries coverage bytes,
dimensions/stride, glyph origin/bearings, and cell advance.
2. Move AGE's coverage conversion, mode-1 displacement, mode-3 rounded ellipse samples, clipping, and
integer surface compositing into shared managed code. This layer must not know whether the mask came
from GDI or FreeType.
3. Add a Windows GDI rasterizer using the decoded `CreateICA`/`CreateFontIndirectA`/
`GetGlyphOutlineA` calls. Keep it as the shipped highest-fidelity Windows backend and as the independent
reference oracle for the shared contract; it is not disposable calibration scaffolding.
4. Add an explicitly defined portable rasterizer behind the same contract. A FreeType/Godot
implementation may produce different hinted pixels, especially when the proprietary requested face is
unavailable; profile/configuration must select requested-face substitutions and the default fallback
rather than claiming GDI pixel equivalence.
5. Route live ADV, retained text, History, immediate surface strings, and later ruby/furigana presentation
through one cached glyph service. Retain the existing `Label` path as a temporary portable fallback until
that integration is complete, then remove synthetic embolden/glyph-spacing constants from the exact path.
**Acceptance gates:**
- Fixed synthetic-mask tests byte-match AGE's decoded 16-/32-bit compositor, including overlapping mode-3
neighbors, clipping, alpha, and RGB integer rounding.
- On Windows, representative CP932 glyph masks, `GLYPHMETRICS`, and advances match a direct invocation of
the decoded GDI request; tests compare returned data, not screenshot histograms.
- SC0000 and menu/history checks retain current placement, wrapping, line advance, reveal timing, colors,
effects, and retained-object lifetime after the backend swap.
- The portable backend starts without Windows fonts or GDI, reports/uses its selected substitution policy,
and passes layout/legibility tests without being labeled pixel-identical to native AGE.
- Glyph/font caches remain bounded and reveal-time rendering does not introduce visible frame stalls.
--- ---
## 5. The VM as our analysis instrument — and the correctness bootstrap ## 5. The VM as our analysis instrument — and the correctness bootstrap

View File

@@ -16,9 +16,10 @@ public partial class Main : Godot.Control
{ {
private const int ScreenWidth = 800; private const int ScreenWidth = 800;
private const int ScreenHeight = 600; private const int ScreenHeight = 600;
// Calibrated against GDI GetGlyphOutlineW(GGO_GRAY4_BITMAP) for Himegari's 24px MS Mincho // Provisional approximation only: AGE asks GDI to synthesize LOGFONT weight 700, grid-fit a
// weight-700 profile. This matches native glyph coverage while one pixel of spacing approximates // GGO_GRAY4 mask, and composites that mask itself. Godot instead applies FreeType embolden plus
// GDI's synthetic-bold advance expansion; see docs/engine-re.md. // spacing. Do not retune these values from screenshots; replace this approximation from the decoded
// native glyph-mask contract. See docs/engine-re.md.
private const float NativeBoldEmbolden = 0.53f; private const float NativeBoldEmbolden = 0.53f;
private const int NativeBoldGlyphSpacing = 1; private const int NativeBoldGlyphSpacing = 1;
private TextureRect _screenView = null!; // shows the composited screen backbuffer private TextureRect _screenView = null!; // shows the composited screen backbuffer