engine: add deterministic glyph mask core

This commit is contained in:
gamer147
2026-07-30 18:25:46 -04:00
parent fd2acb262b
commit fe06618439
9 changed files with 818 additions and 8 deletions

View File

@@ -104,6 +104,8 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings)
│ ├── Age.Engine/Sys4/ runtime game-root selection, catalog parser, loose-first bounded
│ ALF asset store, script provider, AGF/LZSS and Windows CUR decoders,
│ and resource facade
│ ├── Age.Engine/Text/ backend-neutral glyph-mask requests/results, deterministic AGE
│ text compositor/layout, and bounded font/glyph cache primitives
│ └── Age.Engine/Persistence/ native S3SD/S4SD + S3RT codecs, shared payload/ReadTextDB,
│ layout-3 numbered state/history/gfx, DAT/STH pair + BMP codec,
│ and profile-owned state

View File

@@ -2938,13 +2938,20 @@ different:
`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`.
surface, an opaque glyph or a destination with alpha zero receives the source RGB and glyph alpha
directly. Otherwise destination alpha becomes `max(destination alpha, glyph alpha)` and each RGB
channel becomes `(dst*(255-alpha)+src*alpha)/255`, with integer truncation.
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.
Mode 2 uses the same primary color for a second unshifted pass after the ordinary primary glyph, but shifts
every cached coverage value right by two first. On a transparent destination this lower-coverage duplicate
does not change the already-written source RGB/max alpha; on an existing colored pixel it performs a second
integer interpolation toward the primary color. Himegari does not select mode 2, but the shared compositor
still reproduces it.
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
@@ -2963,6 +2970,21 @@ face substitutions/defaults remain deferred to the broader runtime configuration
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.”
The platform-neutral half of that correction landed on 2026-07-30 under `Age.Engine.Text`.
`GlyphRasterRequest` carries face, positive pixel height, native requested width, weight, Unicode scalar,
optional original CP932 code, and an explicit native/portable policy. `GlyphMask` owns normalized 0..16
coverage plus stride, GDI-style origin, cell extent, and advance. `AgeGlyphMaskCompositor` implements the
32-bit branch above without calling an OS or Godot API; its synthetic oracle includes transparent and
existing-alpha pixels, negative bearings/clipping, all four effect modes, and overlapping mode-3 masks.
`RetainedGlyphLayoutEngine` consumes any `IGlyphMaskRasterizer`, writes those masks into an `RgbaImage`,
and returns the five-dword edge records plus final cursor, observed overflow, wrap count, and vertical-stop
state. It preserves native horizontal precedence: vertical-only overflow stops before compositing, while a
simultaneous horizontal overflow follows the wrap/kinsoku path. A reusable bounded LRU provides eviction
callbacks for future native font handles; `CachedGlyphMaskRasterizer` applies the same bound to masks. This
core is not yet connected to ops `0x204`/`0x205`, live ADV, History, or Godot, so the Label backend and
visible behavior remain unchanged until the later integration steps.
#### ADV wait indicator -- ops `0x73` / `0x72` (2026-07-11)
The small bat marker is a configured ADV-layout sprite, not a glyph or part of SO001. `SYSTEM4.BIN`

View File

