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
needed for those profiles on this installation.
The old `VariationEmbolden=1.2` was not a good weight-700 approximation: the sample glyph `` had
coverage sum 6,651 versus native 4,615 (+44%), while complete strings remained as much as 9 pixels
narrower than native. `VariationEmbolden=0.53` plus one pixel of glyph spacing yields coverage 4,600
(-0.3%) and bounds all six complete-string errors to 0..3 pixels. Godot reports a 25px/17px line box
for the native 24px/16px profiles, so presentation subtracts that one-pixel backend excess from the
script's extra line spacing, retaining AGE's requested total line advance. The calibration is applied
per bold font variation and through the common live/History/surface style path.
The first numeric pass selected `VariationEmbolden=0.53` plus one pixel of glyph spacing because its raw
Godot atlas sum was close to GDI's raw 4-bit coverage sum and it bounded the six complete-string errors
to 0..3 pixels. A subsequent direct native/port SC0000 screenshot comparison proved that cross-API sum
was not a visually comparable quantity. For `――かつて、戦いがあった。`, both screenshots have the
same viewport-relative top (`y=478`) and bright horizontal bounds (`x=101..410`, 310px), confirming
placement and effective advance. Native nevertheless has 1,656 bright neutral pixels versus the port's
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
parity beyond the calibrated profiles requires a dedicated glyph backend. Portable configurable face
substitutions/defaults are intentionally deferred to the broader runtime configuration design.
The discrepancy is not an unknown value to tune. The native and port pipelines are behaviorally
different:
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)

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
`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
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)

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 |
| 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 |
| 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 |
| 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 |

View File

@@ -518,6 +518,53 @@ requirements.
- Enhancements the VM unlocks: higher/wide resolution, faster text, QoL, save-anywhere, new-content
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