Document native glyph raster backlog
This commit is contained in:
@@ -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 `MS 明朝`. 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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 `MS 明朝`/`MS ゴシック` 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 `MS 明朝`/`MS ゴシック` 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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user