@@ -33,7 +33,7 @@ or replaced before claiming portable exports.
| AGE movie decode (`0x236` scene movies; `0x20f` modal LOGO/OP/ED; `0x24d` movie masks) | `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 and real TEST.AGF green-mask decode | 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. Opcode `0x24d` redirects decoded green bytes through the platform-neutral retained rasterizer and exact managed packed-alpha mask helper | Concurrent/restarted uses of one asset have independent frame/audio/completion/teardown state. Ordinary and mask movies share VFS/FFmpeg ownership; the mask result is a backend-neutral dynamic RGBA surface consumed by either Godot renderer. 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. 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 | The Phase-E retained glyph-mask polish slice is now planned in `docs/remake-architecture-and-roadmap.md`: keep the GDI reference adapter outside the platform-neutral VM project, select it explicitly on Windows, and complete an explicitly non-identical portable rasterizer/substitution policy before removing the temporary Label fallback |
| ADV font discovery/raster fidelity | Live presentation still uses `godot/Main.cs` to load Windows ` 明朝`/` ゴシック` when available and otherwise choose the existing Japanese-font/default fallback; its `Label` path substitutes FreeType embolden and a Godot outline. The new `Age.Engine.Text` core is OS-neutral: requests select native-CP932 or portable-Unicode policy, mask results normalize to 0..16 coverage/GDI-style metrics, and managed code owns deterministic compositing/layout plus bounded caches | Visible behavior is unchanged. On the reference Windows install, regular advances and SC0000 placement agree, but provisional bold remains visibly lighter/softer. Synthetic masks now test the common compositor independently of any OS font; no authored runtime code calls GDI | Next add the independent Windows GDI adapter outside `Age.Engine`, then integrate immediate surface pixels. Keep explicit portable face substitution/raster policy and do not claim cross-backend pixel equivalence |
| 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/settings storage | `Sys4PersistencePaths` models AGE's independent `SAVEPATH` and `REGFILEPATH` resolutions. Godot replaces Himegari's related profile directory with `user://`, yielding `user://SAVE` for native S3SD/S4SD/S3RT files and thumbnails plus `user://SYS4REG.INI` for the BOM-less CP932 options file; the preserving writer changes only its nine `[sound]` keys | Save payloads and engine options are isolated together without changing either native format. Native/drop-in resolution remains available through `USEAPPDATAFOLDER` plus both SYS4INI paths. A single-root override is rejected when `SAVEPATH` is not beneath `REGFILEPATH`, preventing cross-profile guesses | Expose explicit profile/native selection through the future launcher and allow independent overrides for profiles whose two native paths are unrelated. Validate CP932 availability, replace/flush, case, permissions, and interrupted-write behavior on each export target |
| Game-install and repository discovery | `GameRootSelection` accepts `--game-root`, then probes the executable directory and current working directory for `SYS4INI.BIN`; on Unix the frontend prefers inherited shell `PWD` because Godot may change the process directory during project startup. Godot injects the selected root into its catalog and loose-first ALF store. `Paths.cs` remains the development/test locator for generated opcode data, diagnostics, and CLI conveniences | Installed game data no longer depends on the workspace sibling layout, enabling an executable beside `AGE.EXE`, a terminal launch from the install, or a launcher-supplied absolute profile root. A packaged export still needs its generated runtime metadata bundled independently of repository discovery | Add export packaging for the opcode/profile artifacts, let the future profile launcher pass `--game-root`, and run executable-directory/CWD plus case/permission smoke gates on Linux and macOS |

View File

@@ -579,11 +579,18 @@ primitive, which cannot be made equivalent by choosing another embolden constant
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
`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
conversion, modes 03, clipping, alpha/RGB integer blend, metrics, wrapping, final-cursor result, and
bounded font/glyph caches in managed code. Synthetic masks, not OS fonts, are the unit-test oracle.
2. **Land the backend-neutral mask and compositor core.** Completed 2026-07-30 without changing live
presentation. `Age.Engine.Text` now owns immutable `GlyphRasterRequest`/`GlyphMask` contracts and the
`IGlyphMaskRasterizer` seam outside the VM. Requests preserve Unicode scalar plus optional original CP932
code and explicitly select native-CP932 or portable-Unicode policy; results normalize every backend to
0..16 coverage, stride, GDI-style origin, cell extent, and cell advance. `AgeGlyphMaskCompositor`
implements AGE's coverage conversion, modes 03, clipping, transparent-destination rule, maximum-alpha
and integer RGB blend. `RetainedGlyphLayoutEngine` returns native-edge records, wrapping/kinsoku decisions,
overflow state, and final cursor while rasterizing into a backend-neutral RGBA surface. A generic bounded
LRU supports disposable backend font resources and `CachedGlyphMaskRasterizer` bounds masks. Fifteen
synthetic-mask tests, independent of OS fonts, pin coverage rounding, bearings/clipping, mode-1 displacement,
mode-2 quarter coverage, mode-3 sampling/overlap, wrapping, punctuation, vertical overflow, cursor results,
and cache eviction.
3. **Add the independent Windows GDI reference backend.** Keep the P/Invoke adapter outside the
platform-neutral `Age.Engine` project so the VM continues to call no OS APIs. Reproduce the decoded display
IC, LOGFONT, measurement, and `GGO_GRAY4_BITMAP` request, and compare masks/`GLYPHMETRICS`/advances directly