engine: add deterministic glyph mask core
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 `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 | 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 `MS 明朝`/`MS ゴシック` 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 |
|
||||
|
||||
@@ -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 0–3, 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 0–3, 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
|
||||
|
||||
Reference in New Issue
Block a user