From 5ce3a7b6f7733d3db14a1a6e25e2ebaa28b20099 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 28 Jul 2026 16:56:07 -0400 Subject: [PATCH] Document native glyph raster backlog --- docs/engine-re.md | 55 ++++++++++++++++++++----- docs/phase-a-slice-plan.md | 9 +++- docs/platform-portability.md | 2 +- docs/remake-architecture-and-roadmap.md | 47 +++++++++++++++++++++ godot/Main.cs | 7 ++-- 5 files changed, 105 insertions(+), 15 deletions(-) diff --git a/docs/engine-re.md b/docs/engine-re.md index 52740d9..69717f3 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -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) diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index defb09a..ed92c58 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -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) diff --git a/docs/platform-portability.md b/docs/platform-portability.md index bccee6e..a6e3b9e 100644 --- a/docs/platform-portability.md +++ b/docs/platform-portability.md @@ -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 | diff --git a/docs/remake-architecture-and-roadmap.md b/docs/remake-architecture-and-roadmap.md index 0c2e964..b5983da 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -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 diff --git a/godot/Main.cs b/godot/Main.cs index 8634c4e..943c2bb 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -16,9 +16,10 @@ public partial class Main : Godot.Control { private const int ScreenWidth = 800; private const int ScreenHeight = 600; - // Calibrated against GDI GetGlyphOutlineW(GGO_GRAY4_BITMAP) for Himegari's 24px MS Mincho - // weight-700 profile. This matches native glyph coverage while one pixel of spacing approximates - // GDI's synthetic-bold advance expansion; see docs/engine-re.md. + // Provisional approximation only: AGE asks GDI to synthesize LOGFONT weight 700, grid-fit a + // GGO_GRAY4 mask, and composites that mask itself. Godot instead applies FreeType embolden plus + // 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 int NativeBoldGlyphSpacing = 1; private TextureRect _screenView = null!; // shows the composited screen backbuffer