From b42c6590dce8c478a49ef0d58af3d21e58748b8c Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Fri, 17 Jul 2026 14:57:28 +0200 Subject: [PATCH 01/57] ARE ambient-animation detection + fpSEAM point effects; remove tile-scale heuristic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At the LoadArea boundary, walk CGameObjectArray for the active area's live CGameStatic objects (the raw authored ARE record is retained at +0x60) and classify authored point sources into an AppContext snapshot (graphics roadmap §10.3 classification channel). Resolution rides EEex's version-independent GetShare pattern with RIP-operand decoding of the m_maxArrayIndex/entry-table globals; any ambiguity fails closed and disables the scan. Offline-verified on both supported builds (2.6.6.0: RVA 0x276490, globals 0x68D434/0x68D450; 2.7.3.0: RVA 0x276700, globals 0x68F8F4/0x68F910) and validated in-game on 2.7.3. Unclassified resrefs are logged per area to grow the classification table. Water bodies stay on the WED overlay path. Classification table calibrated against the full BG1EE (96%+) and BG2EE ARE exports plus visual frame inspection of the ambiguous BAMs: FLM*/FLSM*/FIM*/ YSFL* flame families, candle-named flames as light, steam as smoke, bubbles as water, new water/lava/wildlife kinds, and a built-in exact-resref table for the identified per-area AM* overlays. ARW[DN]* overlays are day/night shadow scenery, not lit windows. First consumer: the fpSEAM override renders point effects from the snapshot — warm flickering fire glow with heat shimmer above flames, drifting chimney- smoke haze columns, and steady candle/glow halos. Shown fire/light/smoke points are packed into a 32-slot uIeePoints[] uniform array (fire prioritized under capacity pressure) and fed through the uniform bridge under its own revision. Gated by [Shaders] EnablePointEffects and the F10 master toggle; background layer only, with vanilla output untouched when off. Also removes the tile-scale UV/texture-id heuristic tier: raw UV magnitudes are atlas-origin noise and GL texture ids grow with session allocations, so the tier produced false 4x detections on vanilla areas (observed in-game on 2.7.3). Detection is now TIS header -> PVR-table grid GCD -> fail closed to standard 1x delegation via the existing sampling path. --- CLAUDE.md | 8 +- CMakeLists.txt | 2 + assets/override/fpSEAM.glsl | 123 +++++++- docs/architecture.md | 24 +- docs/are-animation-detection.md | 143 +++++++++ docs/tile-upscale.md | 12 +- docs/validation/bgee-2.7.3-evidence.md | 9 + src/iee/app_context.h | 6 + src/iee/area_state.cpp | 166 ++++++++++ src/iee/core/config.cpp | 12 + src/iee/core/config.h | 3 + src/iee/features/tile_render.cpp | 5 - src/iee/game/are_animations.cpp | 291 ++++++++++++++++++ src/iee/game/are_animations.h | 95 ++++++ src/iee/game/build_manifest.cpp | 15 +- src/iee/game/build_manifest.h | 7 + src/iee/game/file_formats.h | 81 +++++ src/iee/game/object_statics.cpp | 112 +++++++ src/iee/game/object_statics.h | 39 +++ src/iee/game/opengl_types.cpp | 2 + src/iee/game/opengl_types.h | 2 + src/iee/game/runtime_types_x64.h | 43 +++ src/iee/game/tile_upscale.cpp | 16 +- src/iee/game/tile_upscale.h | 13 +- src/iee/shader_probe.cpp | 4 + src/iee/shader_probe.h | 4 + src/iee/shader_uniform_bridge.cpp | 71 +++++ src/iee/shader_uniform_bridge.h | 10 + tests/iee_tests.cpp | 368 ++++++++++++++++++++++- tools/InfinityEngine-Enhancer.sample.ini | 8 + 30 files changed, 1646 insertions(+), 48 deletions(-) create mode 100644 docs/are-animation-detection.md create mode 100644 src/iee/game/are_animations.cpp create mode 100644 src/iee/game/are_animations.h create mode 100644 src/iee/game/object_statics.cpp create mode 100644 src/iee/game/object_statics.h diff --git a/CLAUDE.md b/CLAUDE.md index 7dcc3a9..44ccc11 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,9 @@ Operational instructions for AI coding agents working in this repository. and `8x`. Other values fail closed to the existing fallback path. - `CResTileSet::h` is optional. Standard tilesets can have `header == null` while still exposing a valid 12-byte PVR entry table through `pData`. A null header does not imply missing deterministic metadata. - Deterministic detection order for this build is: - `TIS header -> PVR entry table coordinate-grid GCD -> legacy heuristic fallback` + `TIS header -> PVR entry table coordinate-grid GCD -> fail closed to 1x` + (the legacy UV/texture-id heuristic produced false 4x detections on vanilla + areas and was removed — do not reintroduce it). - `+0x1DC` is only the current linear-tiles tone flag for this build. ## Runtime Facts — Renderer / GL @@ -59,6 +61,10 @@ Operational instructions for AI coding agents working in this repository. - `src/iee/game/build_manifest.*` holds build-specific offsets, patterns, and callsites. - `src/iee/game/tis_runtime.*` holds explicit runtime views. - `src/iee/game/tile_upscale.*` holds scale selection logic. +- `src/iee/game/are_animations.*` + `src/iee/game/object_statics.*` classify + the active area's authored ARE ambient animations (fire/smoke/fountain/light + point sources) from the live CGameStatic objects; see + `docs/are-animation-detection.md`. Water bodies stay on the WED overlay path. - `docs/` contains the architecture and reverse-engineering notes future agents should read first. - `docs/threading-model.md` defines callback ownership, GL-thread rules, and ABI exception boundaries. - `docs/superpowers/specs/2026-06-10-graphics-enhancement-roadmap-design.md` is the graphics roadmap: four feature pillars, validation gates (V1-V6), and the full evaluated/dropped/rejected idea ledger. Read it before proposing any rendering feature — most ideas have already been evaluated there. diff --git a/CMakeLists.txt b/CMakeLists.txt index 75d1154..ef31e0a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,10 +92,12 @@ add_library(iee_common STATIC src/iee/core/config.cpp src/iee/core/logger.cpp src/iee/core/pattern_scanner.cpp + src/iee/game/are_animations.cpp src/iee/game/area_texture.cpp src/iee/game/build_manifest.cpp src/iee/game/dds_texture.cpp src/iee/game/game_addrs.cpp + src/iee/game/object_statics.cpp src/iee/game/resref_runtime.cpp src/iee/game/shader_override.cpp src/iee/game/tile_liquid.cpp diff --git a/assets/override/fpSEAM.glsl b/assets/override/fpSEAM.glsl index 7eaf726..4e8a3ac 100644 --- a/assets/override/fpSEAM.glsl +++ b/assets/override/fpSEAM.glsl @@ -22,6 +22,8 @@ uniform highp vec2 uIeeZoom; // physical px per world px, per axis uniform highp vec2 uIeeViewport; // physical px (w, h) uniform highp vec2 uIeeWorldSizeInv; // 1 / world px uniform highp vec3 uIeeWaterTint; // authored water color (avg of the area's water overlay tile) +uniform highp float uIeePointCount; // 0..32 classified ambient-animation points +uniform highp vec4 uIeePoints[32]; // xy=world px, z=kind (1 fire/2 smoke/4 light), w=strength uniform lowp sampler2D uIeeAreaMask; // unit 2: one liquid mode per 64px WED cell uniform lowp sampler2D uIeeNormalMap; // unit 3: tiling water normal map uniform lowp sampler2D uIeeDudvMap; // unit 4: tiling DuDv distortion map @@ -207,6 +209,89 @@ bool ieeIsInteriorWater(vec2 worldPos, float centerCoverage) return true; } +// --- IEE ambient point effects (fire glow / heat shimmer / smoke haze / +// --- candle glow), placed from the area's classified ARE animations. --- + +// Accumulated per-fragment point-effect terms. +struct IeePointFx +{ + vec3 glow; // linear warm light accumulated from fire/light points + float shimmer; // heat-distortion weight 0..1 + float haze; // smoke haze density 0..1 + vec3 hazeTint; // veil color for the haze mix +}; + +IeePointFx ieePointEffects(vec2 worldPos, float t) +{ + IeePointFx fx; + fx.glow = vec3(0.0); + fx.shimmer = 0.0; + fx.haze = 0.0; + fx.hazeTint = vec3(0.42, 0.42, 0.46); + + for (int i = 0; i < 32; ++i) + { + if (float(i) >= uIeePointCount) { break; } + vec4 p = uIeePoints[i]; + vec2 offs = worldPos - p.xy; + float kind = p.z; + float strength = p.w; + + if (kind < 1.5) // fire: warm flickering glow + heat shimmer above + { + float radius = 95.0 * strength; + float d = length(offs); + if (d < radius) + { + float fall = 1.0 - d / radius; + fall *= fall; + // Two-rate flicker, unsynchronized between points. + float fi = float(i) * 7.31; + float flick = 0.80 + 0.14 * sin(t * 9.7 + fi) + 0.06 * sin(t * 23.0 + fi * 1.7); + fx.glow += vec3(1.0, 0.44, 0.14) * (fall * 0.65 * strength * flick); + // Shimmer only above the flame (world y grows downward). + float above = clamp(-offs.y / (radius * 0.9), 0.0, 1.0); + fx.shimmer += fall * above * strength; + } + } + else if (kind < 2.5) // smoke: drifting haze column rising from the point + { + float height = 130.0 * strength; + float rise = -offs.y; // px above the source + if (rise > -10.0 && rise < height) + { + float sway = sin(t * 0.8 + rise * 0.045 + float(i)) * (6.0 + rise * 0.16); + float lateral = abs(offs.x - sway); + float widthPx = 14.0 + rise * 0.30; + if (lateral < widthPx * 2.0) + { + float across = exp(-(lateral * lateral) / (widthPx * widthPx)); + float along = smoothstep(-10.0, 18.0, rise) * (1.0 - rise / height); + float puff = ieeNoise(vec2(worldPos.x * 0.05, + (worldPos.y + t * 34.0) * 0.05) + vec2(float(i))); + fx.haze += across * along * (0.35 + 0.65 * puff) * 0.55 * strength; + } + } + } + else if (kind > 3.5 && kind < 4.5) // light: steady soft glow + { + float radius = 70.0 * strength; + float d = length(offs); + if (d < radius) + { + float fall = 1.0 - d / radius; + fall *= fall; + float breathe = 0.92 + 0.08 * sin(t * 2.1 + float(i) * 3.3); + fx.glow += vec3(1.0, 0.72, 0.38) * (fall * 0.35 * strength * breathe); + } + } + } + + fx.shimmer = clamp(fx.shimmer, 0.0, 1.0); + fx.haze = clamp(fx.haze, 0.0, 0.85); + return fx; +} + // Smoothed shore proximity: 0 deep inside water, ~1 at the land boundary. float ieeShoreFactor(vec2 worldPos, float centerCoverage) { @@ -269,7 +354,31 @@ void main() return; } - vec4 texColor = seamSample(vTc); + // Ambient point effects: computed before sampling so heat shimmer can + // ride the one seamSample call as a texture-coordinate offset. Base pass + // only; the WATER_ALPHA secondary pass and fades stay vanilla. + IeePointFx fx; + fx.glow = vec3(0.0); + fx.shimmer = 0.0; + fx.haze = 0.0; + fx.hazeTint = vec3(0.42, 0.42, 0.46); + if (uIeeEnabled > 0.5 && uIeeEnabled < 1.5 && vColor.a > 0.9 && uIeePointCount > 0.5) + { + fx = ieePointEffects(worldPos, uIeeTime); + } + + vec2 sampleTc = vTc; + if (fx.shimmer > 0.01) + { + // Small wavering refraction above flames; uTcScale converts the + // world-pixel amplitude into atlas units, so the offset stays within + // the seam handling's tolerance. + vec2 wobble = vec2(sin(uIeeTime * 13.0 + worldPos.y * 0.35), + cos(uIeeTime * 11.0 + worldPos.x * 0.30)); + sampleTc += wobble * fx.shimmer * 1.4 * uTcScale; + } + + vec4 texColor = seamSample(sampleTc); // Inside flagged cells the engine draws the // base tile with TRANSPARENT pixels exactly where water composites @@ -368,6 +477,18 @@ void main() texColor.a = max(texColor.a, waterMask); } + // Firelight, candle glow, and smoke haze over the (possibly water-graded) + // background art, in linear light so the warm lift does not band. + if (fx.haze > 0.004 || fx.glow.r + fx.glow.g + fx.glow.b > 0.004) + { + vec3 lin = ieeSrgbToLinear(texColor.rgb); + // Multiplicative warm lift preserves the art's detail; the small + // additive term lets glow read on dark night maps. + lin = lin * (vec3(1.0) + fx.glow * 1.6) + fx.glow * 0.045; + lin = mix(lin, fx.hazeTint * (0.35 + 0.40 * lin), fx.haze); + texColor.rgb = ieeLinearToSrgb(lin); + } + texColor = texColor * vColor; float grey = dot(texColor.rgb, vec3(0.299, 0.587, 0.114)); diff --git a/docs/architecture.md b/docs/architecture.md index 82c16fb..aea2d12 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -58,7 +58,24 @@ The supported runtime is intentionally narrow: one EEex-loaded Windows DLL, one - Encapsulates scale detection. - TIS header metadata is authoritative. - Headerless PVR tables are classified from same-page coordinate deltas, not raw atlas origins. -- UV / texture-id heuristics remain fallback only. +- No heuristic tier: unresolved metadata fails closed to standard 1x + delegation (the former UV / texture-id fallback produced false 4x + detections on vanilla areas and was removed). + +`src/iee/game/are_animations.*` + +- Host-safe ARE V1.0 animation-section parsing and conservative + fire/smoke/fountain/light classification of authored ambient animations. +- The classification table grows from runtime logs of unclassified resrefs, + not speculation. See [are-animation-detection.md](are-animation-detection.md). + +`src/iee/game/object_statics.*` + +- Host-safe decode of the `CGameObjectArray` globals out of the engine's + `GetShare` body (manifest pattern + RIP-operand decode) and the read-only + walk that collects the live `CGameStatic` records for an area. +- Sole source for the ARE-animation snapshot; any resolution ambiguity fails + closed and disables the scan for the session. `src/iee/hooks.*` @@ -72,6 +89,8 @@ The supported runtime is intentionally narrow: one EEex-loaded Windows DLL, one - Active-area resolution (manifest-driven CInfGame offsets), view-transform reads, and the post-LoadArea WED cache refresh plus render-thread area-texture queue. +- Also refreshes the ARE ambient-animation snapshot (`AppContext::areaAnimations`) + from the live object array at the same boundary, generation-checked. - Refresh publication is generation-checked; an older load/render callback cannot overwrite the newest immutable WED snapshot. - GL objects are recreated when the current WGL context changes. @@ -110,6 +129,9 @@ The supported runtime is intentionally narrow: one EEex-loaded Windows DLL, one - Owns atomic uniform inputs, location resolution, and render-thread uniform/texture binding. A state revision avoids repeating unchanged GL queries, sampler writes, and enhancer texture binds. +- Also carries the area's classified ambient-animation point set + (`uIeePointCount`/`uIeePoints[32]`) with its own revision, feeding the + fpSEAM fire/smoke/light point effects. `src/iee/shader_diagnostics.*` diff --git a/docs/are-animation-detection.md b/docs/are-animation-detection.md new file mode 100644 index 0000000..721416b --- /dev/null +++ b/docs/are-animation-detection.md @@ -0,0 +1,143 @@ +# ARE Ambient-Animation Detection + +Status: data path implemented and host-validated (in-memory CGameStatic +walk); in-game (Windows) validation pending. + +## Purpose + +The graphics roadmap (§10.3) lists emissives as blocked on classification: +light sources are BAM overlays mixed with map pixels, with no clean mask. +The ARE file's animation section **is** that classification channel — every +authored ambient animation (fireplaces, torches, chimney smoke, fountain +sprays, glows) is listed with exact world coordinates, a resref, a schedule, +and appearance flags. + +At the `LoadArea` boundary this feature collects the active area's ambient +animations by walking the engine's live `CGameStatic` objects, classifies +them, and publishes an immutable snapshot (`AppContext::areaAnimations`). + +The first consumer is the fpSEAM point-effects pass: the snapshot's shown +fire/smoke/light entries are packed into a 32-slot `uIeePoints[]` uniform +array (`build_area_effect_points`, fire prioritized under capacity pressure) +and fed through the existing uniform bridge. The shader renders warm +flickering fire glow with heat shimmer above flames, drifting chimney-smoke +haze columns, and steady candle/glow halos — background layer only, gated by +`[Shaders] EnablePointEffects` and the F10 master toggle. Effects that must +composite over sprites (true bloom) remain future P4 work consuming the same +points. + +Scope boundary: **water bodies are not ARE animations.** Rivers/lakes/sea are +WED overlays and stay on the existing `wed_runtime`/`tile_liquid` path. ARE +animations cover point phenomena only (the fountain *spray*, not the pool). + +## Format facts (verified) + +Layout mirrored from NearInfinity (`org.infinity.resource.are.{AreResource,Animation}`) +and validated against real BG2EE data (AR0406, AR0700; see below). + +- ARE V1.0 header: animation **count at +0xAC**, section **offset at +0xB0**. + Only "AREA"/"V1.0" is accepted; IWD2 "V9.1" shifts offsets and fails closed. +- Animation record: fixed **76 bytes** — + name[32], X u16 @+32, Y u16 @+34 (world pixels; tile = coord/64), + schedule u32 @+36 (hour bitmask), resref[8] @+40 (BAM; EE also WBM/PVRZ), + flags u32 @+52, height i16 @+56, translucency u16 @+58. +- Flag bits (NearInfinity EE `FLAGS_ARRAY`): bit0 `Is shown`, bit1 `No shadow`, + bit2 `Not light source`, bit8 `Draw as background`, bit13 `EE: Use WBM`, + bit15 `EE: Use PVRZ`. **Animations are light sources unless bit2 is set** — + confirmed on real data: `FIRE_4` entries have bit2 clear, fly-swarm + `FLIESS` entries have bit2 set. + +## Classification + +`classify_area_animation(resref, name)` → `Fire | Smoke | Fountain | Light | None`. +Conservative prefix/substring tables; unknown entries stay `None` and are +logged per area ("ARE unclassified animation resrefs for …"). **That log line +is the mechanism that grows the table from real game data** — do not pad the +table speculatively. + +Evidence so far (full NearInfinity JSON exports, classified by the real +implementation): + +- BG1EE (596 areas, 8,517 animations): 97.7% classified — fire 7,190, + wildlife 633, smoke/fog/steam 199, light 174 (candle-named flames, glows), + water 104, fountain 16, lava 5. The remaining 196 are setpiece VFX and + static scenery (portals, dust devils, explosions, trees, drawbridges) and + intentionally stay `None`. +- BG2EE (423 areas, 3,443 animations): 84.2% classified. The remainder is + the long tail of the per-area `AM` overlay family — the + high-count members were identified by visual frame inspection of exported + BAMs and live in the built-in exact-resref table + (`kExactResrefKinds`); a curated per-resref override file for the rest is + the open proposal. +- Frame inspection also disproved a rule: `ARW[DN]*` overlays are + day/night SHADOW scenery, not lit windows. Do not classify them as light. +- Open classification questions live in the owner's + `Documents/ARE_DATA_QUESTIONS.md` triage file. + +## Primary path: in-memory CGameStatic walk + +Reverse-engineered from the PDB-named Ghidra decompilation of BGEE 2.6.6.0 +and cross-checked against the binary's disassembly: + +- The ARE animation record is `CAreaFileStaticObject` in the engine (our + `ARE_Animation_st`, byte-identical). During area load the engine reads + count/+0xAC and offset/+0xB0, strides 0x4C, and news a **`CGameStatic`** + (0x368 bytes) per record — confirming the disk parser's layout from the + engine's own loader. +- `CGameStatic` derives from the 0x60-byte `CGameObject` base + (`m_objectType` +0x8 — statics are type `0x30`/`'0'`; `m_pos` +0xC; + `m_pArea` +0x18) and **retains the raw authored record at +0x60** + (`m_header`). Script actions (`StaticStart` show/hide, palette swaps) + mutate it in place, so the walk sees live state. +- Objects resolve through `CGameObjectArray`'s **static** globals: + `CGameObjectArray::GetShare` (RVA `0x276490` on 2.6.6.0) bounds-checks + `m_maxArrayIndex` (`0x68D434`) / `m_nextObjectId` (`0x68D438`) and indexes + a fixed entry table at `0x68D450` — `{int16 objectId; CGameObject* ptr}`, + stride 16, 15-bit index space. +- Resolution at runtime: the manifest carries EEex's version-independent + GetShare pattern (`48 C7 02 00 00 00 00 83 F9 FF`, from + `InfinityLoader.db`'s generic section); a unique match plus RIP-operand + decode of `cmp WORD PTR [rip+d], ax` and `lea r8, [rip+d]` + (`object_statics.cpp`) yields the two globals. Offline checks against the + real binaries: + - 2.6.6.0: one match at `0x276490`; operands decode to + `0x68D434`/`0x68D450`. + - 2.7.3.0 (Steam install): one match at `0x276700`, byte-identical body + shape (same operand offsets); operands decode to `0x68F8F4`/`0x68F910`. + - Both builds contain exactly one `operator new(0x368)` + + `imul ..., 0x4C` site (the CGameStatic allocation in the area loader), + confirming the object size and record stride are unchanged on 2.7.3. +- Any ambiguity (pattern count ≠ 1, duplicate/missing operands) fails closed + and disables the scan for the session. + +The walk runs fresh on every area refresh (no cache): ~`m_maxArrayIndex` +bounded reads through `safe_read`, no hooks, no writes. + +A disk reader (override → `chitin.key`/BIFF) existed as a fallback during +development and was removed by owner decision once the memory path was +offline-verified on both builds — the ARE V1.0 parser (`parse_are_animations`) +remains as the host-testable format contract for the shared record layout. +Recover the locator from branch history (`key_bif.*`) if a future build +breaks the pattern. + +## Threading and lifecycle + +- `refresh_area_animations` runs inside `refresh_wed_cache` (LoadArea thread, + or the render thread on post-transition re-resolution), after + `resolve_active_area` and independent of WED parse success. +- The walk happens outside all locks; publication takes the shared commit + mutex and re-checks the WED refresh generation, so a stale result can + never overwrite a newer area's snapshot. +- `[Detection] AreaAnimationScan` (default `true`) gates the whole path; + every failure is log-and-disable, never a gameplay block. + +## Remaining validation (Windows/in-game) + +- Confirm the memory walk in-game on 2.6.6.x: `LoadArea`-time collection + matches the authored ARE (NearInfinity or the offline probe is the + cross-check), and script show/hide toggles appear in refreshed snapshots. +- 2.7.3.x: GetShare pattern, globals, and the CGameStatic allocation site are + offline-verified against the Steam 2.7.3.0 binary (see above); the + remaining gap is the same in-game confirmation as 2.6.6.x. +- Grow the classification table from the unclassified-resref logs across a + playthrough before any consumer ships. diff --git a/docs/tile-upscale.md b/docs/tile-upscale.md index d806bd3..9302ab0 100644 --- a/docs/tile-upscale.md +++ b/docs/tile-upscale.md @@ -17,12 +17,15 @@ engine's 64x64 screen-space quad and leaving ARE/WED coordinates untouched. 4. If the header is missing, inspect up to 32 entries from the 12-byte PVR table. Validate page/coordinate bounds, then take the GCD of non-zero `u`/`v` deltas between entries on the same atlas page. -5. Only if both deterministic paths fail, fall back to the legacy UV / texture-id heuristic path and log that fallback. +5. If both deterministic paths fail, detection returns nothing and the render + path samples up to 10 draws before delegating the tileset to the engine as + standard 1x. The header is authoritative when present. Standard tilesets can legitimately have `header == null`, so table-based detection is also an expected deterministic path. Raw `u`/`v` origins are never tile -dimensions; only their translation-invariant grid deltas are considered. Heuristics are a final -safety net. +dimensions; only their translation-invariant grid deltas are considered. The former UV / texture-id +heuristic tier was removed after producing false 4x detections on vanilla areas in-game (raw UV +magnitude is atlas-origin noise and GL texture ids grow with session allocations). The value at `+0x14` is TIS metadata. The PVRZ header describes the atlas page and does not replace this logical tile-size field. Header and table inference @@ -80,5 +83,6 @@ The current build still uses the TIS `+0x1DC` linear-tiles switch for the seam/l ## Failure Model - Missing manifest or incompatible callsites abort initialization. -- Missing tile metadata falls back to heuristics. +- Missing tile metadata fails closed: after the sampling window the tileset is + delegated to the engine as standard 1x. - Unknown builds should stop during initialization rather than silently running with invalid hooks. diff --git a/docs/validation/bgee-2.7.3-evidence.md b/docs/validation/bgee-2.7.3-evidence.md index b373622..48cef2d 100644 --- a/docs/validation/bgee-2.7.3-evidence.md +++ b/docs/validation/bgee-2.7.3-evidence.md @@ -17,6 +17,15 @@ Runbook: [new-build-validation.md](../new-build-validation.md) |---|---|---|---|---|---| | `CInfGame::LoadArea` | unchanged 2.6.6 pattern | exactly 1 | `0x27EBD0` | `0x27E710` | `+0x4C0` | | `CVidTile::RenderTexture` | unchanged 2.6.6 pattern | exactly 1 | `0x4257C0` | `0x4247E0` | `+0xFE0` | +| `CGameObjectArray::GetShare` | `48 C7 02 00 00 00 00 83 F9 FF` | exactly 1 | `0x276700` | `0x276490` | `+0x270` | + +GetShare's body is byte-identical in shape on both builds (RIP operands at ++0x1B/+0x35): 2.7.3 globals decode to `m_maxArrayIndex = 0x68F8F4` and the +entry table at `0x68F910` (2.6.6: `0x68D434`/`0x68D450`). Both binaries hold +exactly one `operator new(0x368)` + `imul ..., 0x4C` CGameStatic allocation +site, so the static object size and ARE record stride are unchanged +(2026-07-17, ARE-animation memory path; see +[are-animation-detection.md](../are-animation-detection.md)). Both prologues are byte-identical to 2.6.6 (24-byte dumps recorded in the validation session log). diff --git a/src/iee/app_context.h b/src/iee/app_context.h index 4d8496d..bab870d 100644 --- a/src/iee/app_context.h +++ b/src/iee/app_context.h @@ -4,6 +4,7 @@ #include #include "iee/core/config.h" +#include "iee/game/are_animations.h" #include "iee/game/build_manifest.h" #include "iee/game/game_addrs.h" #include "iee/game/renderer.h" @@ -24,11 +25,16 @@ struct AppContext { // the active area when transitions settle after LoadArea returns. std::atomic infGame{nullptr}; std::atomic> wed{}; + // Authored ARE ambient animations of the active area, classified for + // point-effect placement. Detection data only; no render path consumes + // it yet. + std::atomic> areaAnimations{}; game::ResrefBuffer lastLoggedWedArea{}; void reset_area_state() { activeArea.store(nullptr); wed.store(std::shared_ptr{}); + areaAnimations.store(std::shared_ptr{}); lastLoggedWedArea.fill('\0'); } diff --git a/src/iee/area_state.cpp b/src/iee/area_state.cpp index 100282b..6e6e1bd 100644 --- a/src/iee/area_state.cpp +++ b/src/iee/area_state.cpp @@ -6,14 +6,18 @@ #include #include #include +#include #include #include +#include #include "app_context.h" #include "iee/core/gl_state_guard.h" #include "iee/core/logger.h" #include "iee/core/pattern_scanner.h" +#include "iee/game/are_animations.h" #include "iee/game/area_texture.h" +#include "iee/game/object_statics.h" #include "iee/game/opengl_types.h" #include "iee/game/resref_runtime.h" #include "iee/game/texture_units.h" @@ -64,6 +68,162 @@ void queue_no_liquid_snapshot() { queue_area_gpu_snapshot(std::move(noLiquid), {0.5f, 0.5f, 0.5f}); } +constexpr std::size_t kMaxUnclassifiedResrefsLogged = 12; + +// Per-area log dedupe across the LoadArea and render-thread refresh paths. +std::mutex g_areAnimationsLogMutex; +game::ResrefBuffer g_lastLoggedAreResref{}; + +bool read_area_resref(const game::CGameArea* area, game::ResrefBuffer& out) { + game::CResRef runtimeResref{}; + const auto* resrefAddress = + reinterpret_cast(area) + offsetof(game::CGameArea, m_resref); + return core::safe_read(resrefAddress, runtimeResref) && + game::read_runtime_resref(runtimeResref.m_resRef.data(), out); +} + +void log_area_animation_summary(const game::AreaAnimationsInfo& info) { + std::size_t shown = 0; + for (const auto& animation : info.animations) { + if (animation.isShown()) ++shown; + } + LOG_INFO( + "ARE animations {}: total={}, shown={}, fire={}, smoke={}, fountain={}, light={}, " + "water={}, lava={}, wildlife={}, unclassified={}", + info.areaResrefView(), info.animations.size(), shown, + info.count_of(game::AreaAnimationKind::Fire), info.count_of(game::AreaAnimationKind::Smoke), + info.count_of(game::AreaAnimationKind::Fountain), + info.count_of(game::AreaAnimationKind::Light), info.count_of(game::AreaAnimationKind::Water), + info.count_of(game::AreaAnimationKind::Lava), + info.count_of(game::AreaAnimationKind::Wildlife), + info.count_of(game::AreaAnimationKind::None)); + + std::string unclassified; + std::size_t unclassifiedListed = 0; + for (const auto& animation : info.animations) { + if (animation.kind != game::AreaAnimationKind::None) { + LOG_DEBUG("ARE animation {}: kind={}, resref={}, name=\"{}\", pos=({}, {}), shown={}, " + "lightSource={}", + info.areaResrefView(), game::area_animation_kind_name(animation.kind), + animation.resrefView(), animation.nameView(), animation.x, animation.y, + animation.isShown(), animation.isLightSource()); + continue; + } + if (unclassifiedListed < kMaxUnclassifiedResrefsLogged && !animation.resrefView().empty() && + unclassified.find(animation.resrefView()) == std::string::npos) { + if (!unclassified.empty()) unclassified += ", "; + unclassified += animation.resrefView(); + ++unclassifiedListed; + } + } + if (!unclassified.empty()) { + // The visibility that grows the classification table from real data. + LOG_INFO("ARE unclassified animation resrefs for {}: {}", info.areaResrefView(), unclassified); + } +} + +// Resolves the CGameObjectArray globals once per process from the manifest's +// GetShare pattern. Any ambiguity fails closed and latches, disabling the +// scan for the session. +const game::ObjectArrayGlobals& resolved_object_array(const game::BuildManifest& manifest) { + static const game::ObjectArrayGlobals globals = [&manifest] { + game::ObjectArrayGlobals resolved{}; + if (manifest.patterns.objectArrayGetShare.empty()) { + return resolved; + } + std::size_t matchCount = 0; + auto* function = core::find_unique_in_module( + nullptr, manifest.patterns.objectArrayGetShare, &matchCount); + if (!function) { + LOG_WARN("ARE animation scan disabled: GetShare pattern matched {} times", matchCount); + return resolved; + } + if (!game::decode_object_array_globals(static_cast(function), 0x60, + resolved)) { + LOG_WARN("ARE animation scan disabled: GetShare RIP operands did not decode"); + resolved = {}; + return resolved; + } + const auto moduleBase = reinterpret_cast(GetModuleHandleW(nullptr)); + LOG_INFO("CGameObjectArray resolved: GetShare RVA=0x{:X} (reference 0x{:X}), entries RVA=0x{:X}, " + "maxIndex RVA=0x{:X}", + reinterpret_cast(function) - moduleBase, + manifest.referenceRvas.objectArrayGetShare, + reinterpret_cast(resolved.entries) - moduleBase, + reinterpret_cast(resolved.maxArrayIndex) - moduleBase); + return resolved; + }(); + return globals; +} + +// Collects and classifies the active area's authored ARE ambient animations +// from the live CGameStatic objects in the engine's object array — fresh on +// every refresh (cheap, reflects script toggles and save state). Publication +// reuses the WED refresh generation so a stale result can never overwrite a +// newer area's snapshot. +void refresh_area_animations(AppContext& ctx, const game::CGameArea* area, + std::uint64_t refreshGeneration) noexcept { + if (!ctx.cfg.enableAreaAnimationScan || !area) { + return; + } + try { + game::ResrefBuffer areaResref{}; + if (!read_area_resref(area, areaResref) || game::resref_view(areaResref).empty()) { + LOG_DEBUG("ARE animation scan: active area resref unavailable"); + return; + } + + const auto& objectArray = resolved_object_array(*ctx.manifest); + if (!objectArray.valid()) { + return; // Already logged once at resolution time. + } + + game::AreaAnimationsInfo info{}; + if (!game::collect_area_static_animations(objectArray, area, info)) { + LOG_DEBUG("ARE animation walk failed for {}", game::resref_view(areaResref)); + return; + } + info.areaResref = areaResref; + auto snapshot = std::make_shared(std::move(info)); + + // Pack the shader point set outside the commit lock; publish it together + // with the snapshot under the same generation gate. + std::vector effectPoints; + if (ctx.cfg.enablePointEffects) { + effectPoints = game::build_area_effect_points(*snapshot); + } + + { + std::lock_guard commitLock(g_areaRefreshCommitMutex); + if (g_areaRefreshGeneration.load(std::memory_order_acquire) != refreshGeneration) { + LOG_DEBUG("Discarding stale ARE animation refresh generation {}", refreshGeneration); + return; + } + ctx.areaAnimations.store(snapshot); + static_assert(sizeof(game::AreaEffectPoint) == 4 * sizeof(float)); + probe::set_area_effect_points( + effectPoints.empty() ? nullptr : &effectPoints.front().x, effectPoints.size()); + } + + // The walk reruns every refresh; log the summary once per area resref. + bool logSummary = false; + { + std::lock_guard logLock(g_areAnimationsLogMutex); + logSummary = g_lastLoggedAreResref != areaResref; + if (logSummary) g_lastLoggedAreResref = areaResref; + } + if (logSummary) { + log_area_animation_summary(*snapshot); + LOG_INFO("ARE effect points published: {} (pointEffects={})", effectPoints.size(), + ctx.cfg.enablePointEffects); + } + } catch (const std::exception& e) { + LOG_ERROR("ARE animation refresh failed: {}", e.what()); + } catch (...) { + LOG_ERROR("ARE animation refresh failed with an unknown exception"); + } +} + const game::CGameArea* read_loaded_area_candidate(const game::CGameArea* candidate) { if (!candidate) { return nullptr; @@ -175,6 +335,7 @@ void refresh_wed_cache(AppContext& ctx, void* infGame) { std::lock_guard commitLock(g_areaRefreshCommitMutex); refreshGeneration = g_areaRefreshGeneration.fetch_add(1, std::memory_order_acq_rel) + 1; queue_no_liquid_snapshot(); + probe::set_area_effect_points(nullptr, 0); ctx.activeArea.store(nullptr); ctx.wed.store(std::shared_ptr{}); } @@ -198,6 +359,10 @@ void refresh_wed_cache(AppContext& ctx, void* infGame) { return; } + // Independent of WED parsing: a WED failure must not cost the authored + // ARE animation classification, and vice versa. + refresh_area_animations(ctx, area, refreshGeneration); + const auto* areaBytes = reinterpret_cast(area); game::CResWED* wedPointer = nullptr; if (!core::safe_read(areaBytes + offsetof(game::CGameArea, m_pResWED), wedPointer) || @@ -376,6 +541,7 @@ void reset_gpu_area_state() noexcept { std::lock_guard commitLock(g_areaRefreshCommitMutex); g_areaRefreshGeneration.fetch_add(1, std::memory_order_acq_rel); queue_no_liquid_snapshot(); + probe::set_area_effect_points(nullptr, 0); } catch (...) { // The previous immutable GPU snapshot remains valid until another // transition or refresh can publish a complete generation. diff --git a/src/iee/core/config.cpp b/src/iee/core/config.cpp index bef334a..24c96d4 100644 --- a/src/iee/core/config.cpp +++ b/src/iee/core/config.cpp @@ -97,6 +97,12 @@ static void apply_kv(EngineConfig& cfg, const std::string& section, const std::s return; } + // [Detection] + if (iequals(section, "detection")) { + if (iequals(key, "AreaAnimationScan")) assign_bool(cfg.enableAreaAnimationScan); + return; + } + // [Shaders] if (iequals(section, "shaders")) { if (iequals(key, "DumpEngineShaders")) @@ -105,6 +111,8 @@ static void apply_kv(EngineConfig& cfg, const std::string& section, const std::s assign_bool(cfg.enableDebugHotkeys); else if (iequals(key, "EnableWaterEffect")) assign_bool(cfg.enableWaterEffect); + else if (iequals(key, "EnablePointEffects")) + assign_bool(cfg.enablePointEffects); return; } } @@ -182,10 +190,14 @@ bool ConfigManager::save(const std::filesystem::path& path, const EngineConfig& f << "MaxAnisotropy = " << cfg.maxAnisotropy << "\n"; f << "LODBias = " << cfg.lodBias << "\n"; + write_section(f, "Detection"); + write_bool(f, "AreaAnimationScan", cfg.enableAreaAnimationScan); + write_section(f, "Shaders"); write_bool(f, "DumpEngineShaders", cfg.dumpEngineShaders); write_bool(f, "EnableDebugHotkeys", cfg.enableDebugHotkeys); write_bool(f, "EnableWaterEffect", cfg.enableWaterEffect); + write_bool(f, "EnablePointEffects", cfg.enablePointEffects); return true; } diff --git a/src/iee/core/config.h b/src/iee/core/config.h index f6ca12b..50f1d98 100644 --- a/src/iee/core/config.h +++ b/src/iee/core/config.h @@ -11,6 +11,9 @@ struct EngineConfig { bool dumpEngineShaders = false; bool enableDebugHotkeys = false; bool enableWaterEffect = true; + bool enablePointEffects = true; + + bool enableAreaAnimationScan = true; bool enableVerboseLogging = false; bool enablePerformanceLogging = false; diff --git a/src/iee/features/tile_render.cpp b/src/iee/features/tile_render.cpp index 3ca5bcb..792fb6b 100644 --- a/src/iee/features/tile_render.cpp +++ b/src/iee/features/tile_render.cpp @@ -103,11 +103,6 @@ bool render_tile(AppContext& ctx, void* vidTile, int texId, void* unused, int x, detection->scaleFactor, reinterpret_cast(tileInfo.tileset), detection->detectedTileDimension); break; - case game::ScaleDetectionSource::Heuristic: - LOG_INFO("Detected {}x tileset 0x{:X} via heuristic fallback (texId={}, UV=({}, {}))", - detection->scaleFactor, reinterpret_cast(tileInfo.tileset), - texId, entry.u, entry.v); - break; } if (detection->scaleFactor == 1) { diff --git a/src/iee/game/are_animations.cpp b/src/iee/game/are_animations.cpp new file mode 100644 index 0000000..4643164 --- /dev/null +++ b/src/iee/game/are_animations.cpp @@ -0,0 +1,291 @@ +#include "are_animations.h" + +#include +#include +#include +#include +#include + +#include "file_formats.h" + +namespace iee::game { +namespace { +constexpr std::uint32_t kAreFileType = 0x41455241; // "AREA" +constexpr std::uint32_t kAreFileVersion = 0x302E3156; // "V1.0" + +template +bool read_struct(const std::byte* base, std::size_t size, std::size_t offset, T& out) noexcept { + if (offset > size || size - offset < sizeof(T)) { + return false; + } + + std::memcpy(&out, base + offset, sizeof(T)); + return true; +} + +ResrefBuffer copy_resref(const std::array& raw) noexcept { + ResrefBuffer out{}; + out.fill('\0'); + for (std::size_t i = 0; i < 8; ++i) { + const auto c = static_cast(raw[i]); + if (c == '\0') break; + out[i] = c; + } + return out; +} + +std::string upper_copy(std::string_view value) { + std::string result(value); + std::transform(result.begin(), result.end(), result.begin(), [](unsigned char character) { + return static_cast(std::toupper(character)); + }); + return result; +} + +bool starts_with_any(std::string_view value, + std::initializer_list prefixes) noexcept { + for (const auto prefix : prefixes) { + if (value.starts_with(prefix)) return true; + } + return false; +} + +bool contains_any(std::string_view value, std::initializer_list needles) { + for (const auto needle : needles) { + if (value.find(needle) != std::string_view::npos) return true; + } + return false; +} +} // namespace + +std::string_view area_animation_kind_name(AreaAnimationKind kind) noexcept { + switch (kind) { + case AreaAnimationKind::Fire: + return "fire"; + case AreaAnimationKind::Smoke: + return "smoke"; + case AreaAnimationKind::Fountain: + return "fountain"; + case AreaAnimationKind::Light: + return "light"; + case AreaAnimationKind::Water: + return "water"; + case AreaAnimationKind::Lava: + return "lava"; + case AreaAnimationKind::Wildlife: + return "wildlife"; + case AreaAnimationKind::None: + default: + return "none"; + } +} + +namespace { +// Per-area overlays whose kind was established by visual frame inspection of +// the exported BAMs (see docs/are-animation-detection.md). Exact matches win +// over every generic rule. +struct ExactResrefKind { + std::string_view resref; + AreaAnimationKind kind; +}; +constexpr ExactResrefKind kExactResrefKinds[] = { + {"AM003XA", AreaAnimationKind::Fire}, // lit hearth overlay (many BG2 city areas) + {"AM4000Z", AreaAnimationKind::Fire}, // flame columns (AR4000 heads) + {"AM5204D", AreaAnimationKind::Fire}, // bonfire + {"AM5508D", AreaAnimationKind::Fire}, // ember glow arc + {"AM5508C", AreaAnimationKind::Light}, // glow orb + {"AM0202FL", AreaAnimationKind::Light}, // star glint/flare + {"AM6004A", AreaAnimationKind::Smoke}, // dark smoke plume + {"AM6004B", AreaAnimationKind::Smoke}, // dark smoke plume + {"AM0604A", AreaAnimationKind::Fountain}, // tiered fountain +}; +} // namespace + +// Table grown from the full BG1EE + BG2EE ARE exports plus visual frame +// inspection of the ambiguous BAMs. Candle-named flames are deliberately +// Light (dim glow), bare FLM*/FLSM* flames are Fire. Note: ARW[DN]* +// overlays are day/night SHADOW scenery, not lit windows — verified from +// frames; they intentionally stay None. +AreaAnimationKind classify_area_animation(std::string_view resref, + std::string_view name) noexcept { + if (resref.empty() && name.empty()) return AreaAnimationKind::None; + + const auto upperResref = upper_copy(resref); + const auto upperName = upper_copy(name); + + for (const auto& exact : kExactResrefKinds) { + if (upperResref == exact.resref) return exact.kind; + } + + if (upperName.find("CANDLE") != std::string_view::npos) { + return AreaAnimationKind::Light; + } + // "FPIT" (fire pit) confirmed against BG2EE AR0406; FLM*/FLSM* are the + // BG1EE small/medium/large flame BAM families (sconces, wall flames); + // FIM* are yellow flames (OH areas) and YSFL* the blue flames of AR1009 — + // both verified from exported frames. + if (starts_with_any(upperResref, + {"FLAM", "FIRE", "TORCH", "BRAZ", "FPIT", "FLM", "FLSM", "FIM", "YSFL"}) || + contains_any(upperName, {"FIRE", "FLAME", "TORCH", "BRAZIER", "SCONCE"})) { + return AreaAnimationKind::Fire; + } + // Steam pipes (BG2EE AR3017 planar machinery, sewers) read as smoke. + if (starts_with_any(upperResref, {"SMOK", "CHIM", "STEAM", "AMSTEAM"}) || + contains_any(upperName, {"SMOKE", "CHIMNEY", "FOG", "MIST", "STEAM"})) { + return AreaAnimationKind::Smoke; + } + if (starts_with_any(upperResref, {"FOUNT", "FNTN"}) || + contains_any(upperName, {"FOUNTAIN"})) { + return AreaAnimationKind::Fountain; + } + if (contains_any(upperName, {"LAVA"})) { + return AreaAnimationKind::Lava; + } + if (starts_with_any(upperResref, {"SPLASH", "RIPPLE", "WTDBL", "BUBBLE"}) || + contains_any(upperName, {"WATERFALL", "WATER", "SPLASH", "RIPPLE", "LAKE", "RIVER", + "BUBBLE"})) { + return AreaAnimationKind::Water; + } + if (starts_with_any(upperResref, {"BUTRFLY", "FLIES", "FISH", "BIRD"}) || + contains_any(upperName, {"FISH", "FLIES", "BUTTERFLY", "BUTRFLY", "MAGGOT"})) { + return AreaAnimationKind::Wildlife; + } + // "LIGHTNING" is weather, not an authored light source. + const bool nameIsLightning = upperName.find("LIGHTNING") != std::string_view::npos; + if (starts_with_any(upperResref, {"GLOW", "CANDL", "LANT", "LAMP"}) || + (!nameIsLightning && contains_any(upperName, {"GLOW", "LANTERN", "LAMP", "LIGHT"}))) { + return AreaAnimationKind::Light; + } + return AreaAnimationKind::None; +} + +std::string_view AreaAnimationInfo::nameView() const noexcept { + std::size_t length = 0; + while (length < name.size() - 1 && name[length] != '\0') ++length; + return {name.data(), length}; +} + +bool AreaAnimationInfo::isShown() const noexcept { + return (flags & kAreAnimationFlagIsShown) != 0; +} + +bool AreaAnimationInfo::isLightSource() const noexcept { + return (flags & kAreAnimationFlagNotLightSource) == 0; +} + +std::size_t AreaAnimationsInfo::count_of(AreaAnimationKind kind) const noexcept { + std::size_t count = 0; + for (const auto& animation : animations) { + if (animation.kind == kind) ++count; + } + return count; +} + +bool parse_are_animations(const std::byte* data, std::size_t size, + AreaAnimationsInfo& out) noexcept { + out = {}; + + if (!data || size < sizeof(ARE_Header_st)) { + return false; + } + + ARE_Header_st header{}; + if (!read_struct(data, size, 0, header)) { + return false; + } + if (header.nFileType != kAreFileType || header.nFileVersion != kAreFileVersion) { + return false; + } + + const auto count = static_cast(header.nAnimations); + if (count == 0) { + return true; + } + if (count > kMaxAreaAnimationRecords) { + return false; + } + + const auto sectionOffset = static_cast(header.nAnimationsOffset); + const auto sectionBytes = count * sizeof(ARE_Animation_st); + if (sectionOffset > size || sectionBytes > size - sectionOffset) { + return false; + } + + try { + out.animations.reserve(count); + } catch (...) { + out = {}; + return false; + } + + for (std::size_t i = 0; i < count; ++i) { + ARE_Animation_st record{}; + if (!read_struct(data, size, sectionOffset + i * sizeof(ARE_Animation_st), record)) { + out = {}; + return false; + } + + try { + out.animations.push_back(make_area_animation_info(record)); + } catch (...) { + out = {}; + return false; + } + } + + return true; +} + +std::vector build_area_effect_points(const AreaAnimationsInfo& info) { + std::vector points; + points.reserve((std::min)(info.animations.size(), kMaxAreaEffectPoints)); + + const auto strengthFor = [](AreaAnimationKind kind) noexcept { + switch (kind) { + case AreaAnimationKind::Fire: + return 1.0f; + case AreaAnimationKind::Smoke: + return 1.0f; + case AreaAnimationKind::Light: + return 0.6f; // candles/glows: smaller, steadier halo + default: + return 0.0f; + } + }; + + // Fire carries the effect's visual identity; when an area exceeds the + // uniform capacity, drop smoke before light before fire. + const AreaAnimationKind passes[] = {AreaAnimationKind::Fire, AreaAnimationKind::Light, + AreaAnimationKind::Smoke}; + for (const auto pass : passes) { + for (const auto& animation : info.animations) { + if (animation.kind != pass || !animation.isShown()) continue; + if (points.size() >= kMaxAreaEffectPoints) return points; + points.push_back(AreaEffectPoint{static_cast(animation.x), + static_cast(animation.y), + static_cast(static_cast(animation.kind)), + strengthFor(animation.kind)}); + } + } + return points; +} + +AreaAnimationInfo make_area_animation_info(const ARE_Animation_st& record) noexcept { + AreaAnimationInfo animation{}; + animation.x = record.nX; + animation.y = record.nY; + animation.height = record.nHeight; + animation.schedule = record.nSchedule; + animation.flags = record.nFlags; + animation.translucency = record.nTranslucency; + animation.resref = copy_resref(record.rrAnimation); + animation.name.fill('\0'); + for (std::size_t c = 0; c < record.szName.size(); ++c) { + const auto character = static_cast(record.szName[c]); + if (character == '\0') break; + animation.name[c] = character; + } + animation.kind = classify_area_animation(animation.resrefView(), animation.nameView()); + return animation; +} +} // namespace iee::game diff --git a/src/iee/game/are_animations.h b/src/iee/game/are_animations.h new file mode 100644 index 0000000..5126ec1 --- /dev/null +++ b/src/iee/game/are_animations.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "resref_runtime.h" + +namespace iee::game { +// Classification of an authored ARE ambient animation for point-effect +// placement (graphics roadmap §10.3: the authored classification channel for +// emissive candidates). Water bodies stay on the WED overlay path. +enum class AreaAnimationKind : int { + None = 0, + Fire = 1, + Smoke = 2, // includes authored fog/mist + Fountain = 3, + Light = 4, // candles, glows, lit night windows + Water = 5, // splashes, ripples, waterfalls, scrolling water + Lava = 6, + Wildlife = 7, // fish, flies, butterflies — explicitly no effect +}; + +[[nodiscard]] std::string_view area_animation_kind_name(AreaAnimationKind kind) noexcept; + +// Conservative resref/name prefix classification. Unknown entries return +// None; runtime logging of unclassified resrefs is the mechanism that grows +// the table from real game data. +[[nodiscard]] AreaAnimationKind classify_area_animation(std::string_view resref, + std::string_view name) noexcept; + +struct AreaAnimationInfo { + std::uint16_t x{}; + std::uint16_t y{}; + std::int16_t height{}; + std::uint32_t schedule{}; + std::uint32_t flags{}; + std::uint16_t translucency{}; + ResrefBuffer resref{}; + // Authored editor label, NUL-terminated (source field is 32 bytes). + std::array name{}; + AreaAnimationKind kind{AreaAnimationKind::None}; + + [[nodiscard]] std::string_view resrefView() const noexcept { return resref_view(resref); } + [[nodiscard]] std::string_view nameView() const noexcept; + [[nodiscard]] bool isShown() const noexcept; + [[nodiscard]] bool isLightSource() const noexcept; +}; + +struct AreaAnimationsInfo { + ResrefBuffer areaResref{}; + std::vector animations{}; + + [[nodiscard]] std::string_view areaResrefView() const noexcept { + return resref_view(areaResref); + } + [[nodiscard]] std::size_t count_of(AreaAnimationKind kind) const noexcept; +}; + +// Shared ceiling for authored animation records, whichever channel supplies +// them (disk parse or the in-memory CGameStatic walk). +inline constexpr std::size_t kMaxAreaAnimationRecords = 4096; + +struct ARE_Animation_st; + +// Converts one raw authored record (identical layout on disk and inside a +// live CGameStatic) into a classified AreaAnimationInfo. +[[nodiscard]] AreaAnimationInfo make_area_animation_info(const ARE_Animation_st& record) noexcept; + +// Parses the animation section out of a complete ARE V1.0 file image. +// Bounded and fail-closed: malformed counts/offsets return false and leave +// `out` empty. Other ARE versions (e.g. IWD2 V9.1) are rejected. +[[nodiscard]] bool parse_are_animations(const std::byte* data, std::size_t size, + AreaAnimationsInfo& out) noexcept; + +// One shader point effect: world-pixel position, the AreaAnimationKind as a +// float, and a per-kind strength scale. Matches the uIeePoints vec4 layout. +struct AreaEffectPoint { + float x{}; + float y{}; + float kind{}; + float strength{}; +}; + +// The fpSEAM override's fixed uniform-array capacity. +inline constexpr std::size_t kMaxAreaEffectPoints = 32; + +// Selects the shown fire/smoke/light animations as shader points, fire first +// so the capacity cap drops the least impactful kinds. Returns at most +// kMaxAreaEffectPoints entries. +[[nodiscard]] std::vector build_area_effect_points( + const AreaAnimationsInfo& info); +} // namespace iee::game diff --git a/src/iee/game/build_manifest.cpp b/src/iee/game/build_manifest.cpp index fc3002a..a09bfc1 100644 --- a/src/iee/game/build_manifest.cpp +++ b/src/iee/game/build_manifest.cpp @@ -143,8 +143,11 @@ constexpr BuildManifest kKnownBuilds[] = { { "40 55 53 56 57 41 54 41 55 41 56 41 57 48 8D AC 24 48 FD FF FF", "48 8B C4 44 89 48 20 48 83 EC 48 48 89 58 08 8B DA 48 89 68 10", + // CGameObjectArray::GetShare (EEex InfinityLoader.db, version- + // independent section). Verified unique on 2.6.6.0 at 0x276490. + "48 C7 02 00 00 00 00 83 F9 FF", }, - {0x27E710, 0x4247E0}, + {0x27E710, 0x4247E0, 0x276490}, {0x100, 0x1DC, 0x14, 0x6590, 0x6598, 0x65F8}, {{ {"CRes_Demand", 0x36, BranchInstructionKind::CallRel32, 0xE8, 1, 5, true}, @@ -171,8 +174,12 @@ constexpr BuildManifest kKnownBuilds[] = { { "40 55 53 56 57 41 54 41 55 41 56 41 57 48 8D AC 24 48 FD FF FF", "48 8B C4 44 89 48 20 48 83 EC 48 48 89 58 08 8B DA 48 89 68 10", + // Same EEex version-independent pattern. Offline-verified on the + // 2.7.3.0 binary: unique match, identical body shape, globals at + // 0x68F8F4 (max index) / 0x68F910 (entry table). + "48 C7 02 00 00 00 00 83 F9 FF", }, - {0x27EBD0, 0x4257C0}, + {0x27EBD0, 0x4257C0, 0x276700}, {0x100, 0x1DC, 0x14, 0x6590, 0x6598, 0x65F8}, {{ {"CRes_Demand", 0x36, BranchInstructionKind::CallRel32, 0xE8, 1, 5, true}, @@ -194,11 +201,15 @@ static_assert(validate_pattern_format(kKnownBuilds[0].patterns.loadArea), "LoadArea pattern format is invalid"); static_assert(validate_pattern_format(kKnownBuilds[0].patterns.renderTexture), "RenderTexture pattern format is invalid"); +static_assert(validate_pattern_format(kKnownBuilds[0].patterns.objectArrayGetShare), + "GetShare pattern format is invalid"); static_assert(kKnownBuilds[0].validate(), "Known build manifest is invalid"); static_assert(validate_pattern_format(kKnownBuilds[1].patterns.loadArea), "2.7.3 LoadArea pattern format is invalid"); static_assert(validate_pattern_format(kKnownBuilds[1].patterns.renderTexture), "2.7.3 RenderTexture pattern format is invalid"); +static_assert(validate_pattern_format(kKnownBuilds[1].patterns.objectArrayGetShare), + "2.7.3 GetShare pattern format is invalid"); static_assert(kKnownBuilds[1].validate(), "2.7.3 build manifest is invalid"); } // namespace diff --git a/src/iee/game/build_manifest.h b/src/iee/game/build_manifest.h index ee5eeee..d8446c3 100644 --- a/src/iee/game/build_manifest.h +++ b/src/iee/game/build_manifest.h @@ -31,11 +31,18 @@ struct BranchInstructionDesc { struct PatternSet { std::string_view loadArea{}; std::string_view renderTexture{}; + // Optional: CGameObjectArray::GetShare. Sourced from EEex's + // version-independent binding pattern; resolves the static object-array + // globals for the ARE-animation memory path. Empty (or a non-unique match + // at runtime) disables that path — the disk reader remains the fallback. + std::string_view objectArrayGetShare{}; }; struct ReferenceRvas { std::uintptr_t loadArea{}; std::uintptr_t renderTexture{}; + // Diagnostic only; 0 means "not yet observed on this build". + std::uintptr_t objectArrayGetShare{}; }; struct RuntimeOffsets { diff --git a/src/iee/game/file_formats.h b/src/iee/game/file_formats.h index 3372885..e8e4881 100644 --- a/src/iee/game/file_formats.h +++ b/src/iee/game/file_formats.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include namespace iee::game { @@ -115,6 +116,80 @@ namespace iee::game { std::uint32_t dwFlags{}; }; + // ARE V1.0 (BGEE). Only the fields this project consumes are modeled: + // the fixed header prefix that carries the animation section pointers and + // the 76-byte ambient-animation record (layout verified against + // NearInfinity's org.infinity.resource.are.{AreResource,Animation}). + struct ARE_Header_st { + std::uint32_t nFileType{}; // "AREA" + std::uint32_t nFileVersion{}; // "V1.0" + std::array rrWed{}; + std::uint32_t nLastSaved{}; + std::uint32_t nAreaFlags{}; + std::array edges{}; // 4 x (resref + edge flags) + std::uint16_t nLocationFlags{}; // +0x48 + std::uint16_t nRainProbability{}; + std::uint16_t nSnowProbability{}; + std::uint16_t nFogProbability{}; + std::uint16_t nLightningProbability{}; + std::uint8_t nOverlayTransparency{}; // EE; classic wind speed low byte + std::uint8_t ___u0{}; + std::uint32_t nActorsOffset{}; // +0x54 + std::uint16_t nActors{}; + std::uint16_t nTriggers{}; + std::uint32_t nTriggersOffset{}; + std::uint32_t nSpawnPointsOffset{}; + std::uint32_t nSpawnPoints{}; + std::uint32_t nEntrancesOffset{}; + std::uint32_t nEntrances{}; + std::uint32_t nContainersOffset{}; + std::uint16_t nContainers{}; + std::uint16_t nItems{}; + std::uint32_t nItemsOffset{}; + std::uint32_t nVerticesOffset{}; + std::uint16_t nVertices{}; + std::uint16_t nAmbients{}; + std::uint32_t nAmbientsOffset{}; + std::uint32_t nVariablesOffset{}; + std::uint16_t nVariables{}; + std::uint16_t nObjectFlags{}; + std::uint32_t nObjectFlagsOffset{}; + std::array rrAreaScript{}; + std::uint32_t nExploredBitmapSize{}; + std::uint32_t nExploredBitmapOffset{}; + std::uint32_t nDoors{}; + std::uint32_t nDoorsOffset{}; + std::uint32_t nAnimations{}; // +0xAC + std::uint32_t nAnimationsOffset{}; // +0xB0 + }; + + struct ARE_Animation_st { + std::array szName{}; + std::uint16_t nX{}; + std::uint16_t nY{}; + std::uint32_t nSchedule{}; // hour-of-day bitmask + std::array rrAnimation{}; // BAM; EE also WBM/PVRZ + std::uint16_t nAnimationIndex{}; + std::uint16_t nFrameIndex{}; + std::uint32_t nFlags{}; + std::int16_t nHeight{}; + std::uint16_t nTranslucency{}; + std::uint16_t nStartRange{}; + std::uint8_t nLoopProbability{}; + std::uint8_t nStartDelay{}; + std::array rrPalette{}; + std::uint16_t nMovieWidth{}; // EE + std::uint16_t nMovieHeight{}; // EE + }; + + // ARE_Animation_st.nFlags bits (NearInfinity FLAGS_ARRAY, EE variant). + inline constexpr std::uint32_t kAreAnimationFlagIsShown = 1u << 0; + inline constexpr std::uint32_t kAreAnimationFlagNoShadow = 1u << 1; + inline constexpr std::uint32_t kAreAnimationFlagNotLightSource = 1u << 2; + inline constexpr std::uint32_t kAreAnimationFlagDrawAsBackground = 1u << 8; + inline constexpr std::uint32_t kAreAnimationFlagUseWbm = 1u << 13; + inline constexpr std::uint32_t kAreAnimationFlagUsePvrz = 1u << 15; + struct bamHeader_st { std::uint32_t nFileType{}; std::uint32_t nFileVersion{}; @@ -171,6 +246,12 @@ namespace iee::game { static_assert(sizeof(WED_TileData_st) == 0xA); static_assert(sizeof(WED_TiledObject_st) == 0x1A); static_assert(sizeof(WED_WedHeader_st) == 0x2C); + static_assert(sizeof(ARE_Header_st) == 0xB4); + static_assert(offsetof(ARE_Header_st, nAnimations) == 0xAC); + static_assert(offsetof(ARE_Header_st, nAnimationsOffset) == 0xB0); + static_assert(sizeof(ARE_Animation_st) == 0x4C); + static_assert(offsetof(ARE_Animation_st, rrAnimation) == 0x28); + static_assert(offsetof(ARE_Animation_st, nFlags) == 0x34); static_assert(sizeof(bamHeader_st) == 0x18); static_assert(sizeof(BAMHEADERV2) == 0x20); static_assert(sizeof(frame) == 0x18); diff --git a/src/iee/game/object_statics.cpp b/src/iee/game/object_statics.cpp new file mode 100644 index 0000000..40d8a88 --- /dev/null +++ b/src/iee/game/object_statics.cpp @@ -0,0 +1,112 @@ +#include "object_statics.h" + +#include +#include +#include + +#include "iee/core/pattern_scanner.h" + +namespace iee::game { +namespace { +// GetShare instruction encodings holding the RIP-relative globals: +// 66 39 05 cmp WORD PTR [rip+disp], ax -> m_maxArrayIndex +// 4C 8D 05 lea r8, [rip+disp] -> entry table +constexpr std::size_t kRipInstructionSize = 7; + +const std::byte* decode_rip_operand(const std::byte* instruction) noexcept { + std::int32_t displacement = 0; + std::memcpy(&displacement, instruction + 3, sizeof(displacement)); + return instruction + kRipInstructionSize + displacement; +} + +bool matches(const std::byte* code, std::initializer_list bytes) noexcept { + std::size_t index = 0; + for (const auto expected : bytes) { + if (std::to_integer(code[index]) != expected) return false; + ++index; + } + return true; +} +} // namespace + +bool decode_object_array_globals(const std::byte* function, std::size_t windowSize, + ObjectArrayGlobals& out) noexcept { + out = {}; + if (!function || windowSize < kRipInstructionSize || + !core::is_readable(function, windowSize)) { + return false; + } + + const std::byte* maxIndexAddress = nullptr; + const std::byte* entriesAddress = nullptr; + bool ambiguous = false; + for (std::size_t offset = 0; offset + kRipInstructionSize <= windowSize; ++offset) { + const auto* code = function + offset; + if (matches(code, {0x66, 0x39, 0x05})) { + if (maxIndexAddress) ambiguous = true; + maxIndexAddress = decode_rip_operand(code); + } else if (matches(code, {0x4C, 0x8D, 0x05})) { + if (entriesAddress) ambiguous = true; + entriesAddress = decode_rip_operand(code); + } + } + + if (ambiguous || !maxIndexAddress || !entriesAddress) { + return false; + } + + out.maxArrayIndex = reinterpret_cast(maxIndexAddress); + out.entries = reinterpret_cast(entriesAddress); + return true; +} + +bool collect_area_static_animations(const ObjectArrayGlobals& globals, const CGameArea* area, + AreaAnimationsInfo& out) noexcept { + out = {}; + if (!globals.valid() || !area) { + return false; + } + + std::int16_t maxIndex = 0; + if (!core::safe_read(globals.maxArrayIndex, maxIndex) || maxIndex < 0) { + return false; + } + const auto entryCount = + (std::min)(static_cast(maxIndex) + 1, kObjectArrayMaxEntries); + + try { + for (std::size_t index = 0; index < entryCount; ++index) { + CGameObjectArrayEntry entry{}; + if (!core::safe_read(globals.entries + index, entry) || !entry.m_objectPtr) { + continue; + } + + const auto* objectBytes = reinterpret_cast(entry.m_objectPtr); + std::uint8_t objectType = 0; + if (!core::safe_read(objectBytes + offsetof(CGameObject, m_objectType), objectType) || + objectType != kGameObjectTypeStatic) { + continue; + } + const CGameArea* owner = nullptr; + if (!core::safe_read(objectBytes + offsetof(CGameObject, m_pArea), owner) || + owner != area) { + continue; + } + + ARE_Animation_st record{}; + if (!core::safe_read(objectBytes + offsetof(CGameStatic, m_header), record)) { + continue; + } + out.animations.push_back(make_area_animation_info(record)); + if (out.animations.size() >= kMaxAreaAnimationRecords) { + break; + } + } + } catch (...) { + out = {}; + return false; + } + + return true; +} +} // namespace iee::game diff --git a/src/iee/game/object_statics.h b/src/iee/game/object_statics.h new file mode 100644 index 0000000..127da07 --- /dev/null +++ b/src/iee/game/object_statics.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include "are_animations.h" +#include "runtime_types_x64.h" + +namespace iee::game { +// Resolved CGameObjectArray statics. On the validated builds the entry table +// is a fixed static array (not a heap pointer): CGameObjectArray::GetShare +// loads its address with a RIP-relative lea. See +// docs/are-animation-detection.md for the reverse-engineering evidence. +struct ObjectArrayGlobals { + const CGameObjectArrayEntry* entries{}; + const std::int16_t* maxArrayIndex{}; + + [[nodiscard]] bool valid() const noexcept { return entries != nullptr && maxArrayIndex != nullptr; } +}; + +// 15-bit index space per the GetShare locator encoding. +inline constexpr std::size_t kObjectArrayMaxEntries = 0x8000; + +// Decodes the object-array globals out of CGameObjectArray::GetShare's body: +// `cmp WORD PTR [rip+d], ax` (m_maxArrayIndex) and `lea r8, [rip+d]` (entry +// table). `function` points at the manifest pattern match; the scan is +// bounded to `windowSize` bytes and each instruction must occur exactly once +// or the decode fails closed. +[[nodiscard]] bool decode_object_array_globals(const std::byte* function, std::size_t windowSize, + ObjectArrayGlobals& out) noexcept; + +// Walks the engine object array and collects the authored static-animation +// records (CGameStatic::m_header) owned by `area` into classified entries. +// Returns false when the array is unresolved or unreadable; an area with no +// statics yields true with an empty list. +[[nodiscard]] bool collect_area_static_animations(const ObjectArrayGlobals& globals, + const CGameArea* area, + AreaAnimationsInfo& out) noexcept; +} // namespace iee::game diff --git a/src/iee/game/opengl_types.cpp b/src/iee/game/opengl_types.cpp index a3272ac..9aaed1d 100644 --- a/src/iee/game/opengl_types.cpp +++ b/src/iee/game/opengl_types.cpp @@ -150,6 +150,8 @@ bool OpenGLFunctions::initialize() noexcept { glUniform1i = reinterpret_cast(get_ext_proc_address(opengl32, "glUniform1i")); glUniform2f = reinterpret_cast(get_ext_proc_address(opengl32, "glUniform2f")); glUniform3f = reinterpret_cast(get_ext_proc_address(opengl32, "glUniform3f")); + glUniform4fv = + reinterpret_cast(get_ext_proc_address(opengl32, "glUniform4fv")); glActiveTexture = reinterpret_cast(get_ext_proc_address(opengl32, "glActiveTexture")); glCompressedTexImage2D = reinterpret_cast( diff --git a/src/iee/game/opengl_types.h b/src/iee/game/opengl_types.h index 8767b32..b1f9a43 100644 --- a/src/iee/game/opengl_types.h +++ b/src/iee/game/opengl_types.h @@ -92,6 +92,7 @@ using PFN_glUniform1f = void(APIENTRY*)(int location, float v0); using PFN_glUniform1i = void(APIENTRY*)(int location, int v0); using PFN_glUniform2f = void(APIENTRY*)(int location, float v0, float v1); using PFN_glUniform3f = void(APIENTRY*)(int location, float v0, float v1, float v2); +using PFN_glUniform4fv = void(APIENTRY*)(int location, int count, const float* value); using PFN_glActiveTexture = void(APIENTRY*)(unsigned texture); using PFN_glCompressedTexImage2D = void(APIENTRY*)(unsigned target, int level, unsigned internalformat, int width, int height, @@ -144,6 +145,7 @@ struct OpenGLFunctions { PFN_glUniform1i glUniform1i{}; PFN_glUniform2f glUniform2f{}; PFN_glUniform3f glUniform3f{}; + PFN_glUniform4fv glUniform4fv{}; PFN_glActiveTexture glActiveTexture{}; PFN_glCompressedTexImage2D glCompressedTexImage2D{}; PFN_glGenerateMipmap glGenerateMipmap{}; diff --git a/src/iee/game/runtime_types_x64.h b/src/iee/game/runtime_types_x64.h index 19fa404..af506e0 100644 --- a/src/iee/game/runtime_types_x64.h +++ b/src/iee/game/runtime_types_x64.h @@ -395,6 +395,41 @@ namespace iee::game { std::array _tail{}; }; + // Shared CGameObject header prefix of every area game object. Verified + // against the 2.6.6 decompilation (PDB-named); the full base is 0x60 + // bytes. See docs/are-animation-detection.md for the evidence trail. + struct CGameObject { + void *_vtable{}; + std::uint8_t m_objectType{}; + std::byte _pad0[3]{}; + CPoint m_pos{}; + std::int32_t m_posZ{}; + CGameArea *m_pArea{}; + std::array _tail{}; + }; + + // CGameObject::m_objectType for ARE "static" ambient animations + // (script share type '0'; CGameStatic). + inline constexpr std::uint8_t kGameObjectTypeStatic = 0x30; + + // Runtime object created from each ARE animation record. The engine keeps + // the raw authored 76-byte record (CAreaFileStaticObject in the PDB, our + // ARE_Animation_st) embedded at +0x60; script state (e.g. the shown flag + // toggled by StaticStart) mutates it in place. + struct CGameStatic { + CGameObject baseclass_0{}; + ARE_Animation_st m_header{}; + std::array _tail{}; + }; + + // CGameObjectArray's static entry table: object id in the low word, + // object pointer at +0x8, stride 16 (CGameObjectArray::GetShare). + struct CGameObjectArrayEntry { + std::int16_t m_objectId{}; + std::byte _pad0[6]{}; + CGameObject *m_objectPtr{}; + }; + struct CGameSprite { std::array _pad0{}; CResRef m_resref{}; @@ -498,6 +533,14 @@ namespace iee::game { static_assert(offsetof(CGameArea, m_visibility) == 0xBB0); static_assert(offsetof(CGameArea, m_lTiledObjects) == 0xED0); static_assert(offsetof(CGameArea, m_ptOldViewPos) == 0xF78); + static_assert(offsetof(CGameObject, m_objectType) == 0x8); + static_assert(offsetof(CGameObject, m_pos) == 0xC); + static_assert(offsetof(CGameObject, m_pArea) == 0x18); + static_assert(sizeof(CGameObject) == 0x60); + static_assert(offsetof(CGameStatic, m_header) == 0x60); + static_assert(sizeof(CGameStatic) == 0x368); + static_assert(sizeof(CGameObjectArrayEntry) == 0x10); + static_assert(offsetof(CGameObjectArrayEntry, m_objectPtr) == 0x8); static_assert(offsetof(CGameSprite, m_resref) == 0x540); static_assert(offsetof(CGameSprite, m_currentArea) == 0x3A20); static_assert(offsetof(CGameSprite, m_spriteEffectVidCell) == 0x3C70); diff --git a/src/iee/game/tile_upscale.cpp b/src/iee/game/tile_upscale.cpp index 28cfc8a..693c981 100644 --- a/src/iee/game/tile_upscale.cpp +++ b/src/iee/game/tile_upscale.cpp @@ -81,19 +81,9 @@ std::optional infer_scale_from_tile_table(const TileInfo& return std::nullopt; } -bool is_upscaled_by_heuristics(const TileInfo& tileInfo, int textureId) { - if (!tileInfo.table || tileInfo.index < 0 || - static_cast(tileInfo.index) >= tileInfo.tileCount) { - return false; - } - - const auto& entry = tileInfo.entry; - return entry.u > UpscaleThresholds::UV_THRESHOLD || entry.v > UpscaleThresholds::UV_THRESHOLD || - textureId > UpscaleThresholds::TEXTURE_ID_THRESHOLD; -} - std::optional detect_scale(const TileInfo& tileInfo, int textureId, const BuildManifest& manifest) { + (void)textureId; if (auto headerDetection = detect_scale_from_tis_header(tileInfo, manifest)) { return headerDetection; } @@ -102,10 +92,6 @@ std::optional detect_scale(const TileInfo& tileInfo, int t return tableDetection; } - if (is_upscaled_by_heuristics(tileInfo, textureId)) { - return ScaleDetectionResult{4, ScaleDetectionSource::Heuristic, 0}; - } - return std::nullopt; } } // namespace iee::game diff --git a/src/iee/game/tile_upscale.h b/src/iee/game/tile_upscale.h index d9581e5..272cc13 100644 --- a/src/iee/game/tile_upscale.h +++ b/src/iee/game/tile_upscale.h @@ -7,8 +7,6 @@ namespace iee::game { namespace UpscaleThresholds { -constexpr int UV_THRESHOLD = 1024; -constexpr int TEXTURE_ID_THRESHOLD = 10000; constexpr int DETECTION_SAMPLE_COUNT = 10; } // namespace UpscaleThresholds @@ -22,7 +20,6 @@ constexpr std::uint32_t Upscaled8x = 0x200; enum class ScaleDetectionSource : std::uint8_t { TisHeader, TileTable, - Heuristic, }; struct ScaleDetectionResult { @@ -43,9 +40,13 @@ struct ScaleDetectionResult { [[nodiscard]] std::optional infer_scale_from_tile_table( const TileInfo& tileInfo); -[[nodiscard]] bool is_upscaled_by_heuristics(const TileInfo& tileInfo, int textureId); - -// Production scale selection: deterministic metadata first, heuristic last. +// Production scale selection: deterministic metadata only (TIS header, then +// PVR entry-table coordinate grid). When neither resolves, nullopt fails +// closed into the caller's sampling path, which delegates the tileset to the +// engine as standard 1x. Raw UV magnitudes and GL texture ids are not valid +// scale signals (atlas origins are arbitrary; texture ids grow with session +// allocations) — the former heuristic built on them produced false 4x +// detections on vanilla areas and was removed. [[nodiscard]] std::optional detect_scale(const TileInfo& tileInfo, int textureId, const BuildManifest& manifest); diff --git a/src/iee/shader_probe.cpp b/src/iee/shader_probe.cpp index 798dd58..ebb122e 100644 --- a/src/iee/shader_probe.cpp +++ b/src/iee/shader_probe.cpp @@ -1086,6 +1086,10 @@ void set_area_world_size(float widthPx, float heightPx) noexcept { void set_area_water_tint(float r, float g, float b) noexcept { uniforms::set_water_tint(r, g, b); } +void set_area_effect_points(const float* xyzw, std::size_t count) noexcept { + uniforms::set_effect_points(xyzw, count); +} + void set_area_view(float scrollX, float scrollY, float viewWorldW, float viewWorldH) noexcept { uniforms::set_view(scrollX, scrollY, viewWorldW, viewWorldH); } diff --git a/src/iee/shader_probe.h b/src/iee/shader_probe.h index 21f04e7..b5fd3b6 100644 --- a/src/iee/shader_probe.h +++ b/src/iee/shader_probe.h @@ -33,6 +33,10 @@ void set_area_world_size(float widthPx, float heightPx) noexcept; // liquid overlay tiles, linear 0..1). Neutral 0.5 grey = unknown. void set_area_water_tint(float r, float g, float b) noexcept; +// Classified ambient-animation point effects of the current area: +// `count` vec4 records (world x, world y, kind, strength). nullptr/0 clears. +void set_area_effect_points(const float* xyzw, std::size_t count) noexcept; + // Published once per world frame from DrawColorTone(Seam); consumed by the // uniform feed. // viewWorldW/H = rViewPort size (world px visible); the feed derives the diff --git a/src/iee/shader_uniform_bridge.cpp b/src/iee/shader_uniform_bridge.cpp index 05807a8..1edfa90 100644 --- a/src/iee/shader_uniform_bridge.cpp +++ b/src/iee/shader_uniform_bridge.cpp @@ -3,9 +3,13 @@ #include #include +#include #include +#include +#include #include "area_state.h" +#include "iee/core/logger.h" #include "iee/game/opengl_types.h" #include "iee/game/texture_units.h" #include "iee/water_textures.h" @@ -26,6 +30,14 @@ std::atomic g_waterTintB{0.5f}; std::atomic g_effectValue{0.0f}; std::atomic g_feedCount{0}; std::atomic g_stateRevision{1}; + +// Effect point set (area-scoped, written by area refresh threads, read by the +// render-thread feed). The array cannot be atomic; a mutex plus its own +// revision keeps the feed's copy coherent and cheap when unchanged. +std::mutex g_effectPointsMutex; +std::array g_effectPoints{}; +std::size_t g_effectPointCount{0}; +std::atomic g_effectPointsRevision{1}; std::atomic g_performanceCalls{0}; std::atomic g_performanceSkipped{0}; std::atomic g_performanceTextureBindPasses{0}; @@ -96,6 +108,7 @@ void reset() noexcept { g_effectValue.store(0.0f, std::memory_order_relaxed); g_performanceEnabled.store(false, std::memory_order_relaxed); g_feedCount.store(0, std::memory_order_relaxed); + set_effect_points(nullptr, 0); g_stateRevision.store(1, std::memory_order_release); (void)take_performance_stats(); } @@ -138,6 +151,29 @@ void set_view(float scrollX, float scrollY, float viewWorldWidth, float viewWorl if (changed) advance_state_revision(); } +void set_effect_points(const float* xyzw, std::size_t count) noexcept { + try { + if (!xyzw) count = 0; + const auto clamped = (std::min)(count, kMaxEffectPoints); + std::lock_guard lock(g_effectPointsMutex); + if (clamped == g_effectPointCount && + (clamped == 0 || + std::memcmp(g_effectPoints.data(), xyzw, clamped * 4 * sizeof(float)) == 0)) { + return; + } + g_effectPointCount = clamped; + if (clamped > 0 && xyzw) { + std::memcpy(g_effectPoints.data(), xyzw, clamped * 4 * sizeof(float)); + } + std::fill(g_effectPoints.begin() + static_cast(clamped * 4), + g_effectPoints.end(), 0.0f); + g_effectPointsRevision.fetch_add(1, std::memory_order_release); + advance_state_revision(); + } catch (...) { + // A failed point update must never affect rendering. + } +} + Snapshot snapshot() noexcept { return { .effectValue = g_effectValue.load(std::memory_order_relaxed), @@ -181,6 +217,15 @@ void feed(unsigned program, Locations& locations) { locations.worldSizeInv = resolve_location(gl, program, locations.worldSizeInv, "uIeeWorldSizeInv"); locations.waterTint = resolve_location(gl, program, locations.waterTint, "uIeeWaterTint"); + locations.pointCount = resolve_location(gl, program, locations.pointCount, "uIeePointCount"); + // Array uniforms: GL reports the canonical name as "uIeePoints[0]" and some + // drivers only resolve that spelling; try it first, then the bare name. + if (locations.points == Locations::kUnresolved) { + locations.points = gl.glGetUniformLocation(program, "uIeePoints[0]"); + if (locations.points < 0) { + locations.points = gl.glGetUniformLocation(program, "uIeePoints"); + } + } locations.areaMask = resolve_location(gl, program, locations.areaMask, "uIeeAreaMask"); locations.normalMap = resolve_location(gl, program, locations.normalMap, "uIeeNormalMap"); locations.dudvMap = resolve_location(gl, program, locations.dudvMap, "uIeeDudvMap"); @@ -271,6 +316,32 @@ void feed(unsigned program, Locations& locations) { locations.lastWaterTintG = waterTintG; locations.lastWaterTintB = waterTintB; } + const auto pointsRevision = g_effectPointsRevision.load(std::memory_order_acquire); + if (locations.lastPointsRevision != pointsRevision && + (locations.pointCount >= 0 || locations.points >= 0)) { + std::array points{}; + std::size_t pointCount = 0; + { + std::lock_guard lock(g_effectPointsMutex); + points = g_effectPoints; + pointCount = g_effectPointCount; + } + if (locations.pointCount >= 0) { + gl.glUniform1f(locations.pointCount, static_cast(pointCount)); + } + if (locations.points >= 0 && gl.glUniform4fv) { + gl.glUniform4fv(locations.points, static_cast(kMaxEffectPoints), points.data()); + } + if (locations.lastPointsRevision == 0) { + LOG_DEBUG( + "Point uniforms first feed: program={}, countLocation={}, pointsLocation={}, " + "glUniform4fv={}, count={}", + program, locations.pointCount, locations.points, gl.glUniform4fv != nullptr, + pointCount); + } + locations.lastPointsRevision = pointsRevision; + } + if (!locations.samplersInitialized && gl.glUniform1i) { if (locations.areaMask >= 0) gl.glUniform1i(locations.areaMask, static_cast(game::texture_units::AreaMask)); diff --git a/src/iee/shader_uniform_bridge.h b/src/iee/shader_uniform_bridge.h index 6e91314..cc2715a 100644 --- a/src/iee/shader_uniform_bridge.h +++ b/src/iee/shader_uniform_bridge.h @@ -1,9 +1,13 @@ #pragma once +#include #include namespace iee::probe::uniforms { +// Matches the fpSEAM override's uIeePoints[] capacity. +inline constexpr std::size_t kMaxEffectPoints = 32; + struct Locations { static constexpr int kUnresolved = -2; @@ -14,6 +18,8 @@ struct Locations { int viewport{kUnresolved}; int worldSizeInv{kUnresolved}; int waterTint{kUnresolved}; + int pointCount{kUnresolved}; + int points{kUnresolved}; int areaMask{kUnresolved}; int normalMap{kUnresolved}; int dudvMap{kUnresolved}; @@ -23,6 +29,7 @@ struct Locations { bool viewInitialized{}; bool worldSizeInitialized{}; bool waterTintInitialized{}; + std::uint64_t lastPointsRevision{}; float lastScrollX{}; float lastScrollY{}; float lastViewWorldWidth{}; @@ -64,6 +71,9 @@ void set_effect_enabled(bool enabled) noexcept; [[nodiscard]] float cycle_debug_effect() noexcept; void set_world_size(float widthPx, float heightPx) noexcept; void set_water_tint(float r, float g, float b) noexcept; +// `xyzw` holds `count` vec4 point records (world x, world y, kind, strength). +// nullptr or count 0 clears the point set. +void set_effect_points(const float* xyzw, std::size_t count) noexcept; void set_view(float scrollX, float scrollY, float viewWorldWidth, float viewWorldHeight) noexcept; [[nodiscard]] Snapshot snapshot() noexcept; [[nodiscard]] FeedPerformanceStats take_performance_stats() noexcept; diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 142e3a7..2c29ba2 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -19,7 +19,9 @@ #include "iee/core/pattern_scanner.h" #include "iee/core/performance_samples.h" #include "iee/features/tile_render.h" +#include "iee/game/are_animations.h" #include "iee/game/area_texture.h" +#include "iee/game/object_statics.h" #include "iee/game/build_manifest.h" #include "iee/game/dds_texture.h" #include "iee/game/eeex_doc_layouts_x64.h" @@ -1204,17 +1206,15 @@ void test_scale_selection_precedence() { "Fallback should prefer deterministic table provenance over heuristics"); } - auto heuristicInfo = make_tile_info(0x80, 20000, 4096, 4096); - heuristicInfo.header = nullptr; - heuristicInfo.tileCount = 1; - const auto heuristicDetection = iee::game::detect_scale(heuristicInfo, 20000, manifest); - expect_true(heuristicDetection.has_value(), "Heuristics should still exist as a final fallback"); - if (heuristicDetection) { - expect_eq(heuristicDetection->scaleFactor, 4, - "Heuristic fallback should still detect upscaled tiles"); - expect_true(heuristicDetection->source == iee::game::ScaleDetectionSource::Heuristic, - "Final fallback should report heuristic provenance"); - } + // Large raw UVs and high texture ids are not scale signals: with no header + // and an unresolvable table, detection must fail closed (the render path + // then samples and delegates the tileset as standard 1x). + auto garbageInfo = make_tile_info(0x80, 20000, 4096, 4096); + garbageInfo.header = nullptr; + garbageInfo.tileCount = 1; + const auto garbageDetection = iee::game::detect_scale(garbageInfo, 20000, manifest); + expect_true(!garbageDetection.has_value(), + "Garbage UV/texture-id input must not produce a scale detection"); bool linearFlag = true; auto linearInfo = make_tile_info(iee::game::TisTileDimensions::Upscaled4x, 12000, @@ -1296,7 +1296,9 @@ void test_area_liquid_texture_packing_rejects_mismatch() { iee::game::WedAreaInfo wed{}; wed.baseWidth = 3; wed.baseHeight = 1; - wed.baseOverlayFlags = {0x00}; // wrong size + // assign() instead of a 1-element initializer list: GCC 13 -O2 emits a + // false-positive -Warray-bounds on the list's backing-array copy. + wed.baseOverlayFlags.assign(1, 0x00); // wrong size expect_true(!iee::game::pack_area_liquid_texture(wed).has_value(), "flag/dimension mismatch -> nullopt"); iee::game::WedAreaInfo empty{}; @@ -1330,9 +1332,13 @@ void test_fpseam_override_asset_contract() { // Our feed contract. for (const std::string_view name : {"uIeeEnabled", "uIeeTime", "uIeeScroll", "uIeeZoom", "uIeeViewport", "uIeeWorldSizeInv", - "uIeeWaterTint", "uIeeAreaMask", "uIeeNormalMap", "uIeeDudvMap", "uIeeFoamMap"}) { + "uIeeWaterTint", "uIeePointCount", "uIeePoints", "uIeeAreaMask", "uIeeNormalMap", + "uIeeDudvMap", "uIeeFoamMap"}) { expect_true(source.find(name) != std::string::npos, "fpSEAM override declares feed uniform"); } + // The uniform-array capacity in the shader must match the bridge/packing cap. + expect_true(source.find("uIeePoints[32]") != std::string::npos, + "fpSEAM point array capacity matches kMaxAreaEffectPoints"); expect_true(source.find("#version") == std::string::npos, "no #version line (engine sources are ARB-era GLSL)"); expect_true( @@ -1350,6 +1356,336 @@ void test_fpseam_override_asset_contract() { "confirmed interior water should skip the shoreline filter"); } +void test_classify_area_animation() { + using iee::game::AreaAnimationKind; + using iee::game::classify_area_animation; + + expect_true(classify_area_animation("FLAMBIG", "") == AreaAnimationKind::Fire, + "FLAM* resrefs should classify as fire"); + expect_true(classify_area_animation("torch01", "") == AreaAnimationKind::Fire, + "Resref classification should be case-insensitive"); + expect_true(classify_area_animation("ZZANIM", "Village fireplace") == AreaAnimationKind::Fire, + "Authored names should classify when the resref does not"); + expect_true(classify_area_animation("FPIT1S", "FPIT1S") == AreaAnimationKind::Fire, + "Fire pits (BG2EE AR0406) should classify as fire"); + expect_true(classify_area_animation("FLMSW", "FLMSW") == AreaAnimationKind::Fire, + "Bare FLM* flame BAMs (BG1EE) should classify as fire"); + expect_true(classify_area_animation("FLMS", "Candle03") == AreaAnimationKind::Light, + "Candle-named flames (BG1EE FLMS family) are dim lights, not fires"); + expect_true(classify_area_animation("FLMM", "Sconce01") == AreaAnimationKind::Fire, + "Sconces are wall flames"); + expect_true(classify_area_animation("AR900WN1", "AR900WN1") == AreaAnimationKind::None, + "ARW[DN]* overlays are night/day shadow scenery (verified from frames)"); + expect_true(classify_area_animation("AR900WD1", "AR900WD1") == AreaAnimationKind::None, + "Day shadow overlays stay unclassified scenery"); + expect_true(classify_area_animation("FIM1YLN1", "FIM1YLN1") == AreaAnimationKind::Fire, + "FIM* yellow flames (verified from frames) classify as fire"); + expect_true(classify_area_animation("YSFLBLU2", "YSFLBLU2") == AreaAnimationKind::Fire, + "YSFL* blue flames (verified from frames) classify as fire"); + expect_true(classify_area_animation("AM003XA", "AM003XA") == AreaAnimationKind::Fire, + "The hearth overlay is an exact-resref fire"); + expect_true(classify_area_animation("AM5508C", "AM5508C") == AreaAnimationKind::Light, + "The glow orb overlay is an exact-resref light"); + expect_true(classify_area_animation("AM6004A", "AM6004A") == AreaAnimationKind::Smoke, + "The dark plume overlay is an exact-resref smoke"); + expect_true(classify_area_animation("AM0604A", "AM0604A") == AreaAnimationKind::Fountain, + "The tiered fountain overlay is an exact-resref fountain"); + expect_true(classify_area_animation("AM0202FL", "AM0202FL") == AreaAnimationKind::Light, + "The star glint overlay is a light, not a flame, despite the FL suffix"); + expect_true(classify_area_animation("SPLASH", "SPLASH") == AreaAnimationKind::Water, + "Splashes classify as water effects"); + expect_true(classify_area_animation("DS6000W3", "Waterfall") == AreaAnimationKind::Water, + "Waterfall names classify as water effects"); + expect_true(classify_area_animation("BD0130LL", "Lava_Left") == AreaAnimationKind::Lava, + "Lava names classify as lava"); + expect_true(classify_area_animation("FISH3S", "Fish") == AreaAnimationKind::Wildlife, + "Fish classify as wildlife"); + expect_true(classify_area_animation("FLIESS", "FLIESS") == AreaAnimationKind::Wildlife, + "Fly swarms classify as wildlife"); + expect_true(classify_area_animation("BUTRFLY", "BUTRFLY3") == AreaAnimationKind::Wildlife, + "Butterflies classify as wildlife"); + expect_true(classify_area_animation("BD5100M1", "Mist_BD5100M1") == AreaAnimationKind::Smoke, + "Authored mist folds into the smoke kind"); + expect_true(classify_area_animation("AMSTEAM1", "AMB_Pipe1A") == AreaAnimationKind::Smoke, + "Steam pipes (BG2EE AR3017) fold into the smoke kind"); + expect_true(classify_area_animation("BUBBLES2", "BUBBLES2") == AreaAnimationKind::Water, + "Bubbles (BG2EE sewers) classify as water effects"); + expect_true(classify_area_animation("AMOH7300", "Tank_Bubbles") == AreaAnimationKind::Water, + "Bubble-named overlays classify as water effects"); + expect_true(classify_area_animation("SMOKE2", "") == AreaAnimationKind::Smoke, + "SMOK* resrefs should classify as smoke"); + expect_true(classify_area_animation("ZZANIM", "chimney smoke") == AreaAnimationKind::Smoke, + "Chimney names should classify as smoke"); + expect_true(classify_area_animation("FOUNT1", "") == AreaAnimationKind::Fountain, + "FOUNT* resrefs should classify as fountain"); + expect_true(classify_area_animation("GLOW01", "") == AreaAnimationKind::Light, + "GLOW* resrefs should classify as light"); + expect_true(classify_area_animation("ZZANIM", "window light") == AreaAnimationKind::Light, + "Light names should classify as light"); + expect_true(classify_area_animation("ZZANIM", "lightning strike") == AreaAnimationKind::None, + "Lightning is weather, not an authored light source"); + expect_true(classify_area_animation("ZZANIM", "mystery") == AreaAnimationKind::None, + "Unknown entries must stay unclassified"); + expect_true(classify_area_animation("", "") == AreaAnimationKind::None, + "Empty input classifies as none"); +} + +void test_parse_are_animations() { + using namespace iee::game; + + ARE_Header_st header{}; + header.nFileType = 0x41455241; // "AREA" + header.nFileVersion = 0x302E3156; // "V1.0" + header.nAnimations = 2; + header.nAnimationsOffset = sizeof(ARE_Header_st); + + ARE_Animation_st fire{}; + const char fireName[] = "Fireplace big"; + std::memcpy(fire.szName.data(), fireName, sizeof(fireName) - 1); + fire.nX = 320; + fire.nY = 240; + fire.nHeight = 5; + fire.rrAnimation = {'F', 'L', 'A', 'M', 'B', 'I', 'G', 0}; + fire.nFlags = kAreAnimationFlagIsShown; + fire.nSchedule = 0x00FFFFFF; + + ARE_Animation_st unknown{}; + const char unknownName[] = "mystery"; + std::memcpy(unknown.szName.data(), unknownName, sizeof(unknownName) - 1); + unknown.rrAnimation = {'Z', 'Z', 'X', 'Y', 0, 0, 0, 0}; + unknown.nFlags = kAreAnimationFlagNotLightSource; + + std::vector bytes; + write_bytes(bytes, 0, &header, sizeof(header)); + write_bytes(bytes, sizeof(ARE_Header_st), &fire, sizeof(fire)); + write_bytes(bytes, sizeof(ARE_Header_st) + sizeof(ARE_Animation_st), &unknown, sizeof(unknown)); + + AreaAnimationsInfo info{}; + expect_true(parse_are_animations(bytes.data(), bytes.size(), info), + "A valid ARE V1.0 animation section should parse"); + expect_eq(info.animations.size(), std::size_t{2}, "Both animation records should be read"); + expect_true(info.animations[0].kind == AreaAnimationKind::Fire, + "The FLAM* record should classify as fire"); + expect_true(info.animations[0].resrefView() == "FLAMBIG", "Animation resref should round-trip"); + expect_true(info.animations[0].nameView() == "Fireplace big", + "Animation name should round-trip NUL-terminated"); + expect_eq(info.animations[0].x, std::uint16_t{320}, "Animation X coordinate should round-trip"); + expect_eq(info.animations[0].y, std::uint16_t{240}, "Animation Y coordinate should round-trip"); + expect_true(info.animations[0].isShown(), "Flag bit 0 should report as shown"); + expect_true(info.animations[0].isLightSource(), + "An animation without the not-light-source bit is a light source"); + expect_true(!info.animations[1].isShown(), "Missing flag bit 0 should report as not shown"); + expect_true(!info.animations[1].isLightSource(), + "The not-light-source bit should suppress light-source status"); + expect_eq(info.count_of(AreaAnimationKind::Fire), std::size_t{1}, + "count_of should tally classified kinds"); + expect_eq(info.count_of(AreaAnimationKind::None), std::size_t{1}, + "count_of should tally unclassified records"); + + // Zero animations is a valid area. + auto emptyHeader = header; + emptyHeader.nAnimations = 0; + emptyHeader.nAnimationsOffset = 0; + std::vector emptyBytes; + write_bytes(emptyBytes, 0, &emptyHeader, sizeof(emptyHeader)); + AreaAnimationsInfo emptyInfo{}; + expect_true(parse_are_animations(emptyBytes.data(), emptyBytes.size(), emptyInfo), + "An ARE without animations should parse"); + expect_true(emptyInfo.animations.empty(), "An ARE without animations should yield no records"); + + // Unsupported version (IWD2 V9.1 shifts the section offsets). + auto v91 = header; + v91.nFileVersion = 0x312E3956; // "V9.1" + std::vector v91Bytes(bytes); + write_bytes(v91Bytes, 0, &v91, sizeof(v91)); + AreaAnimationsInfo v91Info{}; + expect_true(!parse_are_animations(v91Bytes.data(), v91Bytes.size(), v91Info), + "Non-V1.0 ARE versions must fail closed"); + + // Truncated section: count says two records but only one fits. + std::vector truncated(bytes.begin(), + bytes.end() - static_cast(sizeof(fire))); + AreaAnimationsInfo truncatedInfo{}; + expect_true(!parse_are_animations(truncated.data(), truncated.size(), truncatedInfo), + "A truncated animation section must fail closed"); + expect_true(truncatedInfo.animations.empty(), "A failed parse must leave the output empty"); + + // Malicious count. + auto hugeHeader = header; + hugeHeader.nAnimations = 1'000'000; + std::vector hugeBytes(bytes); + write_bytes(hugeBytes, 0, &hugeHeader, sizeof(hugeHeader)); + AreaAnimationsInfo hugeInfo{}; + expect_true(!parse_are_animations(hugeBytes.data(), hugeBytes.size(), hugeInfo), + "An implausible animation count must fail closed"); + + AreaAnimationsInfo shortInfo{}; + expect_true(!parse_are_animations(bytes.data(), sizeof(ARE_Header_st) - 1, shortInfo), + "A buffer smaller than the ARE header must fail closed"); +} + +void test_decode_object_array_globals() { + using namespace iee::game; + + // Synthetic CGameObjectArray::GetShare body: manifest pattern prologue, + // then the RIP-relative max-index compare, the (ignored) next-id compare, + // and the entry-table lea, each pointing at slots inside the same buffer. + std::array code{}; + const auto put = [&](std::size_t offset, std::initializer_list bytes) { + std::size_t index = offset; + for (const auto value : bytes) code[index++] = static_cast(value); + }; + const auto putRip = [&](std::size_t offset, std::initializer_list opcode, + std::size_t target) { + put(offset, opcode); + const auto displacement = static_cast(static_cast(target) - + static_cast(offset + 7)); + std::memcpy(code.data() + offset + 3, &displacement, sizeof(displacement)); + }; + put(0, {0x48, 0xC7, 0x02, 0x00, 0x00, 0x00, 0x00, 0x83, 0xF9, 0xFF}); + putRip(10, {0x66, 0x39, 0x05}, 0x80); // cmp [rip+d], ax -> m_maxArrayIndex + putRip(17, {0x66, 0x39, 0x0D}, 0x84); // cmp [rip+d], cx -> ignored + putRip(24, {0x4C, 0x8D, 0x05}, 0x90); // lea r8, [rip+d] -> entry table + + ObjectArrayGlobals globals{}; + expect_true(decode_object_array_globals(code.data(), 0x60, globals), + "GetShare RIP operands should decode from a well-formed body"); + expect_true(reinterpret_cast(globals.maxArrayIndex) == code.data() + 0x80, + "The max-index compare operand should decode to its RIP target"); + expect_true(reinterpret_cast(globals.entries) == code.data() + 0x90, + "The entry-table lea operand should decode to its RIP target"); + + ObjectArrayGlobals tooSmall{}; + expect_true(!decode_object_array_globals(code.data(), 0x10, tooSmall), + "A window without both instructions must fail closed"); + + putRip(40, {0x66, 0x39, 0x05}, 0x88); // duplicate max-index compare + ObjectArrayGlobals ambiguous{}; + expect_true(!decode_object_array_globals(code.data(), 0x60, ambiguous), + "Ambiguous instruction matches must fail closed"); +} + +void test_collect_area_static_animations() { + using namespace iee::game; + + CGameArea areaA{}; + CGameArea areaB{}; + std::array statics{}; + + statics[0].baseclass_0.m_objectType = kGameObjectTypeStatic; + statics[0].baseclass_0.m_pArea = &areaA; + statics[0].m_header.rrAnimation = {'F', 'L', 'A', 'M', 'B', 'I', 'G', 0}; + const char fireName[] = "FLAMBIG"; + std::memcpy(statics[0].m_header.szName.data(), fireName, sizeof(fireName) - 1); + statics[0].m_header.nX = 320; + statics[0].m_header.nY = 240; + statics[0].m_header.nFlags = kAreAnimationFlagIsShown; + + // Same type, different area: filtered. + statics[1].baseclass_0.m_objectType = kGameObjectTypeStatic; + statics[1].baseclass_0.m_pArea = &areaB; + statics[1].m_header.rrAnimation = {'S', 'M', 'O', 'K', 'E', '2', 0, 0}; + + // Same area, different object type: filtered. + statics[2].baseclass_0.m_objectType = 0x31; + statics[2].baseclass_0.m_pArea = &areaA; + + std::array entries{}; + entries[1].m_objectPtr = &statics[0].baseclass_0; + entries[3].m_objectPtr = &statics[1].baseclass_0; + entries[4].m_objectPtr = &statics[2].baseclass_0; + + std::int16_t maxIndex = 5; + const ObjectArrayGlobals globals{entries.data(), &maxIndex}; + + AreaAnimationsInfo out{}; + expect_true(collect_area_static_animations(globals, &areaA, out), + "A readable object array should collect"); + expect_eq(out.animations.size(), std::size_t{1}, + "Only statics owned by the requested area should be collected"); + expect_true(!out.animations.empty() && out.animations[0].kind == AreaAnimationKind::Fire, + "Collected records should classify like the disk parser"); + expect_true(!out.animations.empty() && out.animations[0].x == 320 && + out.animations[0].isShown(), + "Collected records should carry the live header fields"); + + AreaAnimationsInfo invalidOut{}; + expect_true(!collect_area_static_animations(ObjectArrayGlobals{}, &areaA, invalidOut), + "Unresolved globals must fail closed"); + std::int16_t negativeIndex = -1; + const ObjectArrayGlobals negative{entries.data(), &negativeIndex}; + expect_true(!collect_area_static_animations(negative, &areaA, invalidOut), + "A negative max index must fail closed"); +} + +void test_build_area_effect_points() { + using namespace iee::game; + + AreaAnimationsInfo info{}; + const auto add = [&](AreaAnimationKind kind, std::uint16_t x, bool shown) { + AreaAnimationInfo animation{}; + animation.kind = kind; + animation.x = x; + animation.y = 100; + animation.flags = shown ? kAreAnimationFlagIsShown : 0; + info.animations.push_back(animation); + }; + + add(AreaAnimationKind::Smoke, 10, true); + add(AreaAnimationKind::Fire, 20, true); + add(AreaAnimationKind::Fire, 30, false); // hidden: excluded + add(AreaAnimationKind::Light, 40, true); + add(AreaAnimationKind::Wildlife, 50, true); // no effect kind: excluded + add(AreaAnimationKind::Water, 60, true); // handled by the water path: excluded + + const auto points = build_area_effect_points(info); + expect_eq(points.size(), std::size_t{3}, "Only shown fire/smoke/light become points"); + expect_true(!points.empty() && points[0].kind == 1.0f && points[0].x == 20.0f, + "Fire points are packed first"); + expect_true(points.size() >= 2 && points[1].kind == 4.0f && points[1].strength < 1.0f, + "Light points follow with a reduced strength"); + expect_true(points.size() >= 3 && points[2].kind == 2.0f, + "Smoke points are packed last"); + + AreaAnimationsInfo overflow{}; + for (int i = 0; i < 80; ++i) { + AreaAnimationInfo animation{}; + animation.kind = i < 40 ? AreaAnimationKind::Smoke : AreaAnimationKind::Fire; + animation.flags = kAreAnimationFlagIsShown; + overflow.animations.push_back(animation); + } + const auto capped = build_area_effect_points(overflow); + expect_eq(capped.size(), kMaxAreaEffectPoints, "The point set is capped at the uniform size"); + expect_true(!capped.empty() && capped[0].kind == 1.0f, + "Under capacity pressure, fire wins over smoke"); +} + +void test_config_detection_section() { + const auto tempPath = + std::filesystem::current_path() / "InfinityEngine-Enhancer-detection-test.ini"; + { + std::ofstream out(tempPath, std::ios::trunc); + out << "[Detection]\n"; + out << "AreaAnimationScan = false\n"; + } + + iee::core::EngineConfig cfg{}; + expect_true(cfg.enableAreaAnimationScan, "The area animation scan should default to enabled"); + expect_true(iee::core::ConfigManager::load(tempPath, cfg), + "ConfigManager::load should parse the detection section"); + expect_true(!cfg.enableAreaAnimationScan, "AreaAnimationScan=false should disable the scan"); + + expect_true(iee::core::ConfigManager::save(tempPath, cfg), + "ConfigManager::save should persist the detection section"); + iee::core::EngineConfig reloaded{}; + expect_true(iee::core::ConfigManager::load(tempPath, reloaded), + "The saved detection section should reload"); + expect_true(!reloaded.enableAreaAnimationScan, "AreaAnimationScan should round-trip"); + + std::error_code error; + std::filesystem::remove(tempPath, error); +} + int main() { test_parse_ida_pattern(); test_unique_pattern_matching(); @@ -1386,6 +1722,12 @@ int main() { test_area_liquid_texture_packing(); test_area_liquid_texture_packing_rejects_mismatch(); test_fpseam_override_asset_contract(); + test_classify_area_animation(); + test_parse_are_animations(); + test_decode_object_array_globals(); + test_collect_area_static_animations(); + test_build_area_effect_points(); + test_config_detection_section(); if (g_failures != 0) { std::cerr << g_failures << " test(s) failed\n"; diff --git a/tools/InfinityEngine-Enhancer.sample.ini b/tools/InfinityEngine-Enhancer.sample.ini index 1c348f6..a1d9005 100644 --- a/tools/InfinityEngine-Enhancer.sample.ini +++ b/tools/InfinityEngine-Enhancer.sample.ini @@ -11,6 +11,11 @@ EnableAnisotropicFiltering = false MaxAnisotropy = 8.0 LODBias = -0.25 +[Detection] +; Collect the current area's authored ambient animations from engine memory +; and classify fire/smoke/fountain/light point sources. +AreaAnimationScan = true + [Shaders] ; Diagnostic only: archive original shaders to iee-shader-dumps/ next to the DLL. DumpEngineShaders = false @@ -18,3 +23,6 @@ DumpEngineShaders = false EnableDebugHotkeys = false ; Start the bundled water shader enabled when it is installed in the game's override directory. EnableWaterEffect = true +; Fire glow/heat shimmer, chimney-smoke haze, and candle glows placed from the +; area's classified ambient animations (requires the fpSEAM override). +EnablePointEffects = true From 4609c204ca472920408e2899818fac6b6761c374 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Fri, 17 Jul 2026 23:53:31 +0200 Subject: [PATCH 02/57] fpSEAM: flatten point-effect loop into main; add ALIGN-mode point markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The struct-returning helper was the remaining suspect for the effects not rendering despite a verified uniform feed (locations resolved, count fed) — old GLSL frontends miscompile struct returns. The loop now uses flat locals inside main. ALIGN mode (F10 x2) draws a dot and 40px ring at every fed point (orange fire, grey smoke, yellow light), so point placement is verifiable in-game independently of the effect math. --- assets/override/fpSEAM.glsl | 199 +++++++++++++++++++----------------- 1 file changed, 103 insertions(+), 96 deletions(-) diff --git a/assets/override/fpSEAM.glsl b/assets/override/fpSEAM.glsl index 4e8a3ac..5aef0ca 100644 --- a/assets/override/fpSEAM.glsl +++ b/assets/override/fpSEAM.glsl @@ -209,89 +209,6 @@ bool ieeIsInteriorWater(vec2 worldPos, float centerCoverage) return true; } -// --- IEE ambient point effects (fire glow / heat shimmer / smoke haze / -// --- candle glow), placed from the area's classified ARE animations. --- - -// Accumulated per-fragment point-effect terms. -struct IeePointFx -{ - vec3 glow; // linear warm light accumulated from fire/light points - float shimmer; // heat-distortion weight 0..1 - float haze; // smoke haze density 0..1 - vec3 hazeTint; // veil color for the haze mix -}; - -IeePointFx ieePointEffects(vec2 worldPos, float t) -{ - IeePointFx fx; - fx.glow = vec3(0.0); - fx.shimmer = 0.0; - fx.haze = 0.0; - fx.hazeTint = vec3(0.42, 0.42, 0.46); - - for (int i = 0; i < 32; ++i) - { - if (float(i) >= uIeePointCount) { break; } - vec4 p = uIeePoints[i]; - vec2 offs = worldPos - p.xy; - float kind = p.z; - float strength = p.w; - - if (kind < 1.5) // fire: warm flickering glow + heat shimmer above - { - float radius = 95.0 * strength; - float d = length(offs); - if (d < radius) - { - float fall = 1.0 - d / radius; - fall *= fall; - // Two-rate flicker, unsynchronized between points. - float fi = float(i) * 7.31; - float flick = 0.80 + 0.14 * sin(t * 9.7 + fi) + 0.06 * sin(t * 23.0 + fi * 1.7); - fx.glow += vec3(1.0, 0.44, 0.14) * (fall * 0.65 * strength * flick); - // Shimmer only above the flame (world y grows downward). - float above = clamp(-offs.y / (radius * 0.9), 0.0, 1.0); - fx.shimmer += fall * above * strength; - } - } - else if (kind < 2.5) // smoke: drifting haze column rising from the point - { - float height = 130.0 * strength; - float rise = -offs.y; // px above the source - if (rise > -10.0 && rise < height) - { - float sway = sin(t * 0.8 + rise * 0.045 + float(i)) * (6.0 + rise * 0.16); - float lateral = abs(offs.x - sway); - float widthPx = 14.0 + rise * 0.30; - if (lateral < widthPx * 2.0) - { - float across = exp(-(lateral * lateral) / (widthPx * widthPx)); - float along = smoothstep(-10.0, 18.0, rise) * (1.0 - rise / height); - float puff = ieeNoise(vec2(worldPos.x * 0.05, - (worldPos.y + t * 34.0) * 0.05) + vec2(float(i))); - fx.haze += across * along * (0.35 + 0.65 * puff) * 0.55 * strength; - } - } - } - else if (kind > 3.5 && kind < 4.5) // light: steady soft glow - { - float radius = 70.0 * strength; - float d = length(offs); - if (d < radius) - { - float fall = 1.0 - d / radius; - fall *= fall; - float breathe = 0.92 + 0.08 * sin(t * 2.1 + float(i) * 3.3); - fx.glow += vec3(1.0, 0.72, 0.38) * (fall * 0.35 * strength * breathe); - } - } - } - - fx.shimmer = clamp(fx.shimmer, 0.0, 1.0); - fx.haze = clamp(fx.haze, 0.0, 0.85); - return fx; -} - // Smoothed shore proximity: 0 deep inside water, ~1 at the land boundary. float ieeShoreFactor(vec2 worldPos, float centerCoverage) { @@ -351,31 +268,121 @@ void main() vec4 base = art * vColor; gl_FragColor = vec4(base.rgb, base.a); } + + // Point-placement markers: a filled dot plus a 40px ring at every fed + // effect point — orange = fire, grey = smoke, yellow = light. Absent + // rings in ALIGN mode mean the point feed (not the effect math) is + // broken. + if (uIeePointCount > 0.5 && vColor.a > 0.9) + { + float ring = 0.0; + vec3 ringColor = vec3(0.0); + for (int i = 0; i < 32; ++i) + { + if (float(i) >= uIeePointCount) { break; } + vec4 p = uIeePoints[i]; + float d = length(worldPos - p.xy); + float m = smoothstep(4.0, 1.5, abs(d - 40.0)) + smoothstep(6.0, 2.0, d); + if (m > ring) + { + ring = m; + if (p.z < 1.5) { ringColor = vec3(1.0, 0.45, 0.05); } + else if (p.z < 2.5) { ringColor = vec3(0.75, 0.75, 0.85); } + else { ringColor = vec3(1.0, 0.95, 0.20); } + } + } + if (ring > 0.02) + { + gl_FragColor = vec4(mix(gl_FragColor.rgb, ringColor, clamp(ring, 0.0, 1.0)), + gl_FragColor.a); + } + } return; } // Ambient point effects: computed before sampling so heat shimmer can // ride the one seamSample call as a texture-coordinate offset. Base pass - // only; the WATER_ALPHA secondary pass and fades stay vanilla. - IeePointFx fx; - fx.glow = vec3(0.0); - fx.shimmer = 0.0; - fx.haze = 0.0; - fx.hazeTint = vec3(0.42, 0.42, 0.46); - if (uIeeEnabled > 0.5 && uIeeEnabled < 1.5 && vColor.a > 0.9 && uIeePointCount > 0.5) + // only; the WATER_ALPHA secondary pass and fades stay vanilla. Kept as + // flat locals (no struct/function) for maximum GLSL-frontend + // compatibility. + vec3 fxGlow = vec3(0.0); + float fxShimmer = 0.0; + float fxHaze = 0.0; + if (uIeeEnabled > 0.5 && vColor.a > 0.9 && uIeePointCount > 0.5) { - fx = ieePointEffects(worldPos, uIeeTime); + float fxT = uIeeTime; + for (int i = 0; i < 32; ++i) + { + if (float(i) >= uIeePointCount) { break; } + vec4 p = uIeePoints[i]; + vec2 offs = worldPos - p.xy; + float kind = p.z; + float strength = p.w; + + if (kind < 1.5) // fire: warm flickering glow + heat shimmer above + { + float radius = 95.0 * strength; + float d = length(offs); + if (d < radius) + { + float fall = 1.0 - d / radius; + fall *= fall; + // Two-rate flicker, unsynchronized between points. + float fi = float(i) * 7.31; + float flick = 0.80 + 0.14 * sin(fxT * 9.7 + fi) + + 0.06 * sin(fxT * 23.0 + fi * 1.7); + fxGlow += vec3(1.0, 0.44, 0.14) * (fall * 0.65 * strength * flick); + // Shimmer only above the flame (world y grows downward). + float above = clamp(-offs.y / (radius * 0.9), 0.0, 1.0); + fxShimmer += fall * above * strength; + } + } + else if (kind < 2.5) // smoke: drifting haze column rising from the point + { + float height = 130.0 * strength; + float rise = -offs.y; // px above the source + if (rise > -10.0 && rise < height) + { + float sway = sin(fxT * 0.8 + rise * 0.045 + float(i)) * (6.0 + rise * 0.16); + float lateral = abs(offs.x - sway); + float widthPx = 14.0 + rise * 0.30; + if (lateral < widthPx * 2.0) + { + float across = exp(-(lateral * lateral) / (widthPx * widthPx)); + float along = smoothstep(-10.0, 18.0, rise) * (1.0 - rise / height); + float puff = ieeNoise(vec2(worldPos.x * 0.05, + (worldPos.y + fxT * 34.0) * 0.05) + + vec2(float(i))); + fxHaze += across * along * (0.35 + 0.65 * puff) * 0.55 * strength; + } + } + } + else if (kind > 3.5 && kind < 4.5) // light: steady soft glow + { + float radius = 70.0 * strength; + float d = length(offs); + if (d < radius) + { + float fall = 1.0 - d / radius; + fall *= fall; + float breathe = 0.92 + 0.08 * sin(fxT * 2.1 + float(i) * 3.3); + fxGlow += vec3(1.0, 0.72, 0.38) * (fall * 0.35 * strength * breathe); + } + } + } + fxShimmer = clamp(fxShimmer, 0.0, 1.0); + fxHaze = clamp(fxHaze, 0.0, 0.85); } vec2 sampleTc = vTc; - if (fx.shimmer > 0.01) + if (fxShimmer > 0.01) { // Small wavering refraction above flames; uTcScale converts the // world-pixel amplitude into atlas units, so the offset stays within // the seam handling's tolerance. vec2 wobble = vec2(sin(uIeeTime * 13.0 + worldPos.y * 0.35), cos(uIeeTime * 11.0 + worldPos.x * 0.30)); - sampleTc += wobble * fx.shimmer * 1.4 * uTcScale; + sampleTc += wobble * fxShimmer * 1.4 * uTcScale; } vec4 texColor = seamSample(sampleTc); @@ -479,13 +486,13 @@ void main() // Firelight, candle glow, and smoke haze over the (possibly water-graded) // background art, in linear light so the warm lift does not band. - if (fx.haze > 0.004 || fx.glow.r + fx.glow.g + fx.glow.b > 0.004) + if (fxHaze > 0.004 || fxGlow.r + fxGlow.g + fxGlow.b > 0.004) { vec3 lin = ieeSrgbToLinear(texColor.rgb); // Multiplicative warm lift preserves the art's detail; the small // additive term lets glow read on dark night maps. - lin = lin * (vec3(1.0) + fx.glow * 1.6) + fx.glow * 0.045; - lin = mix(lin, fx.hazeTint * (0.35 + 0.40 * lin), fx.haze); + lin = lin * (vec3(1.0) + fxGlow * 1.6) + fxGlow * 0.045; + lin = mix(lin, vec3(0.42, 0.42, 0.46) * (0.35 + 0.40 * lin), fxHaze); texColor.rgb = ieeLinearToSrgb(lin); } From d1aab40b32b6fb01b94e2399818334748cf6a20f Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 18 Jul 2026 00:00:44 +0200 Subject: [PATCH 03/57] Point effects: night-adaptive intensity boost; immediate 1x latch for metadata-free tilesets In-game ALIGN markers confirmed exact point placement, but the first-pass constants were too timid, especially at night where multiplicative glow cannot lift dark pixels. Fire glow gains a night-adaptive additive cast-light term, a wider radius (140px) with a hot core, and an up-biased center onto the visible flame; smoke columns are taller, denser, and read as moonlit haze over dark roofs; candle glow strengthened. Also skips the 10-draw sampling window when a tileset is resident and readable but simply has no deterministic scale metadata (classic paletted tilesets): more draws cannot change that verdict, so it latches standard 1x immediately. Sampling remains for resources still streaming in. --- assets/override/fpSEAM.glsl | 48 +++++++++++++++++++------------- src/iee/features/tile_render.cpp | 12 ++++++++ 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/assets/override/fpSEAM.glsl b/assets/override/fpSEAM.glsl index 5aef0ca..87165d1 100644 --- a/assets/override/fpSEAM.glsl +++ b/assets/override/fpSEAM.glsl @@ -321,17 +321,21 @@ void main() if (kind < 1.5) // fire: warm flickering glow + heat shimmer above { - float radius = 95.0 * strength; + // The ARE anchor sits below the visible flame (BAM frames draw + // upward from it); bias the glow center up onto the fire. + offs.y += 24.0 * strength; + float radius = 140.0 * strength; float d = length(offs); if (d < radius) { float fall = 1.0 - d / radius; - fall *= fall; + // Fuller mid-falloff with a hot core. + fall = fall * fall * (0.55 + 0.45 * fall) + 0.6 * smoothstep(0.75, 1.0, fall); // Two-rate flicker, unsynchronized between points. float fi = float(i) * 7.31; - float flick = 0.80 + 0.14 * sin(fxT * 9.7 + fi) - + 0.06 * sin(fxT * 23.0 + fi * 1.7); - fxGlow += vec3(1.0, 0.44, 0.14) * (fall * 0.65 * strength * flick); + float flick = 0.78 + 0.16 * sin(fxT * 9.7 + fi) + + 0.08 * sin(fxT * 23.0 + fi * 1.7); + fxGlow += vec3(1.0, 0.45, 0.15) * (fall * 0.85 * strength * flick); // Shimmer only above the flame (world y grows downward). float above = clamp(-offs.y / (radius * 0.9), 0.0, 1.0); fxShimmer += fall * above * strength; @@ -339,34 +343,34 @@ void main() } else if (kind < 2.5) // smoke: drifting haze column rising from the point { - float height = 130.0 * strength; + float height = 200.0 * strength; float rise = -offs.y; // px above the source - if (rise > -10.0 && rise < height) + if (rise > -14.0 && rise < height) { - float sway = sin(fxT * 0.8 + rise * 0.045 + float(i)) * (6.0 + rise * 0.16); + float sway = sin(fxT * 0.9 + rise * 0.04 + float(i)) * (7.0 + rise * 0.18); float lateral = abs(offs.x - sway); - float widthPx = 14.0 + rise * 0.30; + float widthPx = 18.0 + rise * 0.35; if (lateral < widthPx * 2.0) { float across = exp(-(lateral * lateral) / (widthPx * widthPx)); - float along = smoothstep(-10.0, 18.0, rise) * (1.0 - rise / height); - float puff = ieeNoise(vec2(worldPos.x * 0.05, - (worldPos.y + fxT * 34.0) * 0.05) + float along = smoothstep(-14.0, 16.0, rise) * (1.0 - rise / height); + float puff = ieeNoise(vec2(worldPos.x * 0.045, + (worldPos.y + fxT * 40.0) * 0.045) + vec2(float(i))); - fxHaze += across * along * (0.35 + 0.65 * puff) * 0.55 * strength; + fxHaze += across * along * (0.40 + 0.60 * puff) * 0.80 * strength; } } } else if (kind > 3.5 && kind < 4.5) // light: steady soft glow { - float radius = 70.0 * strength; + float radius = 90.0 * strength; float d = length(offs); if (d < radius) { float fall = 1.0 - d / radius; fall *= fall; float breathe = 0.92 + 0.08 * sin(fxT * 2.1 + float(i) * 3.3); - fxGlow += vec3(1.0, 0.72, 0.38) * (fall * 0.35 * strength * breathe); + fxGlow += vec3(1.0, 0.74, 0.40) * (fall * 0.55 * strength * breathe); } } } @@ -489,10 +493,16 @@ void main() if (fxHaze > 0.004 || fxGlow.r + fxGlow.g + fxGlow.b > 0.004) { vec3 lin = ieeSrgbToLinear(texColor.rgb); - // Multiplicative warm lift preserves the art's detail; the small - // additive term lets glow read on dark night maps. - lin = lin * (vec3(1.0) + fxGlow * 1.6) + fxGlow * 0.045; - lin = mix(lin, vec3(0.42, 0.42, 0.46) * (0.35 + 0.40 * lin), fxHaze); + float sceneLuma = dot(lin, vec3(0.2126, 0.7152, 0.0722)); + // Multiplicative warm lift preserves the art's detail; the additive + // term is night-adaptive — dark scenes get real cast light, bright + // day scenes only a whisper. + float castLight = 0.05 + 0.50 * (1.0 - clamp(sceneLuma * 4.0, 0.0, 1.0)); + lin = lin * (vec3(1.0) + fxGlow * 1.5) + fxGlow * castLight; + // Smoke veil: reads as moonlit smoke over dark roofs, shadowy over + // bright ground. + vec3 hazeTarget = vec3(0.42, 0.42, 0.46) * (0.42 + 0.5 * sceneLuma) + vec3(0.02); + lin = mix(lin, hazeTarget, fxHaze); texColor.rgb = ieeLinearToSrgb(lin); } diff --git a/src/iee/features/tile_render.cpp b/src/iee/features/tile_render.cpp index 792fb6b..f258cd8 100644 --- a/src/iee/features/tile_render.cpp +++ b/src/iee/features/tile_render.cpp @@ -109,6 +109,18 @@ bool render_tile(AppContext& ctx, void* vidTile, int texId, void* unused, int x, state.lastTexId.store(-1, std::memory_order_relaxed); return false; } + } else if (tileInfo.table && tileInfo.tileCount > 0) { + // The resource is resident and readable yet exposes no deterministic + // scale metadata (classic paletted tilesets, door tiles): that cannot + // change with more draws, so latch standard immediately instead of + // burning the sampling window. Sampling below remains only for + // resources still streaming in. + tilesetState->scaleFactor = 1; + tilesetState->scaleDetected = true; + LOG_INFO("Tileset 0x{:X} has no deterministic scale metadata; delegated as standard", + reinterpret_cast(tileInfo.tileset)); + state.lastTexId.store(-1, std::memory_order_relaxed); + return false; } else if (tilesetState->detectionCount < game::UpscaleThresholds::DETECTION_SAMPLE_COUNT) { const int sampleCount = ++tilesetState->detectionCount; if (sampleCount == 1) { From 51d2709abb5c36e5b2a31c54b635bc6b76aa2a56 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 18 Jul 2026 03:15:41 +0200 Subject: [PATCH 04/57] Point effects v2: textured flame/smoke replacements + CGameStatic::Render suppression Radial-gradient halos read as artificial airbrush blobs (and painted the unexplored void). This pass makes fire and smoke real replacements, the same approach as the water pillar: - New tileable FBM asset iee_effects_noise.rgba (generated in-repo, IRGB blob, unit 6/uIeeNoiseMap) riding the existing texture registry with the same DDS-override path. - fpSEAM draws shaped flame bodies eroded by rising noise with a blackbody ramp (per-resref scale: sconces small, fire pits full), night-adaptive cast light that never paints the void, and noise-billowed smoke plumes that wander and dissipate with altitude. - CGameStatic::Render hook (prologue pattern offline-verified unique on both builds: 0x1F2570 / 0x1F27D0) skips the engine's authored fire/smoke BAM draws while the effect is active; WBM/PVRZ setpieces and all other kinds stay vanilla, and failure to resolve keeps engine draws. --- assets/game-textures/iee_effects_noise.rgba | Bin 0 -> 1048588 bytes assets/override/fpSEAM.glsl | 130 ++++++++++++++------ docs/are-animation-detection.md | 16 ++- src/iee/game/are_animations.cpp | 18 ++- src/iee/game/build_manifest.cpp | 15 ++- src/iee/game/build_manifest.h | 5 + src/iee/game/game_addrs.cpp | 18 +++ src/iee/game/game_addrs.h | 3 + src/iee/game/texture_units.h | 1 + src/iee/hooks.cpp | 47 +++++++ src/iee/shader_uniform_bridge.cpp | 8 +- src/iee/shader_uniform_bridge.h | 1 + src/iee/water_textures.cpp | 7 +- tests/iee_tests.cpp | 2 +- 14 files changed, 216 insertions(+), 55 deletions(-) create mode 100644 assets/game-textures/iee_effects_noise.rgba diff --git a/assets/game-textures/iee_effects_noise.rgba b/assets/game-textures/iee_effects_noise.rgba new file mode 100644 index 0000000000000000000000000000000000000000..ca245484facdd79c1aac6f8a001c13fd54dc3732 GIT binary patch literal 1048588 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%0Dv@o?zb7RYlegN~> z&@y)oZBtXbqHb>x7x`QC8l20T?@qOW2M@B*M>x_q}gTfW-8SQ&%%?tHb198d #2o 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

=MHsPbrN(qZ#ZQgZriY$sPQEFXljYcaL0f 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

(d&e7I1t85rf+OeE)f{Qw*97mt?9C;z~ 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 Z;0d2m3be1 zf16nQuSG7k!^oAo6TMPaW7qO*>_!%g-%5gsJFzEmC$uN;g|^haz>#_oc;Ww0250}^ z|0LX>$$bd3xtSoFpYbz=Is8B8#)|WmQ1O%FFMYCo+@A#$Wt)SAn71rCgqGtO>-0j>*hTR*s?Bw{d3WF|5SK5 zIEMQVxEI4c&h==Ub-TLBxL;jgz8g=MZ`Rh9FDB#Vv&o2YIvp~P*Za()^&abRqr*Db zY_qmETde6;lQrCKuv*)7R%xfsPVLs%;k_Ei3+($<4s}pv8-f0?!qy#CSh}M!OLtsk zYfthV&1rT;bDH95&*FUjc~odT4~rHxe;V$0W8EM5<$hrq?yujt1g%@ApmXaGbnfiJ z*1b(w0oezeF!5-^-Sak~|H&?N0sFHX?N?B_Kd(5IQq1DPVqW$ogxDu3m^l~wrym9 zDKfzDlcwN*vh`^t>YtPu{x`w@#>%J3Q2#XOn{&OcIoD}xbDg?`IzVow$6kP5j;AdD zp_djv^pfJcPEhIksZA^u_AW_ zGjc~VFfVC2eq&imUR!^pudM%M7hGEZPF-04g8%=5|NnBI+5X1y-5;p`FK3Q@tkO}C z1OId3fBx!0La?@<5{&lJg7$7!P~Oe)vpWS|V!OzVZec?EW{Dfx_=Xo;FY<%aydXTu zi6Uzmada&uiH?(!_$u5z!g_lYll6yD+1el?-{?o=JN>X?zZX^4fFuiQ2#H({dtgq|C0;%SMhr*--wX`#J~M734HrM2mj~c|AMjv|F^TaUk`NQ z|3*5isHD=eaxx{YC6ba>JR$DK65?TWK~ge|B&EY}N;U|k6~ka!IS!2ADv1n0 zo*@Gqiptck=$HSe!vFTaWlgZ~fBu*M7dZeKU>pA5;H8Nv{J#eOkKq447xN5O65swW zE&RXo%m3H|P$ZE7lE?ta0qO$$kC_19b$~wHzlt*gQ@DQz-aiDVtgz|=T(QDRN9Y+6CntkI00GcYpd|6pWfO(sUpNHPj~8>6W6 zFv{8}qoU0*tJ*TNt}8Pes3A1-qt=rXTRC`XLzbtNJ#-iVRRSR0UOIMOZTyg>_R#gt~&LK?KAN zWPm1Rm;A~Aq_j>G(z+d!OPmyH09IrG8X3Tb3}8nFuptA`r~_D$0qp1n@E`+(G@W1q z86czWh4RP%72PP@(2c__{Ulu1uZN0;jZoUK8A6u^*bK!?o5%ngAwS&jGOxq`>p=@3 z$@Kt5Zuo7~rq4xfdEJ&BkJGa2hW}mgzl);xos{jsX|)|X9rh!a-*N1UJ5F4A=c&8q zI&-&O=dQl{!nNwYbWJ?h&JDPK$9wBI^4)&B|H^-7y$ak}E`ztw)s6WSvwjZY{_XIk zb{e@Ql*;iruxBoNw znE>$9@PDc}=O&7uD^a*VSpH)B;eKD`H&xUDY9A~g{2!`g4&ZP9r<-@&T=SM!Y{CES z8)2<|BWiYT#GUSqcnJ58dspJM{-qfH7w!yB1;@i9oKrsFT#j~GH>=x>yVcF*`|46Z9TvLH&Ui<{xSJpB6@*Xrb@f zCUgPYvt4L?aSEvyw}^c8iiuah#PS-Dy554a&|63zgZty}aQ{2p|L#_#;r`r*Ls|T= zsY)L-&aTng><6une$Yk_{A&BK6C`}DN3WX-J#UI%SQInI0;~KRJFS@6Y4yx*)z9n} z%gjdkW;QbQVIwLZSPy^DrWR;^Sd9z1A6CN{-rmILY~$SA#)nNi`mk#{pbdH-cJ27X zq2K&)7N*&DQvwpyQu}E%iy-^PiNX@<}-xpB7i^)8cM^ zT0B_CdpciM?-*=@qc4m1^vmJ_?z1msfG?}_=*w!~`=V`|U$k}nMOy}6R;v4DA)8+m zQT-&$@V~kGX*4xH4aWATUf=%I=^DrYWn_TdT(5%v74W|t{+AWLGk|W90mR4vq7pJd z=~XMJfZB_W5C8L9PkLVc$-pf=8dfq7CQjlWoznLND}F~ZleZQ|>c+a9zP2vI{Y$wk z>yO-}^`G>G^`Gdu^&gDu_{Vc*|KUG%E=Nw>%+#@uojVGyln=xF#$im*JxB<9`$W`()koG`JI7euxT{J>U`@7pZ#y&DUP0^fR3=%40Ap-E00nPeo9wX_uem%;xs z_+M5X#pJzVR6ZU=<4 zw(pEzY>(^KY_2BR=x6uQe=zu<)Kl&z34&nYGd_IKR z;eNv!e80}XjARDxF~D;G%8a}Y-KcseBWJKIT)&(%Eug;18!e22fo2r(epwfQ`xA_s zrogCctBi)W#%Lk~wDbjLN1tPM3|VH^m}Pxq!qb#T4k#lBG}vuphuy**NiD;W(=?1$ zn)(I9m4lJ$Rm1N@2Jos|fv~z2j3Wc2HQi87 z+Ygm77r3EY4Yzb_@c$%SL>(Y)*a#<)0}{p!)B!f&|MigHG!41U(~zB*VEsQ0QsgvX zA=lym4WE*}CJETDSaa`&Q8C z+z8v98&R)&BO3Ou#lQV8nhwqd+k;b_Q9fEZ9UZXGS9h5gtJ}+0>|AeV)8QPQ9=-JNxZSupv%~Fn@?d7Cj(>UI zb~3jCnm(S>)+2n*;oM5@&(Zxlr_7sRGAGTWIbrV3%|vrU`=A-A^6r06HD~ zugM|I5M(iyu$V$A$ zoYY&$FZ?fu{}shtNzuxcl)Y?G*~t_X^>j{NN@ZomWJXp=q@|5SO45m^B)wQl+K;AX z!$?{_3}+O>P*yn#W>w=rRyFl!)EmBxdfkirEIgR!hdF@#Z~rHB14mpxw8!-$8=iFB z(6?fiz6E>z6d+OaClcnKIYA6fNdmWpk-LU8d8$v7H}L<%!vESFGC+=aR_BOU`2Ry$ zAU>5v;!9B?=J5ZUtU^A?s^mTVe=Vs}=im?-U=JB!2X%n$?>a!qg1vxm&<}tNP_V3H zKVZVoQDc6V9Pl&d9xrX`!2cZ{W+ZbHnkM{T;U<(h_&))n@c)9?FaLk<1r(42a2`&toVbh)5e10&D@rR9P) zwp`G8mkWBwa=`$MPT=~cXmBo<^sePH-mhrG%QY=dt7?kN4fwyQtutD>I-{enfeNE* zs4#oR{|5fAAqzB76X>!##v!|HSY5zr;m*XC4*u5-SDIR60PT?5&<&9ThP-d|BYqvA zMj&Vyx`L*$A#9pT!lpS3|0hLFG7A5Pun*wJK7dzJw|US9;6@(+aJf(ea02+>gBrjh z19;K-?DHT4_|=U-NZmyJe~|&&p|rLeE^7N>d@B6E8mXhQUer$_8N(F*Uk@jZ({LO_ zOw(`>JMUieB(|29xWVY^d+k5MzJC{M|0>r0IjsHRe_;gf4$7^(M`Eb7g?iUVv*TP}%N;K+U3P*zr z;dpSy-x!{7cSlE@!_hwLXmyuyFy30)A8-7#zc&5*U^4mZa610$XuA6M(fa7`T zCma1AXPdnrXIq`69M;}Irr-GUO_ z-@J41yZ3f}|K7&$KhXUCqm|!-``b?z_`o74gWL-;00_O(0^chwbidh!&NrvX@$M31 z&t77C_hROZPv-gX$$cMQxp(GPcxNu)Q2J*!RS@nE%_$Ag2GO(To0CTOoHRM$Q|p`} zh`EI{d|D_i(EP%Jr$zl~u`W=3Qnd0HR?R16B|a&O`;(*+pCnnDlVo#F61_Qr9(jUT zg_{?S-UM*AX?Kns0M^hm*9YjIGY-&0H=fhP`kW@WkTZ6XGY;mqh4US#w*#yF@DmlR zPb+)A?S4|!7Cv+ONl>ewW~%>bqB@^OOC3}`4c5|^!CLsnNaw#yboR?+OMjW| z=`X^O`68UzFVdC&NxF+aDNpq$<+kpZF~Iyu1o0a|J1T`PhA#no4hsJ`GuBdkAbflh$>zo7bP;Fljv+`_$i1v3&k z`8$eTxV5s2H#DnsO*6|^G_!I=Gs>6ra^ZqrPMzD9qi43|@R@@VK6SA|Cmwd_*tZgf z|6}lf{4gp?9>m27_&>gfJ@(zSB)FTA_;#{l_jXR~*vg4*n|U$4krz8Q@)F;AUJ{$; zr0GdcmYHPb*|m&3J5DR|t0_fglvK39V3<&>4&w50|2Mt3e65SG!JreDx7x@Ft%SVR zO2YpsS-p`)2EdxVo|WcmS!o(1s#*9yEAaz&IV*9Na^JX%d8xNpkOm4xSu9_Y7jh+e z6B(eJEh^gZe>I(#7g9NSKAA-gAS0_K(y~@OE$hV6vR*VJ??B^(~PkMrO>q zHKnLcW12bu=Y|Y+) zt5Vm}U;aNr2G|o&;En3V|sm$(U48W|u0{|7n@ronOs zWKE-A?l*2O=k+_wdEFLVKUpqn2FpctXSt-VFPAihrIIGGRMG~QN;=Px`q1_%XMvTxuGd9!~M%`ZF9M!YcF^79Yz=Q3~ffw_ziME2Xs*n=pzG+ zSRKQJ)zMGcZT*DZ){QwW?FgV2pdBCsU_U_H=Qgwh_Yp2Q*Ga`v^sa(UiX$i=-vph_7@ELSHgb(QqaTNe=t1djfcl8 z8>2(^_Ub-kYrMNO9dG@!y0-DxXfpkCJemBtK3)5BYkl?4-Hp*-`y0c*jyDH?pKbO3 zx!CUhbF$t3v9r_qvAWy%(cY~uRrjh(h5gEM`k=fVJ18@Phh>K6sKju<`&K~0{ia{; zXXwvz4E=eAslQ0E^%n__@iMw%x(f4%>mc89?dM}|zrc6v5#Veezj){5H{kxxy^Ys> zpn07~D-Yi1cNW0=ZP0wNpgwF7WM8d<_?uM_dZPvYcN@-<*+rgrr^xx?65BuA68p>} zwa+jsXy%sNW=;h?w=3znO-0YGYCGI-|0K2cPeMu7bFa=v+2kd@gS|xt>g6&Vz zJi&hX5cEGS#PHKXu6|mn$)^?j>Q;IazTbiO_febQ{`$s=^|*8I%i`GkqELUQY&&11 zZR?Aqr(Xm;{xZ`8IKKO3q?@4nWuVJH4RqC+X{h@xP>R18jp`u=g+FJNTFMo`6gI{1r05#ZSt0h753m41gS9-~CD1 z#y?5Ajeif-FB9qjMzZ~-C;DGH)9_Pg>>vYFunz$LYon+EgmEq~hJUD;UGOB=IvVPj^_ZOr(Yof$oKup*}}cJKtZr5yWsfukTlco-H2;r}4~ zAKXt!{Ci1>Z#O0N?xdxjos87Iosl}XvQqnIR%+YGNu3)xS#UikOHJX|NlspzWED6A zs3?xnbGMpN)JJJWXM~=*VM;z6B;~_?Qr=(C1Kp&&2il#ayxva9E3LG=3hK>_yxGXg z8jYN+TF=XhwLE$N^U`!RFO605k^pd*^AhKR626A}eZ?aDUy{QA(tNHYYk*F+ByVMk z@=CfO&!zGJb%116UQ1-;t$0S>iDebNXjU=@KuqCWeYd9M?6cmZRAWyUfIk-P-tnf323O}t!2GG{w{~9l; zF2esw_&>}|DE#n$07O<|a?Hq;V;+#az`-62Cn6iNBQp432LH>j{+FTtFN5&&PO>-@!2zR8R#wvhq084=YwBcfSF2IwIJ!2cko?=8pm{pF-#u$(dsm(w5vvc}A5hnV0d+kT&@{rR1B7$hPNb&oMVq=o3_B6AqJA}&(T`)8ED=i@*J3zf5{sMG zVo}ptG;Cgr29W`L#3bxN25^&@9{~RYtpBI*|GLv;U3X~d4ZE7&v}tWywBEi=o9#PR zi(}VnckWx=&I7C8b!Z8@k1TQbi6!GXql(^hs_wlcd%i1T-G5^~@ZXw_1GmO~xPQ8E zf9O_M4c}@jnDy6)+@kOAR@sf+DVp&+Ss6Y5*~FbFg|&YY{!hUDvGg4`2LGor_uNwU zf!E4C^7^?aUN4UfP?Jxwg8$JRa}<(_7p6t2x>Dvp$*r(VR~H=&!H+F?ZUuRo`zb>|Wf1m7h{@&hg{WFCB8+)}M<^3w=r<9jc2c_lMVQD#ZRAhLMiwq~A z;eHD4H=X7ghHv+08TyMf{GVhQF5_(DRg`184zIxfT+5B0XS?J zAAa0MR)DvuAy}-N2VW#==oGyQw~yiYHMo8q-rqvqeGTjG37EpW8(`}v=~}S)lXPyt z<&&R;V+61Ff0`WapGJEf!2R|jDEw>uCjYO=p8Xf@|JUry{7blU{}S%}zl5g%rR}pJuxA(}cYM1OB-d{|m6M=feMd_+Jn>42%2+QL* z)V-UOd3I6@N#K4*&hn5tzN~JApWX8HqWUO1}483Q{(kI5O_1c)TJQ;G-n?6sz z>+<9q{Qn03&ol+%f2a%O|AhblKluOuRhB5!0H}BP|4Ck@?&1Gy`2S2=r;a2IWB`D5 z+zJ;@A6Jdo|6_(5cVZqcD1)==YT^i@92(eZIV5)XG1!2cy)Qk6mm zfd7@a1LwO25Qh8XD^XdB6P4yU5owJRk#sp>$&eiuuYxf$#>1gtV~ zrUT$9TSEpw4p6}T$|*7c{I4D$2Micd?Ql7!8!e+xVj1@~FQ-8UF3###mvZ{i67DNn zD(E-i{=KE5_5kbr9e95N=XaMXss^b2sH)08YO4H?nmYTV{!Q^mT~h=26IW9QRZ#iS z#A_{06YlTA{ez{BZgr`vTU+Ys*TKee->_i1+&8X)G3o#d*1#10-(+_6JItp>M=6F8Zy8-x24+xJG_>D8*Ki{0LC#gzz}@^Jz>M#7S)Lc z{@eV@05wZiQnRA_6d9mS2V`}d8#v^3J8(J`H5dHv@hPj`psMNz*aN8IR*G5}6R0D3 zZ7W*Uc47@(FJ9FR;syOEk=Cy!5{A`8(lAaWjpKL*%RXL$A|X+lJL(-?R|+EsMpmZK0jJl-;>UIbHjd$9+f! z+(%^0b3)|2XGFz!Vea~_OjG~0X)kbNI1JqCcZ0V$op^_J|DCoHzSER2=f51i#|_8# z==;Bym*V%bT;g7mhX2#>e+KRc+zkAm$=t2vv-jK@=;j`{!#pwoXceA#)#4MkRD9-^ zN-rzr^6LUlvGT^rRNvV#5U9PgeGBTK@y_zY|H0-PGtzoxBw8PoCL4bg zrtAMzUZ4CQjrFzv>TZnxYqUB3uj$t6A3Iy4KaaMDf1d30|2o|5{H=1p0n!l@dW`;>*7E2$9S(atXVrFKx%*@OV zHbd@Hd)L{gy85O@|CN5_$Wy2L;u)V{q#a{qWIpejYp%IQeCLxR@r#L(+=Xcv?jOdQ zf24e^8!26DN6I(a(dw;sw05f;rtJqk#OL*6G1C@PouXr7hf1+QuEFn*&RCv~J~&*00^k^y_zW zJQapz(81cgey1{4u{N*Wsm(Qb99XahpR8kT-h}hpcN#cZW2=EWXo5C8-1)`SEF9jt zS5MXM)z;FzYBG1PoJ`#-EwOvWL3r znLH?f5~w|>XWC!^EI+7ht6=>>ZQBLM`2A<#^dkrS$^eJR0K3Q{>vtOS!krp*0F|M2 zr_j&B|8qZO8lY~X29QSvh@ua`iww~9zYR42>sMMj`IVMTeW4_-FQgdf|3#@=LX-#9 z8$#H)CWOr^Qqa1h`K?PiuXUm1HqO;J6QUiO-CHP}8K)4rzgm)oCgyu10XD&iyI$bbxXEsJ8+fgFfiV~S7&fhg7xIYW) z?s`O=tVKkzT0|JBf=Waf1MzZHlqf~v{}}uq$NE1Z$>)>e9LVNU;%qi0%4Xs5Oi~n2 zCy@b?LQg6obfuC)S28JdcHN&y!T+hQ|M}r)il2<81ldSRm^KwDydyy^+2hp8OpIQ(MP+l-5qWbetgKl> zs@f#>uPyNX1TbU1oe6Vojacs+Li#y<$S|)98yB?^KTk5E3Ullc;DWirP zMNI!y9@Bk82KXk6X}%%@{DA+Ty8b8Oembs)Yxw^{oJ9>Fr#%$rwY$QC zc2iK)u7fpxmx6YcpVutGH4D6qx`XrRZEgY%ieXPWA}@}IWrgt&T^z%mgaCU0WEzB# z0X)b6PGkTlGC+6z|Ka~(w*YtW@smR?UJeuoUEIe$-{3^%CuW@{W8r%Ot zo$V7gkO5jOuei-ZF9$LJ{Qr>wT1*f2@xAms(@!rjgY+UZL@zKRbcYe6TX238%rX+P zCDiu2?%x_nk!zqckR~edejeV>4djpk@*iXd3V2^6il72&AC$=2K#8mml&I!FnQ9M| z>CQlfo*$^n7QhmtCSL;^jGAH{tib)q0skiheE1*x0t@VF*Z=f7yFzVn$~fy#CN>63 z#M)qySQ#o3OMm5n`Qai3=tcOy%ThN3!2c_#0W6K@m2=~HRfC&TV;U@K09kDYb(%El zHTc`8k7F)CP>B5ifDB-q5+es716U8=-XDSzCEEgv`up3tdTNy3^LQ4UTvP&8O#eh)5Mb2WLef4Eh`$sU+p5U)t}0{OS5|F@%FfKOV#R(UUw53ymK~=w{Er)q�{HoJ_bc$SCgl33)H1 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 literal 0 HcmV?d00001 diff --git a/assets/override/fpSEAM.glsl b/assets/override/fpSEAM.glsl index 87165d1..a53c593 100644 --- a/assets/override/fpSEAM.glsl +++ b/assets/override/fpSEAM.glsl @@ -28,6 +28,7 @@ uniform lowp sampler2D uIeeAreaMask; // unit 2: one liquid mode per 64px WE uniform lowp sampler2D uIeeNormalMap; // unit 3: tiling water normal map uniform lowp sampler2D uIeeDudvMap; // unit 4: tiling DuDv distortion map uniform lowp sampler2D uIeeFoamMap; // unit 5: tiling foam mask +uniform lowp sampler2D uIeeNoiseMap; // unit 6: tiling FBM octaves (R/G smooth, B high-freq, A blobs) varying highp vec2 vTc; varying highp vec2 vRef; @@ -306,6 +307,8 @@ void main() // flat locals (no struct/function) for maximum GLSL-frontend // compatibility. vec3 fxGlow = vec3(0.0); + vec3 fxFlame = vec3(0.0); + float fxFlameA = 0.0; float fxShimmer = 0.0; float fxHaze = 0.0; if (uIeeEnabled > 0.5 && vColor.a > 0.9 && uIeePointCount > 0.5) @@ -319,46 +322,84 @@ void main() float kind = p.z; float strength = p.w; - if (kind < 1.5) // fire: warm flickering glow + heat shimmer above + if (kind < 1.5) // fire: textured flame body + cast light + shimmer { - // The ARE anchor sits below the visible flame (BAM frames draw - // upward from it); bias the glow center up onto the fire. - offs.y += 24.0 * strength; - float radius = 140.0 * strength; - float d = length(offs); + float fw = 21.0 * strength; // half-width at the base + float fh = 76.0 * strength; // flame height above the anchor + + // --- flame body (replaces the engine's BAM flame) --- + if (offs.y < 16.0 && offs.y > -fh * 1.25 && abs(offs.x) < fw * 2.4) + { + float v = clamp(-offs.y / fh, 0.0, 1.2); // 0 base -> 1 tip + float widthAt = fw * (1.05 - 0.62 * min(v, 1.0)); + float xr = offs.x / max(widthAt, 1.0); + float radial = 1.0 - clamp(xr * xr, 0.0, 1.0); + float base = smoothstep(14.0, 0.0, offs.y); + float column = radial * base * smoothstep(1.18, 0.55, v); + // Rising noise erodes the column: calm base, ragged tip. + float seed = p.x * 0.61 + float(i) * 19.0; + float nA = texture2D(uIeeNoiseMap, + vec2((seed + offs.x) / 46.0, (offs.y + fxT * 95.0) / 68.0)).r; + float nB = texture2D(uIeeNoiseMap, + vec2((seed * 0.37 - offs.x) / 27.0, (offs.y + fxT * 150.0) / 39.0)).b; + float intensity = column * (1.25 - v * 0.55) + - (nA * 0.72 + nB * 0.55) * (0.28 + 0.9 * v); + intensity = clamp(intensity * 2.1, 0.0, 1.0); + if (intensity > 0.02) + { + // Blackbody-style ramp: deep red -> orange -> yellow -> near white. + vec3 flame = mix(vec3(0.45, 0.02, 0.0), vec3(1.0, 0.30, 0.02), + smoothstep(0.02, 0.34, intensity)); + flame = mix(flame, vec3(1.0, 0.72, 0.18), smoothstep(0.34, 0.68, intensity)); + flame = mix(flame, vec3(1.05, 0.98, 0.80), smoothstep(0.68, 0.94, intensity)); + float a = smoothstep(0.04, 0.32, intensity); + if (a > fxFlameA) + { + fxFlameA = a; + fxFlame = flame; + } + } + fxShimmer += column * 0.5 * strength; + } + + // --- cast light on the surroundings --- + vec2 g = offs; + g.y += 26.0 * strength; // center the light on the flame body + float radius = 120.0 * strength; + float d = length(g); if (d < radius) { float fall = 1.0 - d / radius; - // Fuller mid-falloff with a hot core. - fall = fall * fall * (0.55 + 0.45 * fall) + 0.6 * smoothstep(0.75, 1.0, fall); - // Two-rate flicker, unsynchronized between points. + fall *= fall; float fi = float(i) * 7.31; - float flick = 0.78 + 0.16 * sin(fxT * 9.7 + fi) - + 0.08 * sin(fxT * 23.0 + fi * 1.7); - fxGlow += vec3(1.0, 0.45, 0.15) * (fall * 0.85 * strength * flick); - // Shimmer only above the flame (world y grows downward). - float above = clamp(-offs.y / (radius * 0.9), 0.0, 1.0); - fxShimmer += fall * above * strength; + float flick = 0.80 + 0.14 * sin(fxT * 9.7 + fi) + + 0.06 * sin(fxT * 23.0 + fi * 1.7); + fxGlow += vec3(1.0, 0.45, 0.15) * (fall * 0.75 * strength * flick); } } - else if (kind < 2.5) // smoke: drifting haze column rising from the point + else if (kind < 2.5) // smoke: textured plume replacing the BAM puffs { - float height = 200.0 * strength; + float height = 220.0 * strength; float rise = -offs.y; // px above the source - if (rise > -14.0 && rise < height) + if (rise > -12.0 && rise < height) { - float sway = sin(fxT * 0.9 + rise * 0.04 + float(i)) * (7.0 + rise * 0.18); - float lateral = abs(offs.x - sway); - float widthPx = 18.0 + rise * 0.35; - if (lateral < widthPx * 2.0) - { - float across = exp(-(lateral * lateral) / (widthPx * widthPx)); - float along = smoothstep(-14.0, 16.0, rise) * (1.0 - rise / height); - float puff = ieeNoise(vec2(worldPos.x * 0.045, - (worldPos.y + fxT * 40.0) * 0.045) - + vec2(float(i))); - fxHaze += across * along * (0.40 + 0.60 * puff) * 0.80 * strength; - } + float prog = rise / height; + float seed = p.x * 0.37 + float(i) * 11.0; + // Slow lateral wander that grows with altitude. + float sway = (texture2D(uIeeNoiseMap, + vec2(seed / 64.0 + fxT * 0.045, prog * 1.7)).a - 0.5) + * (12.0 + 80.0 * prog); + float widthPx = 15.0 + 62.0 * prog; + float lateral = (offs.x - sway) / widthPx; + float across = exp(-lateral * lateral * 1.7); + // Two scrolling octaves shape the billows. + float dA = texture2D(uIeeNoiseMap, + vec2((offs.x + seed) / 110.0, (offs.y + fxT * 46.0) / 110.0)).g; + float dB = texture2D(uIeeNoiseMap, + vec2((offs.x - seed) / 56.0, (offs.y + fxT * 72.0) / 56.0)).r; + float density = clamp(dA * 0.80 + dB * 0.55 - 0.30, 0.0, 1.0); + float along = smoothstep(-12.0, 14.0, rise) * (1.0 - prog); + fxHaze += across * along * density * 1.05 * strength; } } else if (kind > 3.5 && kind < 4.5) // light: steady soft glow @@ -375,7 +416,7 @@ void main() } } fxShimmer = clamp(fxShimmer, 0.0, 1.0); - fxHaze = clamp(fxHaze, 0.0, 0.85); + fxHaze = clamp(fxHaze, 0.0, 0.9); } vec2 sampleTc = vTc; @@ -490,20 +531,29 @@ void main() // Firelight, candle glow, and smoke haze over the (possibly water-graded) // background art, in linear light so the warm lift does not band. - if (fxHaze > 0.004 || fxGlow.r + fxGlow.g + fxGlow.b > 0.004) + if (fxHaze > 0.004 || fxFlameA > 0.004 || fxGlow.r + fxGlow.g + fxGlow.b > 0.004) { vec3 lin = ieeSrgbToLinear(texColor.rgb); float sceneLuma = dot(lin, vec3(0.2126, 0.7152, 0.0722)); - // Multiplicative warm lift preserves the art's detail; the additive - // term is night-adaptive — dark scenes get real cast light, bright - // day scenes only a whisper. - float castLight = 0.05 + 0.50 * (1.0 - clamp(sceneLuma * 4.0, 0.0, 1.0)); + // True black means void/unexplored: no art exists there, so cast + // light must not paint it (the additive term is luma-gated; the + // multiplicative term is naturally zero on black). + float hasArt = smoothstep(0.0015, 0.012, sceneLuma); + // Night-adaptive cast light: dark scenes get real light, bright day + // scenes only a whisper. + float castLight = (0.05 + 0.45 * (1.0 - clamp(sceneLuma * 4.0, 0.0, 1.0))) * hasArt; lin = lin * (vec3(1.0) + fxGlow * 1.5) + fxGlow * castLight; - // Smoke veil: reads as moonlit smoke over dark roofs, shadowy over - // bright ground. - vec3 hazeTarget = vec3(0.42, 0.42, 0.46) * (0.42 + 0.5 * sceneLuma) + vec3(0.02); - lin = mix(lin, hazeTarget, fxHaze); + // Smoke veil: moonlit smoke over dark roofs, shadowy over bright + // ground; never over the void. + vec3 hazeTarget = vec3(0.40, 0.40, 0.45) * (0.40 + 0.55 * sceneLuma) + vec3(0.02); + lin = mix(lin, hazeTarget, fxHaze * hasArt); + // Flame body composites over everything with its own self-glow; + // flames are visible against the void (a fire at a cave mouth), so it + // is deliberately not art-gated. + lin = mix(lin, fxFlame, fxFlameA); + lin += fxFlame * fxFlameA * 0.35; texColor.rgb = ieeLinearToSrgb(lin); + texColor.a = max(texColor.a, fxFlameA); } texColor = texColor * vColor; diff --git a/docs/are-animation-detection.md b/docs/are-animation-detection.md index 721416b..a9134ce 100644 --- a/docs/are-animation-detection.md +++ b/docs/are-animation-detection.md @@ -18,10 +18,18 @@ them, and publishes an immutable snapshot (`AppContext::areaAnimations`). The first consumer is the fpSEAM point-effects pass: the snapshot's shown fire/smoke/light entries are packed into a 32-slot `uIeePoints[]` uniform -array (`build_area_effect_points`, fire prioritized under capacity pressure) -and fed through the existing uniform bridge. The shader renders warm -flickering fire glow with heat shimmer above flames, drifting chimney-smoke -haze columns, and steady candle/glow halos — background layer only, gated by +array (`build_area_effect_points`, fire prioritized under capacity pressure, +per-resref flame scale) and fed through the existing uniform bridge. The +shader draws textured **replacements**, not just halos: shaped flame bodies +eroded by rising FBM noise with a blackbody color ramp, heat shimmer, and +night-adaptive cast light; noise-billowed smoke plumes that wander and +dissipate with altitude; steady candle/glow halos. The tileable FBM octaves +ship as `iee_effects_noise.rgba` (unit 6, generated in-repo — see +`assets/game-textures/`). While the effect is active, a `CGameStatic::Render` +hook (manifest pattern, offline-verified on both builds: RVA `0x1F2570` / +`0x1F27D0`) suppresses the engine's authored fire/smoke BAM draws so the +replacements own the pixels; WBM/PVRZ setpieces and every other kind stay +vanilla, and any resolution failure just keeps the engine draws. Gated by `[Shaders] EnablePointEffects` and the F10 master toggle. Effects that must composite over sprites (true bloom) remain future P4 work consuming the same points. diff --git a/src/iee/game/are_animations.cpp b/src/iee/game/are_animations.cpp index 4643164..2a5da23 100644 --- a/src/iee/game/are_animations.cpp +++ b/src/iee/game/are_animations.cpp @@ -240,10 +240,20 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& std::vector points; points.reserve((std::min)(info.animations.size(), kMaxAreaEffectPoints)); - const auto strengthFor = [](AreaAnimationKind kind) noexcept { - switch (kind) { - case AreaAnimationKind::Fire: + const auto strengthFor = [](const AreaAnimationInfo& animation) noexcept { + switch (animation.kind) { + case AreaAnimationKind::Fire: { + // Flame scale by authored family: wall sconces and small flames stay + // small, fire pits and campfires get the full body. + const auto resref = animation.resrefView(); + if (resref.starts_with("FLM") || resref.starts_with("FLSM") || resref.starts_with("FIM") || + resref.starts_with("YSFL")) { + return 0.55f; + } + if (resref.starts_with("FLAM")) return 0.7f; + if (resref.starts_with("FPIT")) return 1.15f; return 1.0f; + } case AreaAnimationKind::Smoke: return 1.0f; case AreaAnimationKind::Light: @@ -264,7 +274,7 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& points.push_back(AreaEffectPoint{static_cast(animation.x), static_cast(animation.y), static_cast(static_cast(animation.kind)), - strengthFor(animation.kind)}); + strengthFor(animation)}); } } return points; diff --git a/src/iee/game/build_manifest.cpp b/src/iee/game/build_manifest.cpp index a09bfc1..0d03452 100644 --- a/src/iee/game/build_manifest.cpp +++ b/src/iee/game/build_manifest.cpp @@ -146,8 +146,12 @@ constexpr BuildManifest kKnownBuilds[] = { // CGameObjectArray::GetShare (EEex InfinityLoader.db, version- // independent section). Verified unique on 2.6.6.0 at 0x276490. "48 C7 02 00 00 00 00 83 F9 FF", + // CGameStatic::Render prologue (offline-verified unique on both + // supported builds; PDB-named decompilation evidence). + "40 55 56 57 48 83 EC 50 48 8B 05 ? ? ? ? 48 33 C4 48 89 44 24 30 " + "48 8B 05 ? ? ? ? 49 8B F0 48 8B EA 48 8B F9", }, - {0x27E710, 0x4247E0, 0x276490}, + {0x27E710, 0x4247E0, 0x276490, 0x1F2570}, {0x100, 0x1DC, 0x14, 0x6590, 0x6598, 0x65F8}, {{ {"CRes_Demand", 0x36, BranchInstructionKind::CallRel32, 0xE8, 1, 5, true}, @@ -178,8 +182,11 @@ constexpr BuildManifest kKnownBuilds[] = { // 2.7.3.0 binary: unique match, identical body shape, globals at // 0x68F8F4 (max index) / 0x68F910 (entry table). "48 C7 02 00 00 00 00 83 F9 FF", + // CGameStatic::Render (offline-verified unique at 0x1F27D0). + "40 55 56 57 48 83 EC 50 48 8B 05 ? ? ? ? 48 33 C4 48 89 44 24 30 " + "48 8B 05 ? ? ? ? 49 8B F0 48 8B EA 48 8B F9", }, - {0x27EBD0, 0x4257C0, 0x276700}, + {0x27EBD0, 0x4257C0, 0x276700, 0x1F27D0}, {0x100, 0x1DC, 0x14, 0x6590, 0x6598, 0x65F8}, {{ {"CRes_Demand", 0x36, BranchInstructionKind::CallRel32, 0xE8, 1, 5, true}, @@ -203,6 +210,10 @@ static_assert(validate_pattern_format(kKnownBuilds[0].patterns.renderTexture), "RenderTexture pattern format is invalid"); static_assert(validate_pattern_format(kKnownBuilds[0].patterns.objectArrayGetShare), "GetShare pattern format is invalid"); +static_assert(validate_pattern_format(kKnownBuilds[0].patterns.staticRender), + "StaticRender pattern format is invalid"); +static_assert(validate_pattern_format(kKnownBuilds[1].patterns.staticRender), + "2.7.3 StaticRender pattern format is invalid"); static_assert(kKnownBuilds[0].validate(), "Known build manifest is invalid"); static_assert(validate_pattern_format(kKnownBuilds[1].patterns.loadArea), "2.7.3 LoadArea pattern format is invalid"); diff --git a/src/iee/game/build_manifest.h b/src/iee/game/build_manifest.h index d8446c3..54ff179 100644 --- a/src/iee/game/build_manifest.h +++ b/src/iee/game/build_manifest.h @@ -36,6 +36,10 @@ struct PatternSet { // globals for the ARE-animation memory path. Empty (or a non-unique match // at runtime) disables that path — the disk reader remains the fallback. std::string_view objectArrayGetShare{}; + // Optional: CGameStatic::Render. Hooked to suppress the engine's authored + // fire/smoke BAM draws while the fpSEAM point effects replace them. Empty + // or non-unique keeps the engine draws (effects stay additive). + std::string_view staticRender{}; }; struct ReferenceRvas { @@ -43,6 +47,7 @@ struct ReferenceRvas { std::uintptr_t renderTexture{}; // Diagnostic only; 0 means "not yet observed on this build". std::uintptr_t objectArrayGetShare{}; + std::uintptr_t staticRender{}; }; struct RuntimeOffsets { diff --git a/src/iee/game/game_addrs.cpp b/src/iee/game/game_addrs.cpp index cfeb8b2..2aaa1ab 100644 --- a/src/iee/game/game_addrs.cpp +++ b/src/iee/game/game_addrs.cpp @@ -62,6 +62,24 @@ namespace iee::game { recover("RenderTexture", out.RenderTexture, renderTextureMatches, manifest.referenceRvas.renderTexture, manifest.patterns.renderTexture); + // Optional target: point-effect BAM replacement. Failure only keeps + // the engine's authored draws; never blocks initialization. + if (!manifest.patterns.staticRender.empty()) { + std::size_t staticRenderMatches = 0; + out.StaticRender = reinterpret_cast(core::find_unique_in_module( + nullptr, manifest.patterns.staticRender, &staticRenderMatches)); + recover("StaticRender", out.StaticRender, staticRenderMatches, + manifest.referenceRvas.staticRender, manifest.patterns.staticRender); + if (out.StaticRender) { + LOG_INFO("CGameStatic::Render resolved at RVA 0x{:X} (reference 0x{:X})", + out.StaticRender - moduleBase, manifest.referenceRvas.staticRender); + } else { + LOG_WARN("CGameStatic::Render pattern matched {} times; authored fire/smoke " + "draws will not be replaced", + staticRenderMatches); + } + } + const bool success = out.LoadArea && out.RenderTexture; if (success) { diff --git a/src/iee/game/game_addrs.h b/src/iee/game/game_addrs.h index d5a2c5e..3a3f16e 100644 --- a/src/iee/game/game_addrs.h +++ b/src/iee/game/game_addrs.h @@ -11,6 +11,9 @@ namespace iee::game { struct GameAddresses { std::uintptr_t LoadArea = 0; std::uintptr_t RenderTexture = 0; + // Optional (0 when the pattern did not resolve uniquely): the point + // effects then stay additive instead of replacing the engine draws. + std::uintptr_t StaticRender = 0; bool initialized = false; }; diff --git a/src/iee/game/texture_units.h b/src/iee/game/texture_units.h index fd08863..96ac3a4 100644 --- a/src/iee/game/texture_units.h +++ b/src/iee/game/texture_units.h @@ -9,5 +9,6 @@ inline constexpr unsigned AreaMask = 2; inline constexpr unsigned WaterNormal = 3; inline constexpr unsigned WaterDudv = 4; inline constexpr unsigned WaterFoam = 5; +inline constexpr unsigned EffectsNoise = 6; } // namespace iee::game::texture_units diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index b995af1..03074f2 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -23,6 +23,7 @@ namespace iee::hooks { using LoadAreaFn = void* (*)(void*, void*, unsigned char, unsigned char, unsigned char); using RenderTextureFn = void (*)(void*, int, void*, int, int, unsigned long); using DrawColorToneFn = void (*)(int); +using StaticRenderFn = void (*)(void*, void*, void*); // Hook management - initialize MinHook // Intentionally explicit lifetime: a static smart-pointer destructor would @@ -31,6 +32,7 @@ static core::HookInit* g_hookInit = nullptr; static core::Hook g_loadAreaHook; static core::Hook g_renderTextureHook; static core::Hook g_drawColorToneHook; +static core::Hook g_staticRenderHook; static AppContext* g_ctx = nullptr; @@ -275,6 +277,33 @@ static void detour_draw_color_tone(int mode) { g_drawColorToneHook.original()(mode); } +// CGameStatic::Render hook: while the fpSEAM point effects are active, the +// authored fire/smoke BAM draws are replaced by our textured effects, so the +// engine's own little flame/puff loops are skipped. Everything else (lights, +// wildlife, WBM/PVRZ setpieces, unclassified overlays) renders vanilla. +static void detour_static_render(void* thisPtr, void* area, void* vidMode) { + try { + if (g_ctx && g_ctx->cfg.enablePointEffects && g_ctx->cfg.enableWaterEffect && + probe::override_effect_enabled() && thisPtr) { + game::ARE_Animation_st header{}; + const auto* headerAddress = + reinterpret_cast(thisPtr) + offsetof(game::CGameStatic, m_header); + if (core::safe_read(headerAddress, header) && + (header.nFlags & + (game::kAreAnimationFlagUseWbm | game::kAreAnimationFlagUsePvrz)) == 0) { + const auto info = game::make_area_animation_info(header); + if (info.kind == game::AreaAnimationKind::Fire || + info.kind == game::AreaAnimationKind::Smoke) { + return; // replaced by the shader's point effects + } + } + } + } catch (...) { + // Suppression is cosmetic; any doubt falls through to the engine draw. + } + g_staticRenderHook.original()(thisPtr, area, vidMode); +} + // RenderTexture hook - thin dispatch into the tile upscale feature static void detour_render_texture(void* thisPtr, int texId, void* unused, int x, int y, unsigned long flags) { @@ -350,6 +379,20 @@ bool install_all(AppContext& ctx) { "transform cannot be published safely. Tile upscaling remains enabled."); } + if (ctx.addrs.StaticRender) { + try { + g_staticRenderHook.create(reinterpret_cast(ctx.addrs.StaticRender), + reinterpret_cast(&detour_static_render)); + g_staticRenderHook.enable(); + LOG_INFO("CGameStatic::Render hook installed (fire/smoke BAM replacement)"); + } catch (const std::exception& e) { + LOG_WARN("CGameStatic::Render hook failed ({}); authored fire/smoke draws stay vanilla", + e.what()); + } catch (...) { + LOG_WARN("CGameStatic::Render hook failed; authored fire/smoke draws stay vanilla"); + } + } + g_loadAreaHook.enable(); LOG_INFO("LoadArea hook enabled"); @@ -364,6 +407,7 @@ bool install_all(AppContext& ctx) { return true; } catch (const std::exception& e) { LOG_ERROR("Exception during hook installation: {}", e.what()); + (void)g_staticRenderHook.remove(); (void)g_drawColorToneHook.remove(); (void)g_renderTextureHook.remove(); (void)g_loadAreaHook.remove(); @@ -373,6 +417,7 @@ bool install_all(AppContext& ctx) { return false; } catch (...) { LOG_ERROR("Unknown exception during hook installation"); + (void)g_staticRenderHook.remove(); (void)g_drawColorToneHook.remove(); (void)g_renderTextureHook.remove(); (void)g_loadAreaHook.remove(); @@ -389,6 +434,7 @@ void uninstall_all() noexcept { } catch (...) { } + (void)g_staticRenderHook.remove(); (void)g_drawColorToneHook.remove(); (void)g_renderTextureHook.remove(); (void)g_loadAreaHook.remove(); @@ -408,6 +454,7 @@ void prepare_for_shutdown() noexcept { // state are torn down. MinHook itself stays initialized until // uninstall_all(), after every MinHook-backed subsystem has removed its // hooks. + (void)g_staticRenderHook.disable(); (void)g_drawColorToneHook.disable(); (void)g_renderTextureHook.disable(); (void)g_loadAreaHook.disable(); diff --git a/src/iee/shader_uniform_bridge.cpp b/src/iee/shader_uniform_bridge.cpp index 1edfa90..b4cc911 100644 --- a/src/iee/shader_uniform_bridge.cpp +++ b/src/iee/shader_uniform_bridge.cpp @@ -230,6 +230,7 @@ void feed(unsigned program, Locations& locations) { locations.normalMap = resolve_location(gl, program, locations.normalMap, "uIeeNormalMap"); locations.dudvMap = resolve_location(gl, program, locations.dudvMap, "uIeeDudvMap"); locations.foamMap = resolve_location(gl, program, locations.foamMap, "uIeeFoamMap"); + locations.noiseMap = resolve_location(gl, program, locations.noiseMap, "uIeeNoiseMap"); g_feedCount.fetch_add(1, std::memory_order_relaxed); const auto stateRevision = g_stateRevision.load(std::memory_order_acquire); @@ -242,11 +243,12 @@ void feed(unsigned program, Locations& locations) { bool boundTextures = false; if (effectValue >= 0.5f) { boundTextures = locations.areaMask >= 0 || locations.normalMap >= 0 || locations.dudvMap >= 0 || - locations.foamMap >= 0; + locations.foamMap >= 0 || locations.noiseMap >= 0; if (locations.areaMask >= 0 && !area::bind_area_texture()) { effectValue = 0.0f; } - if ((locations.normalMap >= 0 || locations.dudvMap >= 0 || locations.foamMap >= 0) && + if ((locations.normalMap >= 0 || locations.dudvMap >= 0 || locations.foamMap >= 0 || + locations.noiseMap >= 0) && !water::ensure_water_textures_bound()) { effectValue = 0.0f; } @@ -351,6 +353,8 @@ void feed(unsigned program, Locations& locations) { gl.glUniform1i(locations.dudvMap, static_cast(game::texture_units::WaterDudv)); if (locations.foamMap >= 0) gl.glUniform1i(locations.foamMap, static_cast(game::texture_units::WaterFoam)); + if (locations.noiseMap >= 0) + gl.glUniform1i(locations.noiseMap, static_cast(game::texture_units::EffectsNoise)); locations.samplersInitialized = true; } locations.lastAppliedRevision = stateRevision; diff --git a/src/iee/shader_uniform_bridge.h b/src/iee/shader_uniform_bridge.h index cc2715a..64a6f24 100644 --- a/src/iee/shader_uniform_bridge.h +++ b/src/iee/shader_uniform_bridge.h @@ -24,6 +24,7 @@ struct Locations { int normalMap{kUnresolved}; int dudvMap{kUnresolved}; int foamMap{kUnresolved}; + int noiseMap{kUnresolved}; bool samplersInitialized{}; bool viewInitialized{}; diff --git a/src/iee/water_textures.cpp b/src/iee/water_textures.cpp index e66a8dc..91ea5f3 100644 --- a/src/iee/water_textures.cpp +++ b/src/iee/water_textures.cpp @@ -46,10 +46,13 @@ struct Entry { unsigned unit; }; -constexpr std::array kEntries{{ +constexpr std::array kEntries{{ {"iee_water_normal", game::texture_units::WaterNormal}, {"iee_water_dudv", game::texture_units::WaterDudv}, {"iee_water_foam", game::texture_units::WaterFoam}, + // Tileable FBM octaves for the fire/smoke point effects (R/G smooth FBM, + // B high frequency, A low-frequency blobs). + {"iee_effects_noise", game::texture_units::EffectsNoise}, }}; std::mutex g_textureMutex; @@ -242,7 +245,7 @@ bool upload_all_to_current_context() { if (context == g_uploadBlockedContext) return false; core::GlStateGuard guard({game::texture_units::WaterNormal, game::texture_units::WaterDudv, - game::texture_units::WaterFoam}); + game::texture_units::WaterFoam, game::texture_units::EffectsNoise}); // Do not attribute an error left by preceding engine work to the first DDS // upload and then suppress retries for the lifetime of this GL context. game::gl::discard_errors(); diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 2c29ba2..bf58ca4 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1333,7 +1333,7 @@ void test_fpseam_override_asset_contract() { for (const std::string_view name : {"uIeeEnabled", "uIeeTime", "uIeeScroll", "uIeeZoom", "uIeeViewport", "uIeeWorldSizeInv", "uIeeWaterTint", "uIeePointCount", "uIeePoints", "uIeeAreaMask", "uIeeNormalMap", - "uIeeDudvMap", "uIeeFoamMap"}) { + "uIeeDudvMap", "uIeeFoamMap", "uIeeNoiseMap"}) { expect_true(source.find(name) != std::string::npos, "fpSEAM override declares feed uniform"); } // The uniform-array capacity in the shader must match the bridge/packing cap. From 1994d966b30a9ebb49ff9eb922d679372b2784f3 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 18 Jul 2026 03:19:26 +0200 Subject: [PATCH 05/57] ci: add iee_effects_noise.rgba to the release-bundle allow-list --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d36652d..c1b05f8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,6 +109,7 @@ jobs: "override/M_IEEE.lua" "override/fpSEAM.glsl" "iee-textures/README.md" + "iee-textures/iee_effects_noise.rgba" "iee-textures/iee_water_dudv.rgba" "iee-textures/iee_water_foam.rgba" "iee-textures/iee_water_normal.rgba" From 3beab79ca2e84c1c7221d39cc22bb4f7813325bb Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 18 Jul 2026 03:32:09 +0200 Subject: [PATCH 06/57] Point effects: authored-size flames with palette colors; wispier smoke; fix haze mottling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calibrated against the exported BAM frame dimensions: FIRE* torch flames are 12x45 px, FLAM* sconces ~10x30, YSFL* small flames 7x14 — the v2 flame body was 42x76 for everything, hence the traffic-cone look. Flame strength now derives from authored height per family with the authored ~1:3.5 aspect, noise sampling scales with flame size so small flames keep structure, and the hot-white core is confined to flicker peaks. FLAMBLU*-family flames keep their authored blue (palette id in the kind fraction drives both ramp and cast-light color). Smoke plumes are narrower, wispier, and capped at 62% opacity so the roof stays readable, and the per-texel luma gate is removed from the haze mix — it mottled dark stonework; the gate remains only on the fire cast-light additive term, which is what protects the unexplored void. Also archives the owner-observed water follow-up (some cells render more transparent than the opaque water body) in the phase-2 water plan backlog. --- assets/override/fpSEAM.glsl | 80 +++++++++++-------- .../plans/2026-06-11-phase2-water.md | 10 +++ src/iee/game/are_animations.cpp | 32 +++++--- 3 files changed, 78 insertions(+), 44 deletions(-) diff --git a/assets/override/fpSEAM.glsl b/assets/override/fpSEAM.glsl index a53c593..e792c9f 100644 --- a/assets/override/fpSEAM.glsl +++ b/assets/override/fpSEAM.glsl @@ -324,35 +324,46 @@ void main() if (kind < 1.5) // fire: textured flame body + cast light + shimmer { - float fw = 21.0 * strength; // half-width at the base - float fh = 76.0 * strength; // flame height above the anchor + // Sized from the authored BAM frames: strength = height / 76, + // width follows the authored ~1:3.5 aspect. + float fh = 76.0 * strength; // flame height above the anchor + float fw = 3.5 + 0.16 * fh; // half-width at the base + float blue = fract(kind) * 10.0; // palette id: >0.5 = blue flame // --- flame body (replaces the engine's BAM flame) --- - if (offs.y < 16.0 && offs.y > -fh * 1.25 && abs(offs.x) < fw * 2.4) + if (offs.y < 8.0 && offs.y > -fh * 1.3 && abs(offs.x) < fw * 2.6) { float v = clamp(-offs.y / fh, 0.0, 1.2); // 0 base -> 1 tip - float widthAt = fw * (1.05 - 0.62 * min(v, 1.0)); + float widthAt = fw * (1.05 - 0.60 * min(v, 1.0)); float xr = offs.x / max(widthAt, 1.0); float radial = 1.0 - clamp(xr * xr, 0.0, 1.0); - float base = smoothstep(14.0, 0.0, offs.y); - float column = radial * base * smoothstep(1.18, 0.55, v); + float base = smoothstep(8.0, -1.0, offs.y); + float column = radial * base * smoothstep(1.20, 0.50, v); // Rising noise erodes the column: calm base, ragged tip. + // Sample scale follows the flame size so small flames keep + // visible structure. + float ns = max(fh, 14.0); float seed = p.x * 0.61 + float(i) * 19.0; float nA = texture2D(uIeeNoiseMap, - vec2((seed + offs.x) / 46.0, (offs.y + fxT * 95.0) / 68.0)).r; + vec2((seed + offs.x) / (ns * 0.62), + (offs.y + fxT * ns * 1.35) / (ns * 0.9))).r; float nB = texture2D(uIeeNoiseMap, - vec2((seed * 0.37 - offs.x) / 27.0, (offs.y + fxT * 150.0) / 39.0)).b; - float intensity = column * (1.25 - v * 0.55) - - (nA * 0.72 + nB * 0.55) * (0.28 + 0.9 * v); - intensity = clamp(intensity * 2.1, 0.0, 1.0); + vec2((seed * 0.37 - offs.x) / (ns * 0.36), + (offs.y + fxT * ns * 2.1) / (ns * 0.52))).b; + float intensity = column * (1.15 - v * 0.5) + - (nA * 0.78 + nB * 0.60) * (0.36 + 0.95 * v); + intensity = clamp(intensity * 1.75, 0.0, 1.0); if (intensity > 0.02) { - // Blackbody-style ramp: deep red -> orange -> yellow -> near white. - vec3 flame = mix(vec3(0.45, 0.02, 0.0), vec3(1.0, 0.30, 0.02), - smoothstep(0.02, 0.34, intensity)); - flame = mix(flame, vec3(1.0, 0.72, 0.18), smoothstep(0.34, 0.68, intensity)); - flame = mix(flame, vec3(1.05, 0.98, 0.80), smoothstep(0.68, 0.94, intensity)); - float a = smoothstep(0.04, 0.32, intensity); + // Ramp: deep -> mid -> bright -> hot core (kept small). + vec3 c0 = mix(vec3(0.42, 0.02, 0.0), vec3(0.01, 0.06, 0.42), blue); + vec3 c1 = mix(vec3(1.0, 0.28, 0.02), vec3(0.08, 0.38, 0.95), blue); + vec3 c2 = mix(vec3(1.0, 0.70, 0.16), vec3(0.45, 0.75, 1.0), blue); + vec3 c3 = mix(vec3(1.02, 0.95, 0.72), vec3(0.85, 0.95, 1.05), blue); + vec3 flame = mix(c0, c1, smoothstep(0.02, 0.38, intensity)); + flame = mix(flame, c2, smoothstep(0.38, 0.74, intensity)); + flame = mix(flame, c3, smoothstep(0.80, 0.97, intensity)); + float a = smoothstep(0.03, 0.30, intensity); if (a > fxFlameA) { fxFlameA = a; @@ -364,8 +375,8 @@ void main() // --- cast light on the surroundings --- vec2 g = offs; - g.y += 26.0 * strength; // center the light on the flame body - float radius = 120.0 * strength; + g.y += fh * 0.4; // center the light on the flame body + float radius = 42.0 + 85.0 * strength; float d = length(g); if (d < radius) { @@ -374,32 +385,34 @@ void main() float fi = float(i) * 7.31; float flick = 0.80 + 0.14 * sin(fxT * 9.7 + fi) + 0.06 * sin(fxT * 23.0 + fi * 1.7); - fxGlow += vec3(1.0, 0.45, 0.15) * (fall * 0.75 * strength * flick); + vec3 glowColor = mix(vec3(1.0, 0.45, 0.15), vec3(0.30, 0.55, 1.0), + step(0.5, blue)); + fxGlow += glowColor * (fall * (0.30 + 0.55 * strength) * flick); } } else if (kind < 2.5) // smoke: textured plume replacing the BAM puffs { - float height = 220.0 * strength; + float height = 170.0 * strength; float rise = -offs.y; // px above the source - if (rise > -12.0 && rise < height) + if (rise > -10.0 && rise < height) { float prog = rise / height; float seed = p.x * 0.37 + float(i) * 11.0; // Slow lateral wander that grows with altitude. float sway = (texture2D(uIeeNoiseMap, vec2(seed / 64.0 + fxT * 0.045, prog * 1.7)).a - 0.5) - * (12.0 + 80.0 * prog); - float widthPx = 15.0 + 62.0 * prog; + * (8.0 + 34.0 * prog); + float widthPx = 11.0 + 26.0 * prog; float lateral = (offs.x - sway) / widthPx; - float across = exp(-lateral * lateral * 1.7); + float across = exp(-lateral * lateral * 1.9); // Two scrolling octaves shape the billows. float dA = texture2D(uIeeNoiseMap, - vec2((offs.x + seed) / 110.0, (offs.y + fxT * 46.0) / 110.0)).g; + vec2((offs.x + seed) / 96.0, (offs.y + fxT * 42.0) / 96.0)).g; float dB = texture2D(uIeeNoiseMap, - vec2((offs.x - seed) / 56.0, (offs.y + fxT * 72.0) / 56.0)).r; - float density = clamp(dA * 0.80 + dB * 0.55 - 0.30, 0.0, 1.0); - float along = smoothstep(-12.0, 14.0, rise) * (1.0 - prog); - fxHaze += across * along * density * 1.05 * strength; + vec2((offs.x - seed) / 48.0, (offs.y + fxT * 66.0) / 48.0)).r; + float density = clamp(dA * 0.80 + dB * 0.55 - 0.38, 0.0, 1.0); + float along = smoothstep(-10.0, 12.0, rise) * (1.0 - prog); + fxHaze += across * along * density * 0.62 * strength; } } else if (kind > 3.5 && kind < 4.5) // light: steady soft glow @@ -416,7 +429,7 @@ void main() } } fxShimmer = clamp(fxShimmer, 0.0, 1.0); - fxHaze = clamp(fxHaze, 0.0, 0.9); + fxHaze = clamp(fxHaze, 0.0, 0.62); } vec2 sampleTc = vTc; @@ -544,9 +557,10 @@ void main() float castLight = (0.05 + 0.45 * (1.0 - clamp(sceneLuma * 4.0, 0.0, 1.0))) * hasArt; lin = lin * (vec3(1.0) + fxGlow * 1.5) + fxGlow * castLight; // Smoke veil: moonlit smoke over dark roofs, shadowy over bright - // ground; never over the void. + // ground. Not art-gated — a per-texel luma gate mottles dark stone, + // and smoke drifting over the night sky reads naturally. vec3 hazeTarget = vec3(0.40, 0.40, 0.45) * (0.40 + 0.55 * sceneLuma) + vec3(0.02); - lin = mix(lin, hazeTarget, fxHaze * hasArt); + lin = mix(lin, hazeTarget, fxHaze); // Flame body composites over everything with its own self-glow; // flames are visible against the void (a fire at a cave mouth), so it // is deliberately not art-gated. diff --git a/docs/superpowers/plans/2026-06-11-phase2-water.md b/docs/superpowers/plans/2026-06-11-phase2-water.md index 6e55aad..4b34044 100644 --- a/docs/superpowers/plans/2026-06-11-phase2-water.md +++ b/docs/superpowers/plans/2026-06-11-phase2-water.md @@ -654,3 +654,13 @@ git push https://github.com/TheForgotten69/InfinityEngine-Enhancer.git feat/phas - The one acknowledged unknown is the world-position equation; Task 6 + runbook step 3 exist precisely to measure it, and fixes are uniform-side (no shader redesign). - Type consistency: `pack_area_liquid_texture` returns `AreaCellTexture` (Task 1) consumed in Task 2; `set_area_world_size`/`set_area_scroll_zoom` declared Task 3, called Tasks 2/4; `g_overrideEffectValue` conversion is self-contained in Task 6. ``` + +## Follow-up backlog (post-baseline observations) + +- 2026-07-18 (owner, in-game 2.7.3, v20+ baseline): some water cells/bands + render noticeably MORE TRANSPARENT than the main strongly-opaque water + body — likely the WATER_ALPHA secondary-pass suppression interacting with + cells whose only water contribution came from that pass, or alpha + differences across mask-boundary cells. Water is otherwise still correct. + Needs a dedicated alpha-consistency pass; do not fold it into unrelated + point-effect work. diff --git a/src/iee/game/are_animations.cpp b/src/iee/game/are_animations.cpp index 2a5da23..14d32d8 100644 --- a/src/iee/game/are_animations.cpp +++ b/src/iee/game/are_animations.cpp @@ -240,19 +240,19 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& std::vector points; points.reserve((std::min)(info.animations.size(), kMaxAreaEffectPoints)); + // Flame strength is authored-height / 76px, calibrated against the real + // BAM frame dimensions (FIRE* torch flames ~12x45, FLAM* sconces ~10x30, + // YSFL*/small flames ~7x14, FLMS candle 2x5, fire pits the full body). const auto strengthFor = [](const AreaAnimationInfo& animation) noexcept { switch (animation.kind) { case AreaAnimationKind::Fire: { - // Flame scale by authored family: wall sconces and small flames stay - // small, fire pits and campfires get the full body. const auto resref = animation.resrefView(); - if (resref.starts_with("FLM") || resref.starts_with("FLSM") || resref.starts_with("FIM") || - resref.starts_with("YSFL")) { - return 0.55f; - } - if (resref.starts_with("FLAM")) return 0.7f; - if (resref.starts_with("FPIT")) return 1.15f; - return 1.0f; + if (resref.starts_with("FPIT")) return 1.0f; + if (resref.starts_with("FIRE")) return 0.62f; + if (resref.starts_with("FLAM")) return 0.44f; + if (resref.starts_with("FLM") || resref.starts_with("FLSM")) return 0.30f; + if (resref.starts_with("YSFL") || resref.starts_with("FIM")) return 0.26f; + return 0.5f; } case AreaAnimationKind::Smoke: return 1.0f; @@ -263,6 +263,17 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& } }; + // Authored flame colors survive the replacement: the kind's fractional + // digit carries a palette id the shader decodes (0 = warm, 1 = blue). + const auto kindValueFor = [](const AreaAnimationInfo& animation) noexcept { + float value = static_cast(static_cast(animation.kind)); + if (animation.kind == AreaAnimationKind::Fire && + animation.resrefView().find("BLU") != std::string_view::npos) { + value += 0.1f; + } + return value; + }; + // Fire carries the effect's visual identity; when an area exceeds the // uniform capacity, drop smoke before light before fire. const AreaAnimationKind passes[] = {AreaAnimationKind::Fire, AreaAnimationKind::Light, @@ -272,8 +283,7 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& if (animation.kind != pass || !animation.isShown()) continue; if (points.size() >= kMaxAreaEffectPoints) return points; points.push_back(AreaEffectPoint{static_cast(animation.x), - static_cast(animation.y), - static_cast(static_cast(animation.kind)), + static_cast(animation.y), kindValueFor(animation), strengthFor(animation)}); } } From d24a4a7aad73d220b3b014e9fb6c92fcbe68ce3e Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 18 Jul 2026 03:46:54 +0200 Subject: [PATCH 07/57] Point effects: authored BAM draw-box geometry; overlay art keeps its engine draw Two field-observed defects, both from ignoring the BAM frame tables: - Suppression scope: AM*/AR* per-area overlay images (AM5204C lit-hearth art in AR2616) were suppressed like standalone flames, blacking out the hearth while a synthetic flame burned on the floor anchor. Replacement is now limited to standalone flame/smoke BAM families (should_replace_animation_draw); overlay fires keep the engine draw and contribute cast light only (palette id .2), overlay smoke packs no point. - Placement/size: points now carry the exact authored draw box, extracted from the game BAM frame tables (frame w/h and center offsets; e.g. FLAMBLU2 draws down-right of its anchor at 8x15). Each point uses two vec4 slots (base-center position + height, half-width); flame bodies, plumes, and glows derive from authored pixels instead of guessed scales. --- assets/override/fpSEAM.glsl | 37 ++++++---- src/iee/area_state.cpp | 2 +- src/iee/game/are_animations.cpp | 118 ++++++++++++++++++++++-------- src/iee/game/are_animations.h | 25 ++++++- src/iee/hooks.cpp | 3 +- src/iee/shader_uniform_bridge.cpp | 17 +++-- src/iee/shader_uniform_bridge.h | 4 +- tests/iee_tests.cpp | 55 +++++++++----- 8 files changed, 183 insertions(+), 78 deletions(-) diff --git a/assets/override/fpSEAM.glsl b/assets/override/fpSEAM.glsl index e792c9f..1dc785d 100644 --- a/assets/override/fpSEAM.glsl +++ b/assets/override/fpSEAM.glsl @@ -23,7 +23,10 @@ uniform highp vec2 uIeeViewport; // physical px (w, h) uniform highp vec2 uIeeWorldSizeInv; // 1 / world px uniform highp vec3 uIeeWaterTint; // authored water color (avg of the area's water overlay tile) uniform highp float uIeePointCount; // 0..32 classified ambient-animation points -uniform highp vec4 uIeePoints[32]; // xy=world px, z=kind (1 fire/2 smoke/4 light), w=strength +// Two vec4 slots per point i: [2i] = (baseX, baseY, kind, heightPx) where the +// kind fraction is a palette id (.0 warm body / .1 blue body / .2 glow only); +// [2i+1] = (halfWidthPx, reserved...). +uniform highp vec4 uIeePoints[64]; uniform lowp sampler2D uIeeAreaMask; // unit 2: one liquid mode per 64px WED cell uniform lowp sampler2D uIeeNormalMap; // unit 3: tiling water normal map uniform lowp sampler2D uIeeDudvMap; // unit 4: tiling DuDv distortion map @@ -281,7 +284,7 @@ void main() for (int i = 0; i < 32; ++i) { if (float(i) >= uIeePointCount) { break; } - vec4 p = uIeePoints[i]; + vec4 p = uIeePoints[2 * i]; float d = length(worldPos - p.xy); float m = smoothstep(4.0, 1.5, abs(d - 40.0)) + smoothstep(6.0, 2.0, d); if (m > ring) @@ -317,21 +320,25 @@ void main() for (int i = 0; i < 32; ++i) { if (float(i) >= uIeePointCount) { break; } - vec4 p = uIeePoints[i]; + vec4 p = uIeePoints[2 * i]; + vec4 pb = uIeePoints[2 * i + 1]; vec2 offs = worldPos - p.xy; float kind = p.z; - float strength = p.w; + float strength = clamp(p.w / 76.0, 0.05, 1.4); if (kind < 1.5) // fire: textured flame body + cast light + shimmer { - // Sized from the authored BAM frames: strength = height / 76, - // width follows the authored ~1:3.5 aspect. - float fh = 76.0 * strength; // flame height above the anchor - float fw = 3.5 + 0.16 * fh; // half-width at the base - float blue = fract(kind) * 10.0; // palette id: >0.5 = blue flame + // Authored geometry from the BAM frame table: p.w = height, + // pb.x = half-width, position already at the flame's + // bottom-center. + float fh = max(p.w, 5.0); + float fw = max(pb.x, 1.5); + float palette = fract(kind) * 10.0; // 0 warm / 1 blue / 2 glow-only + float blue = (palette > 0.5 && palette < 1.5) ? 1.0 : 0.0; + bool bodyless = palette > 1.5; // --- flame body (replaces the engine's BAM flame) --- - if (offs.y < 8.0 && offs.y > -fh * 1.3 && abs(offs.x) < fw * 2.6) + if (!bodyless && offs.y < 6.0 && offs.y > -fh * 1.3 && abs(offs.x) < fw * 2.6) { float v = clamp(-offs.y / fh, 0.0, 1.2); // 0 base -> 1 tip float widthAt = fw * (1.05 - 0.60 * min(v, 1.0)); @@ -392,7 +399,7 @@ void main() } else if (kind < 2.5) // smoke: textured plume replacing the BAM puffs { - float height = 170.0 * strength; + float height = max(p.w, 20.0); float rise = -offs.y; // px above the source if (rise > -10.0 && rise < height) { @@ -402,7 +409,7 @@ void main() float sway = (texture2D(uIeeNoiseMap, vec2(seed / 64.0 + fxT * 0.045, prog * 1.7)).a - 0.5) * (8.0 + 34.0 * prog); - float widthPx = 11.0 + 26.0 * prog; + float widthPx = max(pb.x, 6.0) + 26.0 * prog; float lateral = (offs.x - sway) / widthPx; float across = exp(-lateral * lateral * 1.9); // Two scrolling octaves shape the billows. @@ -412,19 +419,19 @@ void main() vec2((offs.x - seed) / 48.0, (offs.y + fxT * 66.0) / 48.0)).r; float density = clamp(dA * 0.80 + dB * 0.55 - 0.38, 0.0, 1.0); float along = smoothstep(-10.0, 12.0, rise) * (1.0 - prog); - fxHaze += across * along * density * 0.62 * strength; + fxHaze += across * along * density * 0.62; } } else if (kind > 3.5 && kind < 4.5) // light: steady soft glow { - float radius = 90.0 * strength; + float radius = max(p.w, 10.0); float d = length(offs); if (d < radius) { float fall = 1.0 - d / radius; fall *= fall; float breathe = 0.92 + 0.08 * sin(fxT * 2.1 + float(i) * 3.3); - fxGlow += vec3(1.0, 0.74, 0.40) * (fall * 0.55 * strength * breathe); + fxGlow += vec3(1.0, 0.74, 0.40) * (fall * 0.40 * breathe); } } } diff --git a/src/iee/area_state.cpp b/src/iee/area_state.cpp index 6e6e1bd..95fc2c8 100644 --- a/src/iee/area_state.cpp +++ b/src/iee/area_state.cpp @@ -200,7 +200,7 @@ void refresh_area_animations(AppContext& ctx, const game::CGameArea* area, return; } ctx.areaAnimations.store(snapshot); - static_assert(sizeof(game::AreaEffectPoint) == 4 * sizeof(float)); + static_assert(sizeof(game::AreaEffectPoint) == 8 * sizeof(float)); probe::set_area_effect_points( effectPoints.empty() ? nullptr : &effectPoints.front().x, effectPoints.size()); } diff --git a/src/iee/game/are_animations.cpp b/src/iee/game/are_animations.cpp index 14d32d8..afd3487 100644 --- a/src/iee/game/are_animations.cpp +++ b/src/iee/game/are_animations.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "file_formats.h" @@ -236,42 +237,101 @@ bool parse_are_animations(const std::byte* data, std::size_t size, return true; } +namespace { +// Authored flame draw geometry, extracted from the game BAM frame tables +// (frame size w/h and center offsets cx/cy; the engine draws the frame's +// top-left at anchor-(cx,cy)). dx/dy move the anchor to the drawn flame's +// bottom-center; height/halfWidth are the authored footprint in world px. +struct FlameGeometry { + std::string_view resref; + float dx; + float dy; + float height; + float halfWidth; +}; +constexpr FlameGeometry kFlameGeometry[] = { + {"FLAMBLU2", 4.0f, 15.0f, 15.0f, 4.0f}, {"FLMS", 0.0f, 3.0f, 5.0f, 1.5f}, + {"FLMSW", 0.0f, 3.0f, 5.0f, 1.5f}, {"FLSM1W", 0.0f, 10.0f, 20.0f, 10.0f}, + {"FLSM2W", 0.0f, 20.0f, 40.0f, 20.0f}, {"FLSM1RED", 9.0f, 19.0f, 20.0f, 10.0f}, + {"FLM1RED1", 0.0f, 4.0f, 20.0f, 10.0f}, {"FLML", 1.0f, 11.0f, 21.0f, 5.0f}, + {"FLMM", 1.0f, 8.0f, 15.0f, 4.0f}, {"YSFLBLU2", 0.0f, 0.0f, 15.0f, 4.0f}, + {"FIM1YLN1", 0.0f, 5.0f, 25.0f, 12.0f}, {"FIM2YLN2", 0.0f, 8.0f, 50.0f, 25.0f}, + {"FIRE", -6.0f, 30.0f, 48.0f, 6.0f}, {"FLAME2S", 0.0f, 3.0f, 12.0f, 6.0f}, + {"FLAME2L", 0.0f, 10.0f, 29.0f, 6.0f}, {"FPIT1S", 0.0f, 13.0f, 24.0f, 14.0f}, + {"FIRE_1", 1.0f, 25.0f, 50.0f, 17.0f}, {"FIRE_4", 0.0f, 15.0f, 27.0f, 7.0f}, +}; +constexpr FlameGeometry kDefaultFlameGeometry{"", 0.0f, 4.0f, 34.0f, 8.0f}; + +const FlameGeometry& flame_geometry_for(std::string_view resref) noexcept { + for (const auto& entry : kFlameGeometry) { + if (entry.resref == resref) return entry; + } + return kDefaultFlameGeometry; +} +} // namespace + +bool should_replace_animation_draw(std::string_view resref, AreaAnimationKind kind) noexcept { + // Only standalone flame/smoke BAM families are replaced; per-area overlay + // art (AM*/AR* hearth and plume images, name-classified unknowns) keeps + // its engine draw. + const auto upper = upper_copy(resref); + if (kind == AreaAnimationKind::Fire) { + return starts_with_any(upper, {"FLAM", "FIRE", "FPIT", "FLM", "FLSM", "YSFL", "FIM", "TORCH", + "BRAZ"}); + } + if (kind == AreaAnimationKind::Smoke) { + return starts_with_any(upper, {"SMOK", "CHIM"}); + } + return false; +} + std::vector build_area_effect_points(const AreaAnimationsInfo& info) { std::vector points; points.reserve((std::min)(info.animations.size(), kMaxAreaEffectPoints)); - // Flame strength is authored-height / 76px, calibrated against the real - // BAM frame dimensions (FIRE* torch flames ~12x45, FLAM* sconces ~10x30, - // YSFL*/small flames ~7x14, FLMS candle 2x5, fire pits the full body). - const auto strengthFor = [](const AreaAnimationInfo& animation) noexcept { + const auto makePoint = [](const AreaAnimationInfo& animation) noexcept + -> std::optional { + AreaEffectPoint point{}; + point.x = static_cast(animation.x); + point.y = static_cast(animation.y); + const float base = static_cast(static_cast(animation.kind)); + const auto resref = animation.resrefView(); switch (animation.kind) { case AreaAnimationKind::Fire: { - const auto resref = animation.resrefView(); - if (resref.starts_with("FPIT")) return 1.0f; - if (resref.starts_with("FIRE")) return 0.62f; - if (resref.starts_with("FLAM")) return 0.44f; - if (resref.starts_with("FLM") || resref.starts_with("FLSM")) return 0.30f; - if (resref.starts_with("YSFL") || resref.starts_with("FIM")) return 0.26f; - return 0.5f; + if (!should_replace_animation_draw(resref, animation.kind)) { + // Overlay art keeps rendering; contribute warm cast light only. + point.kind = base + 0.2f; + point.height = 40.0f; + point.halfWidth = 10.0f; + return point; + } + const auto& geometry = flame_geometry_for(resref); + point.x += geometry.dx; + point.y += geometry.dy; + point.kind = + base + (resref.find("BLU") != std::string_view::npos ? 0.1f : 0.0f); + point.height = geometry.height; + point.halfWidth = geometry.halfWidth; + return point; + } + case AreaAnimationKind::Smoke: { + if (!should_replace_animation_draw(resref, animation.kind)) { + return std::nullopt; // authored plume art stays; nothing to add + } + point.kind = base; + point.height = 170.0f; + point.halfWidth = 11.0f; + return point; + } + case AreaAnimationKind::Light: { + point.kind = base; + point.height = 54.0f; // glow radius + point.halfWidth = 0.0f; + return point; } - case AreaAnimationKind::Smoke: - return 1.0f; - case AreaAnimationKind::Light: - return 0.6f; // candles/glows: smaller, steadier halo default: - return 0.0f; - } - }; - - // Authored flame colors survive the replacement: the kind's fractional - // digit carries a palette id the shader decodes (0 = warm, 1 = blue). - const auto kindValueFor = [](const AreaAnimationInfo& animation) noexcept { - float value = static_cast(static_cast(animation.kind)); - if (animation.kind == AreaAnimationKind::Fire && - animation.resrefView().find("BLU") != std::string_view::npos) { - value += 0.1f; + return std::nullopt; } - return value; }; // Fire carries the effect's visual identity; when an area exceeds the @@ -282,9 +342,7 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& for (const auto& animation : info.animations) { if (animation.kind != pass || !animation.isShown()) continue; if (points.size() >= kMaxAreaEffectPoints) return points; - points.push_back(AreaEffectPoint{static_cast(animation.x), - static_cast(animation.y), kindValueFor(animation), - strengthFor(animation)}); + if (const auto point = makePoint(animation)) points.push_back(*point); } } return points; diff --git a/src/iee/game/are_animations.h b/src/iee/game/are_animations.h index 5126ec1..9f3f840 100644 --- a/src/iee/game/are_animations.h +++ b/src/iee/game/are_animations.h @@ -75,18 +75,35 @@ struct ARE_Animation_st; [[nodiscard]] bool parse_are_animations(const std::byte* data, std::size_t size, AreaAnimationsInfo& out) noexcept; -// One shader point effect: world-pixel position, the AreaAnimationKind as a -// float, and a per-kind strength scale. Matches the uIeePoints vec4 layout. +// One shader point effect, two vec4 uniform slots per point. +// Slot A: base-center world position (anchor corrected by the authored BAM +// draw box), encoded kind, and effect height in world px. The kind's +// fractional digit is a palette id: .0 warm flame body, .1 blue flame body, +// .2 glow-only (the engine keeps drawing the authored art). +// Slot B: half-width in world px; the rest is reserved. struct AreaEffectPoint { float x{}; float y{}; float kind{}; - float strength{}; + float height{}; + float halfWidth{}; + float reserved1{}; + float reserved2{}; + float reserved3{}; }; +static_assert(sizeof(AreaEffectPoint) == 8 * sizeof(float)); -// The fpSEAM override's fixed uniform-array capacity. +// The fpSEAM override's fixed capacity (points; the uniform array holds two +// vec4 slots per point). inline constexpr std::size_t kMaxAreaEffectPoints = 32; +// True when the animation's authored draw is a standalone flame/smoke BAM +// that the shader replaces outright (the CGameStatic::Render hook suppresses +// the engine draw). False for per-area overlay art (hearths, plume images) +// that must keep rendering; fire overlays then contribute glow only. +[[nodiscard]] bool should_replace_animation_draw(std::string_view resref, + AreaAnimationKind kind) noexcept; + // Selects the shown fire/smoke/light animations as shader points, fire first // so the capacity cap drops the least impactful kinds. Returns at most // kMaxAreaEffectPoints entries. diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index 03074f2..2ca9d35 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -292,8 +292,7 @@ static void detour_static_render(void* thisPtr, void* area, void* vidMode) { (header.nFlags & (game::kAreAnimationFlagUseWbm | game::kAreAnimationFlagUsePvrz)) == 0) { const auto info = game::make_area_animation_info(header); - if (info.kind == game::AreaAnimationKind::Fire || - info.kind == game::AreaAnimationKind::Smoke) { + if (game::should_replace_animation_draw(info.resrefView(), info.kind)) { return; // replaced by the shader's point effects } } diff --git a/src/iee/shader_uniform_bridge.cpp b/src/iee/shader_uniform_bridge.cpp index b4cc911..1710d18 100644 --- a/src/iee/shader_uniform_bridge.cpp +++ b/src/iee/shader_uniform_bridge.cpp @@ -35,7 +35,7 @@ std::atomic g_stateRevision{1}; // render-thread feed). The array cannot be atomic; a mutex plus its own // revision keeps the feed's copy coherent and cheap when unchanged. std::mutex g_effectPointsMutex; -std::array g_effectPoints{}; +std::array g_effectPoints{}; std::size_t g_effectPointCount{0}; std::atomic g_effectPointsRevision{1}; std::atomic g_performanceCalls{0}; @@ -157,16 +157,17 @@ void set_effect_points(const float* xyzw, std::size_t count) noexcept { const auto clamped = (std::min)(count, kMaxEffectPoints); std::lock_guard lock(g_effectPointsMutex); if (clamped == g_effectPointCount && - (clamped == 0 || - std::memcmp(g_effectPoints.data(), xyzw, clamped * 4 * sizeof(float)) == 0)) { + (clamped == 0 || std::memcmp(g_effectPoints.data(), xyzw, + clamped * kEffectPointFloats * sizeof(float)) == 0)) { return; } g_effectPointCount = clamped; if (clamped > 0 && xyzw) { - std::memcpy(g_effectPoints.data(), xyzw, clamped * 4 * sizeof(float)); + std::memcpy(g_effectPoints.data(), xyzw, clamped * kEffectPointFloats * sizeof(float)); } - std::fill(g_effectPoints.begin() + static_cast(clamped * 4), - g_effectPoints.end(), 0.0f); + std::fill( + g_effectPoints.begin() + static_cast(clamped * kEffectPointFloats), + g_effectPoints.end(), 0.0f); g_effectPointsRevision.fetch_add(1, std::memory_order_release); advance_state_revision(); } catch (...) { @@ -321,7 +322,7 @@ void feed(unsigned program, Locations& locations) { const auto pointsRevision = g_effectPointsRevision.load(std::memory_order_acquire); if (locations.lastPointsRevision != pointsRevision && (locations.pointCount >= 0 || locations.points >= 0)) { - std::array points{}; + std::array points{}; std::size_t pointCount = 0; { std::lock_guard lock(g_effectPointsMutex); @@ -332,7 +333,7 @@ void feed(unsigned program, Locations& locations) { gl.glUniform1f(locations.pointCount, static_cast(pointCount)); } if (locations.points >= 0 && gl.glUniform4fv) { - gl.glUniform4fv(locations.points, static_cast(kMaxEffectPoints), points.data()); + gl.glUniform4fv(locations.points, static_cast(kMaxEffectPoints * 2), points.data()); } if (locations.lastPointsRevision == 0) { LOG_DEBUG( diff --git a/src/iee/shader_uniform_bridge.h b/src/iee/shader_uniform_bridge.h index 64a6f24..8d26b2c 100644 --- a/src/iee/shader_uniform_bridge.h +++ b/src/iee/shader_uniform_bridge.h @@ -5,8 +5,10 @@ namespace iee::probe::uniforms { -// Matches the fpSEAM override's uIeePoints[] capacity. +// Point capacity; each point occupies two vec4 uniform slots, so the +// fpSEAM override declares uIeePoints[kMaxEffectPoints * 2]. inline constexpr std::size_t kMaxEffectPoints = 32; +inline constexpr std::size_t kEffectPointFloats = 8; struct Locations { static constexpr int kUnresolved = -2; diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index bf58ca4..d5ba838 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1336,9 +1336,10 @@ void test_fpseam_override_asset_contract() { "uIeeDudvMap", "uIeeFoamMap", "uIeeNoiseMap"}) { expect_true(source.find(name) != std::string::npos, "fpSEAM override declares feed uniform"); } - // The uniform-array capacity in the shader must match the bridge/packing cap. - expect_true(source.find("uIeePoints[32]") != std::string::npos, - "fpSEAM point array capacity matches kMaxAreaEffectPoints"); + // The uniform-array capacity in the shader must match the bridge/packing + // cap (two vec4 slots per point). + expect_true(source.find("uIeePoints[64]") != std::string::npos, + "fpSEAM point array capacity matches kMaxAreaEffectPoints * 2"); expect_true(source.find("#version") == std::string::npos, "no #version line (engine sources are ARB-era GLSL)"); expect_true( @@ -1622,35 +1623,55 @@ void test_build_area_effect_points() { using namespace iee::game; AreaAnimationsInfo info{}; - const auto add = [&](AreaAnimationKind kind, std::uint16_t x, bool shown) { + const auto add = [&](AreaAnimationKind kind, const char* resref, std::uint16_t x, bool shown) { AreaAnimationInfo animation{}; animation.kind = kind; animation.x = x; animation.y = 100; animation.flags = shown ? kAreAnimationFlagIsShown : 0; + for (std::size_t c = 0; resref[c] != '\0' && c < 8; ++c) animation.resref[c] = resref[c]; info.animations.push_back(animation); }; - add(AreaAnimationKind::Smoke, 10, true); - add(AreaAnimationKind::Fire, 20, true); - add(AreaAnimationKind::Fire, 30, false); // hidden: excluded - add(AreaAnimationKind::Light, 40, true); - add(AreaAnimationKind::Wildlife, 50, true); // no effect kind: excluded - add(AreaAnimationKind::Water, 60, true); // handled by the water path: excluded + add(AreaAnimationKind::Smoke, "CHIMSMK", 10, true); + add(AreaAnimationKind::Smoke, "AM6004A", 15, true); // authored plume art: no point + add(AreaAnimationKind::Fire, "FIRE_4", 20, true); + add(AreaAnimationKind::Fire, "FLAMBLU2", 25, true); // blue palette + draw-box shift + add(AreaAnimationKind::Fire, "AM5204C", 28, true); // hearth overlay: glow only + add(AreaAnimationKind::Fire, "FIRE_4", 30, false); // hidden: excluded + add(AreaAnimationKind::Light, "FLMS", 40, true); + add(AreaAnimationKind::Wildlife, "FISH3S", 50, true); // no effect kind: excluded + add(AreaAnimationKind::Water, "SPLASH", 60, true); // water path: excluded const auto points = build_area_effect_points(info); - expect_eq(points.size(), std::size_t{3}, "Only shown fire/smoke/light become points"); - expect_true(!points.empty() && points[0].kind == 1.0f && points[0].x == 20.0f, - "Fire points are packed first"); - expect_true(points.size() >= 2 && points[1].kind == 4.0f && points[1].strength < 1.0f, - "Light points follow with a reduced strength"); - expect_true(points.size() >= 3 && points[2].kind == 2.0f, - "Smoke points are packed last"); + expect_eq(points.size(), std::size_t{5}, "Shown fire/light + replaceable smoke become points"); + expect_true(!points.empty() && points[0].kind == 1.0f && points[0].x == 20.0f && + points[0].height == 27.0f && points[0].halfWidth == 7.0f, + "Fire points come first with authored BAM geometry"); + expect_true(points.size() >= 2 && points[1].kind > 1.05f && points[1].kind < 1.15f && + points[1].x == 29.0f && points[1].y == 115.0f, + "Blue flames carry the palette id and the draw-box anchor shift"); + expect_true(points.size() >= 3 && points[2].kind > 1.15f && points[2].kind < 1.25f, + "Overlay fires become glow-only points"); + expect_true(points.size() >= 4 && points[3].kind == 4.0f, + "Light points follow fire"); + expect_true(points.size() >= 5 && points[4].kind == 2.0f && points[4].x == 10.0f, + "Only standalone smoke BAMs become plume points"); AreaAnimationsInfo overflow{}; for (int i = 0; i < 80; ++i) { AreaAnimationInfo animation{}; animation.kind = i < 40 ? AreaAnimationKind::Smoke : AreaAnimationKind::Fire; + animation.resref[0] = 'F'; + animation.resref[1] = 'L'; + animation.resref[2] = 'A'; + animation.resref[3] = 'M'; + if (i < 40) { + animation.resref[0] = 'S'; + animation.resref[1] = 'M'; + animation.resref[2] = 'O'; + animation.resref[3] = 'K'; + } animation.flags = kAreAnimationFlagIsShown; overflow.animations.push_back(animation); } From 07eb7244ef0f8ac4a0783fdfa49b5f50afb4ffd5 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 18 Jul 2026 17:05:05 +0200 Subject: [PATCH 08/57] Point effects: palette id moved to a plain slot-B float; elliptical dappled cast light MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The blue-flame palette rode the kind's fractional digit through the uniform feed; field evidence says it never survived to the shader on the target driver. The palette is now a plain float in the point's second slot (0 warm, 1 blue, 2 glow-only) — no fract decoding anywhere — and every packed point is logged at DEBUG (kind/palette/pos/geometry) so the feed is verifiable from the game log alone. Cast light no longer reads as an airbrushed disc: the footprint is a 1.9:1 ellipse (isometric floor projection), modulated by drifting noise dapple, with reduced multiplicative and additive weights. Also archives the liquid-type sweep findings in the water backlog: lava's opaque art never engages the alpha-hole mask (root cause for "lava not working"), washed-out sewage channels, and the saturated harbor sea — all scoped to the dedicated water pass. --- assets/override/fpSEAM.glsl | 19 ++++++++++++------- .../plans/2026-06-11-phase2-water.md | 17 +++++++++++++++++ src/iee/area_state.cpp | 4 ++++ src/iee/game/are_animations.cpp | 7 ++++--- src/iee/game/are_animations.h | 10 +++++----- tests/iee_tests.cpp | 4 ++-- 6 files changed, 44 insertions(+), 17 deletions(-) diff --git a/assets/override/fpSEAM.glsl b/assets/override/fpSEAM.glsl index 1dc785d..9c89a77 100644 --- a/assets/override/fpSEAM.glsl +++ b/assets/override/fpSEAM.glsl @@ -330,10 +330,10 @@ void main() { // Authored geometry from the BAM frame table: p.w = height, // pb.x = half-width, position already at the flame's - // bottom-center. + // bottom-center; pb.y = palette (0 warm / 1 blue / 2 glow-only). float fh = max(p.w, 5.0); float fw = max(pb.x, 1.5); - float palette = fract(kind) * 10.0; // 0 warm / 1 blue / 2 glow-only + float palette = pb.y; float blue = (palette > 0.5 && palette < 1.5) ? 1.0 : 0.0; bool bodyless = palette > 1.5; @@ -381,9 +381,12 @@ void main() } // --- cast light on the surroundings --- + // Elliptical footprint (isometric floor projection) with + // noise dapple so the pool reads as firelight, not a disc. vec2 g = offs; - g.y += fh * 0.4; // center the light on the flame body - float radius = 42.0 + 85.0 * strength; + g.y += fh * 0.35; + g.y *= 1.9; + float radius = 40.0 + 80.0 * strength; float d = length(g); if (d < radius) { @@ -392,9 +395,11 @@ void main() float fi = float(i) * 7.31; float flick = 0.80 + 0.14 * sin(fxT * 9.7 + fi) + 0.06 * sin(fxT * 23.0 + fi * 1.7); + float dapple = 0.72 + 0.55 * texture2D(uIeeNoiseMap, + (worldPos + vec2(fxT * 5.0, -fxT * 3.0)) / 72.0).r; vec3 glowColor = mix(vec3(1.0, 0.45, 0.15), vec3(0.30, 0.55, 1.0), step(0.5, blue)); - fxGlow += glowColor * (fall * (0.30 + 0.55 * strength) * flick); + fxGlow += glowColor * (fall * (0.20 + 0.38 * strength) * flick * dapple); } } else if (kind < 2.5) // smoke: textured plume replacing the BAM puffs @@ -561,8 +566,8 @@ void main() float hasArt = smoothstep(0.0015, 0.012, sceneLuma); // Night-adaptive cast light: dark scenes get real light, bright day // scenes only a whisper. - float castLight = (0.05 + 0.45 * (1.0 - clamp(sceneLuma * 4.0, 0.0, 1.0))) * hasArt; - lin = lin * (vec3(1.0) + fxGlow * 1.5) + fxGlow * castLight; + float castLight = (0.04 + 0.30 * (1.0 - clamp(sceneLuma * 4.0, 0.0, 1.0))) * hasArt; + lin = lin * (vec3(1.0) + fxGlow * 1.2) + fxGlow * castLight; // Smoke veil: moonlit smoke over dark roofs, shadowy over bright // ground. Not art-gated — a per-texel luma gate mottles dark stone, // and smoke drifting over the night sky reads naturally. diff --git a/docs/superpowers/plans/2026-06-11-phase2-water.md b/docs/superpowers/plans/2026-06-11-phase2-water.md index 4b34044..850ac84 100644 --- a/docs/superpowers/plans/2026-06-11-phase2-water.md +++ b/docs/superpowers/plans/2026-06-11-phase2-water.md @@ -664,3 +664,20 @@ git push https://github.com/TheForgotten69/InfinityEngine-Enhancer.git feat/phas differences across mask-boundary cells. Water is otherwise still correct. Needs a dedicated alpha-consistency pass; do not fold it into unrelated point-effect work. +- 2026-07-18 (owner, liquid-type sweep on 2.7.3): the non-water liquid modes + were exercised for the first time with these findings, all for the same + dedicated water pass: + - LAVA (BG1 AR0508/AR0514): no effect at all. Root cause identified: lava + base art is fully OPAQUE — the `waterMask = (1 - texColor.a)` alpha-hole + contour that gates the whole liquid path never engages. Lava needs a + cellMode-driven emissive treatment of opaque art instead of the + hole-mask path. + - SEWAGE (BG1 AR0224 family): acceptable in the main sewer areas; some + channels render washed-out pale grey/white — authored-tint sampling + likely failed (neutral 0.5 grey fallback) and the neutral grade reads + as paper. Verify tint-candidate coverage for sewage overlays. + - SWAMP/sea overlays (BG1 AR1200 docks etc.): harbor sea renders as flat + saturated cyan-blue "plastic" in daylight; also isolated black wedge + artifacts near authored shadow overlays. Never validated at the v20 + baseline; needs tint/grade calibration per liquid mode, not just the + water defaults. diff --git a/src/iee/area_state.cpp b/src/iee/area_state.cpp index 95fc2c8..d93ba10 100644 --- a/src/iee/area_state.cpp +++ b/src/iee/area_state.cpp @@ -216,6 +216,10 @@ void refresh_area_animations(AppContext& ctx, const game::CGameArea* area, log_area_animation_summary(*snapshot); LOG_INFO("ARE effect points published: {} (pointEffects={})", effectPoints.size(), ctx.cfg.enablePointEffects); + for (const auto& point : effectPoints) { + LOG_DEBUG("ARE effect point: kind={}, palette={}, pos=({}, {}), height={}, halfWidth={}", + point.kind, point.reserved1, point.x, point.y, point.height, point.halfWidth); + } } } catch (const std::exception& e) { LOG_ERROR("ARE animation refresh failed: {}", e.what()); diff --git a/src/iee/game/are_animations.cpp b/src/iee/game/are_animations.cpp index afd3487..b7f7a35 100644 --- a/src/iee/game/are_animations.cpp +++ b/src/iee/game/are_animations.cpp @@ -298,9 +298,10 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& const auto resref = animation.resrefView(); switch (animation.kind) { case AreaAnimationKind::Fire: { + point.kind = base; if (!should_replace_animation_draw(resref, animation.kind)) { // Overlay art keeps rendering; contribute warm cast light only. - point.kind = base + 0.2f; + point.reserved1 = 2.0f; // palette id: glow only point.height = 40.0f; point.halfWidth = 10.0f; return point; @@ -308,8 +309,8 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& const auto& geometry = flame_geometry_for(resref); point.x += geometry.dx; point.y += geometry.dy; - point.kind = - base + (resref.find("BLU") != std::string_view::npos ? 0.1f : 0.0f); + point.reserved1 = + resref.find("BLU") != std::string_view::npos ? 1.0f : 0.0f; // palette id point.height = geometry.height; point.halfWidth = geometry.halfWidth; return point; diff --git a/src/iee/game/are_animations.h b/src/iee/game/are_animations.h index 9f3f840..898943f 100644 --- a/src/iee/game/are_animations.h +++ b/src/iee/game/are_animations.h @@ -77,17 +77,17 @@ struct ARE_Animation_st; // One shader point effect, two vec4 uniform slots per point. // Slot A: base-center world position (anchor corrected by the authored BAM -// draw box), encoded kind, and effect height in world px. The kind's -// fractional digit is a palette id: .0 warm flame body, .1 blue flame body, -// .2 glow-only (the engine keeps drawing the authored art). -// Slot B: half-width in world px; the rest is reserved. +// draw box), the integer kind, and effect height in world px. +// Slot B: half-width in world px, then the palette id (fire only: 0 warm +// body, 1 blue body, 2 glow-only — the engine keeps drawing the authored +// art); the rest is reserved. struct AreaEffectPoint { float x{}; float y{}; float kind{}; float height{}; float halfWidth{}; - float reserved1{}; + float reserved1{}; // palette id for fire points float reserved2{}; float reserved3{}; }; diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index d5ba838..b75c5a2 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1648,10 +1648,10 @@ void test_build_area_effect_points() { expect_true(!points.empty() && points[0].kind == 1.0f && points[0].x == 20.0f && points[0].height == 27.0f && points[0].halfWidth == 7.0f, "Fire points come first with authored BAM geometry"); - expect_true(points.size() >= 2 && points[1].kind > 1.05f && points[1].kind < 1.15f && + expect_true(points.size() >= 2 && points[1].kind == 1.0f && points[1].reserved1 == 1.0f && points[1].x == 29.0f && points[1].y == 115.0f, "Blue flames carry the palette id and the draw-box anchor shift"); - expect_true(points.size() >= 3 && points[2].kind > 1.15f && points[2].kind < 1.25f, + expect_true(points.size() >= 3 && points[2].kind == 1.0f && points[2].reserved1 == 2.0f, "Overlay fires become glow-only points"); expect_true(points.size() >= 4 && points[3].kind == 4.0f, "Light points follow fire"); From 4fe81fb90a1585d02b771684a5cb493b37e2e773 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 18 Jul 2026 17:18:03 +0200 Subject: [PATCH 09/57] Point effects: case-insensitive geometry/palette lookup; hearth glow centered on the art MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Field log confirmed the blue-flame bug: live resrefs keep their authored casing and this install stores "flamblu2" lowercase — the exact-match geometry table and the "BLU" palette check both missed, falling back to default geometry with a warm palette (classification survived because it uppercases internally). Both lookups now run on the uppercased resref, with a lowercase regression test. Glow-only overlay points (hearths) anchor at the art's floor edge; their cast light is now centered well up onto the coals instead of pooling on the floor in front. --- assets/override/fpSEAM.glsl | 4 +++- src/iee/game/are_animations.cpp | 10 ++++++---- tests/iee_tests.cpp | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/assets/override/fpSEAM.glsl b/assets/override/fpSEAM.glsl index 9c89a77..8c5ad89 100644 --- a/assets/override/fpSEAM.glsl +++ b/assets/override/fpSEAM.glsl @@ -384,7 +384,9 @@ void main() // Elliptical footprint (isometric floor projection) with // noise dapple so the pool reads as firelight, not a disc. vec2 g = offs; - g.y += fh * 0.35; + // Glow-only overlays (hearths) anchor at the art's floor + // edge; center their light well up onto the coals. + g.y += fh * (bodyless ? 0.85 : 0.35); g.y *= 1.9; float radius = 40.0 + 80.0 * strength; float d = length(g); diff --git a/src/iee/game/are_animations.cpp b/src/iee/game/are_animations.cpp index b7f7a35..215ead5 100644 --- a/src/iee/game/are_animations.cpp +++ b/src/iee/game/are_animations.cpp @@ -289,7 +289,7 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& std::vector points; points.reserve((std::min)(info.animations.size(), kMaxAreaEffectPoints)); - const auto makePoint = [](const AreaAnimationInfo& animation) noexcept + const auto makePoint = [](const AreaAnimationInfo& animation) -> std::optional { AreaEffectPoint point{}; point.x = static_cast(animation.x); @@ -306,11 +306,13 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& point.halfWidth = 10.0f; return point; } - const auto& geometry = flame_geometry_for(resref); + // Live resrefs keep their authored casing (e.g. "flamblu2"); the + // geometry table and palette match on the uppercased form. + const auto upper = upper_copy(resref); + const auto& geometry = flame_geometry_for(upper); point.x += geometry.dx; point.y += geometry.dy; - point.reserved1 = - resref.find("BLU") != std::string_view::npos ? 1.0f : 0.0f; // palette id + point.reserved1 = upper.find("BLU") != std::string::npos ? 1.0f : 0.0f; // palette id point.height = geometry.height; point.halfWidth = geometry.halfWidth; return point; diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index b75c5a2..b5749ba 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1636,7 +1636,7 @@ void test_build_area_effect_points() { add(AreaAnimationKind::Smoke, "CHIMSMK", 10, true); add(AreaAnimationKind::Smoke, "AM6004A", 15, true); // authored plume art: no point add(AreaAnimationKind::Fire, "FIRE_4", 20, true); - add(AreaAnimationKind::Fire, "FLAMBLU2", 25, true); // blue palette + draw-box shift + add(AreaAnimationKind::Fire, "flamblu2", 25, true); // live lowercase resref: blue + shift add(AreaAnimationKind::Fire, "AM5204C", 28, true); // hearth overlay: glow only add(AreaAnimationKind::Fire, "FIRE_4", 30, false); // hidden: excluded add(AreaAnimationKind::Light, "FLMS", 40, true); From 7427b4a38833c5a3e294674c5b1317b512271cc1 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 18 Jul 2026 17:26:48 +0200 Subject: [PATCH 10/57] Point effects: anchor is the flame base; remove heat-shimmer UV distortion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Placement evidence across rounds (ALIGN markers land on the sconce bowls; frame-box-derived offsets rendered flames below them) says the ARE anchor is already the flame's base point — the per-family dx/dy shifts are gone and only the authored footprint varies. The heat shimmer offset the ATLAS texture coordinate; crossing a tile boundary sampled unrelated atlas content, which showed as a vertical smear-seam ("compressed map"). Shimmer is removed from the tile pass — world-space distortion belongs to a future post pass. --- assets/override/fpSEAM.glsl | 28 ++++++++-------------------- src/iee/game/are_animations.cpp | 29 +++++++++++++++-------------- tests/iee_tests.cpp | 5 +++-- 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/assets/override/fpSEAM.glsl b/assets/override/fpSEAM.glsl index 8c5ad89..e2161bb 100644 --- a/assets/override/fpSEAM.glsl +++ b/assets/override/fpSEAM.glsl @@ -304,15 +304,12 @@ void main() return; } - // Ambient point effects: computed before sampling so heat shimmer can - // ride the one seamSample call as a texture-coordinate offset. Base pass - // only; the WATER_ALPHA secondary pass and fades stay vanilla. Kept as - // flat locals (no struct/function) for maximum GLSL-frontend - // compatibility. + // Ambient point effects. Base pass only; the WATER_ALPHA secondary pass + // and fades stay vanilla. Kept as flat locals (no struct/function) for + // maximum GLSL-frontend compatibility. vec3 fxGlow = vec3(0.0); vec3 fxFlame = vec3(0.0); float fxFlameA = 0.0; - float fxShimmer = 0.0; float fxHaze = 0.0; if (uIeeEnabled > 0.5 && vColor.a > 0.9 && uIeePointCount > 0.5) { @@ -377,7 +374,6 @@ void main() fxFlame = flame; } } - fxShimmer += column * 0.5 * strength; } // --- cast light on the surroundings --- @@ -442,22 +438,14 @@ void main() } } } - fxShimmer = clamp(fxShimmer, 0.0, 1.0); fxHaze = clamp(fxHaze, 0.0, 0.62); } - vec2 sampleTc = vTc; - if (fxShimmer > 0.01) - { - // Small wavering refraction above flames; uTcScale converts the - // world-pixel amplitude into atlas units, so the offset stays within - // the seam handling's tolerance. - vec2 wobble = vec2(sin(uIeeTime * 13.0 + worldPos.y * 0.35), - cos(uIeeTime * 11.0 + worldPos.x * 0.30)); - sampleTc += wobble * fxShimmer * 1.4 * uTcScale; - } - - vec4 texColor = seamSample(sampleTc); + // No heat-shimmer UV distortion here: offsetting the atlas coordinate + // can cross a tile boundary and sample unrelated atlas content (a + // vertical smear-seam in the art). Shimmer belongs to a future + // world-space pass, not the tile pass. + vec4 texColor = seamSample(vTc); // Inside flagged cells the engine draws the // base tile with TRANSPARENT pixels exactly where water composites diff --git a/src/iee/game/are_animations.cpp b/src/iee/game/are_animations.cpp index 215ead5..a19b6aa 100644 --- a/src/iee/game/are_animations.cpp +++ b/src/iee/game/are_animations.cpp @@ -238,10 +238,11 @@ bool parse_are_animations(const std::byte* data, std::size_t size, } namespace { -// Authored flame draw geometry, extracted from the game BAM frame tables -// (frame size w/h and center offsets cx/cy; the engine draws the frame's -// top-left at anchor-(cx,cy)). dx/dy move the anchor to the drawn flame's -// bottom-center; height/halfWidth are the authored footprint in world px. +// Authored flame sizes, extracted from the game BAM frame tables. Placement +// evidence (ALIGN markers land on the sconce bowls; frame-box-derived +// offsets rendered flames below the bowls) says the ARE anchor is already +// the flame's base point, so dx/dy stay minimal and only the authored +// footprint varies per family. struct FlameGeometry { std::string_view resref; float dx; @@ -250,17 +251,17 @@ struct FlameGeometry { float halfWidth; }; constexpr FlameGeometry kFlameGeometry[] = { - {"FLAMBLU2", 4.0f, 15.0f, 15.0f, 4.0f}, {"FLMS", 0.0f, 3.0f, 5.0f, 1.5f}, - {"FLMSW", 0.0f, 3.0f, 5.0f, 1.5f}, {"FLSM1W", 0.0f, 10.0f, 20.0f, 10.0f}, - {"FLSM2W", 0.0f, 20.0f, 40.0f, 20.0f}, {"FLSM1RED", 9.0f, 19.0f, 20.0f, 10.0f}, - {"FLM1RED1", 0.0f, 4.0f, 20.0f, 10.0f}, {"FLML", 1.0f, 11.0f, 21.0f, 5.0f}, - {"FLMM", 1.0f, 8.0f, 15.0f, 4.0f}, {"YSFLBLU2", 0.0f, 0.0f, 15.0f, 4.0f}, - {"FIM1YLN1", 0.0f, 5.0f, 25.0f, 12.0f}, {"FIM2YLN2", 0.0f, 8.0f, 50.0f, 25.0f}, - {"FIRE", -6.0f, 30.0f, 48.0f, 6.0f}, {"FLAME2S", 0.0f, 3.0f, 12.0f, 6.0f}, - {"FLAME2L", 0.0f, 10.0f, 29.0f, 6.0f}, {"FPIT1S", 0.0f, 13.0f, 24.0f, 14.0f}, - {"FIRE_1", 1.0f, 25.0f, 50.0f, 17.0f}, {"FIRE_4", 0.0f, 15.0f, 27.0f, 7.0f}, + {"FLAMBLU2", 0.0f, 2.0f, 15.0f, 4.0f}, {"FLMS", 0.0f, 2.0f, 5.0f, 1.5f}, + {"FLMSW", 0.0f, 2.0f, 5.0f, 1.5f}, {"FLSM1W", 0.0f, 2.0f, 20.0f, 10.0f}, + {"FLSM2W", 0.0f, 2.0f, 40.0f, 20.0f}, {"FLSM1RED", 0.0f, 2.0f, 20.0f, 10.0f}, + {"FLM1RED1", 0.0f, 2.0f, 20.0f, 10.0f}, {"FLML", 0.0f, 2.0f, 21.0f, 5.0f}, + {"FLMM", 0.0f, 2.0f, 15.0f, 4.0f}, {"YSFLBLU2", 0.0f, 2.0f, 15.0f, 4.0f}, + {"FIM1YLN1", 0.0f, 2.0f, 25.0f, 12.0f}, {"FIM2YLN2", 0.0f, 2.0f, 50.0f, 25.0f}, + {"FIRE", 0.0f, 2.0f, 48.0f, 6.0f}, {"FLAME2S", 0.0f, 2.0f, 12.0f, 6.0f}, + {"FLAME2L", 0.0f, 2.0f, 29.0f, 6.0f}, {"FPIT1S", 0.0f, 2.0f, 24.0f, 14.0f}, + {"FIRE_1", 0.0f, 2.0f, 50.0f, 17.0f}, {"FIRE_4", 0.0f, 2.0f, 27.0f, 7.0f}, }; -constexpr FlameGeometry kDefaultFlameGeometry{"", 0.0f, 4.0f, 34.0f, 8.0f}; +constexpr FlameGeometry kDefaultFlameGeometry{"", 0.0f, 2.0f, 34.0f, 8.0f}; const FlameGeometry& flame_geometry_for(std::string_view resref) noexcept { for (const auto& entry : kFlameGeometry) { diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index b5749ba..35f4736 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1649,8 +1649,9 @@ void test_build_area_effect_points() { points[0].height == 27.0f && points[0].halfWidth == 7.0f, "Fire points come first with authored BAM geometry"); expect_true(points.size() >= 2 && points[1].kind == 1.0f && points[1].reserved1 == 1.0f && - points[1].x == 29.0f && points[1].y == 115.0f, - "Blue flames carry the palette id and the draw-box anchor shift"); + points[1].x == 25.0f && points[1].y == 102.0f && + points[1].height == 15.0f && points[1].halfWidth == 4.0f, + "Blue flames carry the palette id and the authored footprint at the anchor"); expect_true(points.size() >= 3 && points[2].kind == 1.0f && points[2].reserved1 == 2.0f, "Overlay fires become glow-only points"); expect_true(points.size() >= 4 && points[3].kind == 4.0f, From 12b6d4b519ddd2edc30d48aca56e240206087cd6 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 18 Jul 2026 22:29:17 +0200 Subject: [PATCH 11/57] Point effects: render from CGameObject::m_pos with the authored frame-box offsets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CGameStatic::RenderBam ground truth (decompilation): the engine draws the current frame's top-left at (CGameObject::m_pos - frameCenter) — the BAM's embedded per-frame centers ARE applied, and the render base is the live object position, not the ARE header coordinates (which can diverge; a chimney showed header y 163px off its authored value). The walk now reads m_pos, packing places the flame at the frame box's bottom-center relative to it, and the per-animation debug line logs both positions so any divergence stays visible in the field log. --- src/iee/area_state.cpp | 6 +++--- src/iee/game/are_animations.cpp | 38 ++++++++++++++++++--------------- src/iee/game/are_animations.h | 5 +++++ src/iee/game/object_statics.cpp | 10 ++++++++- tests/iee_tests.cpp | 6 ++++-- 5 files changed, 42 insertions(+), 23 deletions(-) diff --git a/src/iee/area_state.cpp b/src/iee/area_state.cpp index d93ba10..dbacdbf 100644 --- a/src/iee/area_state.cpp +++ b/src/iee/area_state.cpp @@ -102,11 +102,11 @@ void log_area_animation_summary(const game::AreaAnimationsInfo& info) { std::size_t unclassifiedListed = 0; for (const auto& animation : info.animations) { if (animation.kind != game::AreaAnimationKind::None) { - LOG_DEBUG("ARE animation {}: kind={}, resref={}, name=\"{}\", pos=({}, {}), shown={}, " - "lightSource={}", + LOG_DEBUG("ARE animation {}: kind={}, resref={}, name=\"{}\", pos=({}, {}), objPos=({}, " + "{}), shown={}, lightSource={}", info.areaResrefView(), game::area_animation_kind_name(animation.kind), animation.resrefView(), animation.nameView(), animation.x, animation.y, - animation.isShown(), animation.isLightSource()); + animation.objX, animation.objY, animation.isShown(), animation.isLightSource()); continue; } if (unclassifiedListed < kMaxUnclassifiedResrefsLogged && !animation.resrefView().empty() && diff --git a/src/iee/game/are_animations.cpp b/src/iee/game/are_animations.cpp index a19b6aa..86c093e 100644 --- a/src/iee/game/are_animations.cpp +++ b/src/iee/game/are_animations.cpp @@ -238,11 +238,11 @@ bool parse_are_animations(const std::byte* data, std::size_t size, } namespace { -// Authored flame sizes, extracted from the game BAM frame tables. Placement -// evidence (ALIGN markers land on the sconce bowls; frame-box-derived -// offsets rendered flames below the bowls) says the ARE anchor is already -// the flame's base point, so dx/dy stay minimal and only the authored -// footprint varies per family. +// Authored flame draw geometry from the game BAM frame tables. RenderBam +// draws the current frame's top-left at (CGameObject::m_pos - frameCenter), +// so relative to m_pos the flame's bottom-center sits at +// (w/2 - cx, h - cy) — dx/dy below. height/halfWidth are the authored +// footprint in world px. struct FlameGeometry { std::string_view resref; float dx; @@ -251,17 +251,17 @@ struct FlameGeometry { float halfWidth; }; constexpr FlameGeometry kFlameGeometry[] = { - {"FLAMBLU2", 0.0f, 2.0f, 15.0f, 4.0f}, {"FLMS", 0.0f, 2.0f, 5.0f, 1.5f}, - {"FLMSW", 0.0f, 2.0f, 5.0f, 1.5f}, {"FLSM1W", 0.0f, 2.0f, 20.0f, 10.0f}, - {"FLSM2W", 0.0f, 2.0f, 40.0f, 20.0f}, {"FLSM1RED", 0.0f, 2.0f, 20.0f, 10.0f}, - {"FLM1RED1", 0.0f, 2.0f, 20.0f, 10.0f}, {"FLML", 0.0f, 2.0f, 21.0f, 5.0f}, - {"FLMM", 0.0f, 2.0f, 15.0f, 4.0f}, {"YSFLBLU2", 0.0f, 2.0f, 15.0f, 4.0f}, - {"FIM1YLN1", 0.0f, 2.0f, 25.0f, 12.0f}, {"FIM2YLN2", 0.0f, 2.0f, 50.0f, 25.0f}, - {"FIRE", 0.0f, 2.0f, 48.0f, 6.0f}, {"FLAME2S", 0.0f, 2.0f, 12.0f, 6.0f}, - {"FLAME2L", 0.0f, 2.0f, 29.0f, 6.0f}, {"FPIT1S", 0.0f, 2.0f, 24.0f, 14.0f}, - {"FIRE_1", 0.0f, 2.0f, 50.0f, 17.0f}, {"FIRE_4", 0.0f, 2.0f, 27.0f, 7.0f}, + {"FLAMBLU2", 4.0f, 15.0f, 15.0f, 4.0f}, {"FLMS", 0.0f, 3.0f, 5.0f, 1.5f}, + {"FLMSW", 0.0f, 3.0f, 5.0f, 1.5f}, {"FLSM1W", 0.0f, 10.0f, 20.0f, 10.0f}, + {"FLSM2W", 0.0f, 20.0f, 40.0f, 20.0f}, {"FLSM1RED", 9.0f, 19.0f, 20.0f, 10.0f}, + {"FLM1RED1", 0.0f, 4.0f, 20.0f, 10.0f}, {"FLML", 1.0f, 11.0f, 21.0f, 5.0f}, + {"FLMM", 1.0f, 8.0f, 15.0f, 4.0f}, {"YSFLBLU2", 0.0f, 0.0f, 15.0f, 4.0f}, + {"FIM1YLN1", 0.0f, 5.0f, 25.0f, 12.0f}, {"FIM2YLN2", 0.0f, 8.0f, 50.0f, 25.0f}, + {"FIRE", -6.0f, 30.0f, 48.0f, 6.0f}, {"FLAME2S", 0.0f, 3.0f, 12.0f, 6.0f}, + {"FLAME2L", 0.0f, 10.0f, 29.0f, 6.0f}, {"FPIT1S", 0.0f, 15.0f, 24.0f, 14.0f}, + {"FIRE_1", 1.0f, 25.0f, 50.0f, 17.0f}, {"FIRE_4", 0.0f, 15.0f, 27.0f, 7.0f}, }; -constexpr FlameGeometry kDefaultFlameGeometry{"", 0.0f, 2.0f, 34.0f, 8.0f}; +constexpr FlameGeometry kDefaultFlameGeometry{"", 0.0f, 4.0f, 34.0f, 8.0f}; const FlameGeometry& flame_geometry_for(std::string_view resref) noexcept { for (const auto& entry : kFlameGeometry) { @@ -293,8 +293,10 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& const auto makePoint = [](const AreaAnimationInfo& animation) -> std::optional { AreaEffectPoint point{}; - point.x = static_cast(animation.x); - point.y = static_cast(animation.y); + // The engine renders from the live object position, not the header + // coordinates; both are logged so a divergence stays visible. + point.x = static_cast(animation.objX); + point.y = static_cast(animation.objY); const float base = static_cast(static_cast(animation.kind)); const auto resref = animation.resrefView(); switch (animation.kind) { @@ -356,6 +358,8 @@ AreaAnimationInfo make_area_animation_info(const ARE_Animation_st& record) noexc AreaAnimationInfo animation{}; animation.x = record.nX; animation.y = record.nY; + animation.objX = record.nX; + animation.objY = record.nY; animation.height = record.nHeight; animation.schedule = record.nSchedule; animation.flags = record.nFlags; diff --git a/src/iee/game/are_animations.h b/src/iee/game/are_animations.h index 898943f..5748161 100644 --- a/src/iee/game/are_animations.h +++ b/src/iee/game/are_animations.h @@ -34,6 +34,11 @@ enum class AreaAnimationKind : int { struct AreaAnimationInfo { std::uint16_t x{}; std::uint16_t y{}; + // Live CGameObject::m_pos — the position RenderBam actually draws from + // (can differ from the header x/y). Filled by the memory walk; the disk + // parser mirrors x/y. + std::int32_t objX{}; + std::int32_t objY{}; std::int16_t height{}; std::uint32_t schedule{}; std::uint32_t flags{}; diff --git a/src/iee/game/object_statics.cpp b/src/iee/game/object_statics.cpp index 40d8a88..bac5a21 100644 --- a/src/iee/game/object_statics.cpp +++ b/src/iee/game/object_statics.cpp @@ -97,7 +97,15 @@ bool collect_area_static_animations(const ObjectArrayGlobals& globals, const CGa if (!core::safe_read(objectBytes + offsetof(CGameStatic, m_header), record)) { continue; } - out.animations.push_back(make_area_animation_info(record)); + auto info = make_area_animation_info(record); + // RenderBam draws from the live CGameObject position, not the header + // coordinates; prefer it when readable. + CPoint objectPos{}; + if (core::safe_read(objectBytes + offsetof(CGameObject, m_pos), objectPos)) { + info.objX = objectPos.x; + info.objY = objectPos.y; + } + out.animations.push_back(info); if (out.animations.size() >= kMaxAreaAnimationRecords) { break; } diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 35f4736..0d0fee1 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1628,6 +1628,8 @@ void test_build_area_effect_points() { animation.kind = kind; animation.x = x; animation.y = 100; + animation.objX = x; + animation.objY = 100; animation.flags = shown ? kAreAnimationFlagIsShown : 0; for (std::size_t c = 0; resref[c] != '\0' && c < 8; ++c) animation.resref[c] = resref[c]; info.animations.push_back(animation); @@ -1649,9 +1651,9 @@ void test_build_area_effect_points() { points[0].height == 27.0f && points[0].halfWidth == 7.0f, "Fire points come first with authored BAM geometry"); expect_true(points.size() >= 2 && points[1].kind == 1.0f && points[1].reserved1 == 1.0f && - points[1].x == 25.0f && points[1].y == 102.0f && + points[1].x == 29.0f && points[1].y == 115.0f && points[1].height == 15.0f && points[1].halfWidth == 4.0f, - "Blue flames carry the palette id and the authored footprint at the anchor"); + "Blue flames carry the palette id and the frame-box offset from the object pos"); expect_true(points.size() >= 3 && points[2].kind == 1.0f && points[2].reserved1 == 2.0f, "Overlay fires become glow-only points"); expect_true(points.size() >= 4 && points[3].kind == 4.0f, From 5ce679c8aa1d59d37104be4981da3ca68636644e Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 18 Jul 2026 22:46:01 +0200 Subject: [PATCH 12/57] Point effects: engine-native flame geometry from CVidCell::m_pFrame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-resref geometry table is now a fallback only. The walk mirrors the engine's own cached current-frame entry (CVidCell::m_pFrame at CGameStatic +0xB0+0x128, the exact frameTableEntry RenderBam draws with), so any flame BAM — including mods — places and sizes itself from the same numbers the engine uses: frame top-left at (m_pos - center), flame at the box's bottom-center. The fallback covers only the first refresh before an object has rendered once and the disk-parse path. The per-animation debug line now prints the live frame geometry, and CVidCell/CGameStatic layouts carry static asserts for the new fields. --- src/iee/area_state.cpp | 6 ++++-- src/iee/game/are_animations.cpp | 16 ++++++++++++++- src/iee/game/are_animations.h | 8 ++++++++ src/iee/game/object_statics.cpp | 17 ++++++++++++++++ src/iee/game/runtime_types_x64.h | 10 ++++++++-- tests/iee_tests.cpp | 34 ++++++++++++++++++++++++++++++++ 6 files changed, 86 insertions(+), 5 deletions(-) diff --git a/src/iee/area_state.cpp b/src/iee/area_state.cpp index dbacdbf..0e85972 100644 --- a/src/iee/area_state.cpp +++ b/src/iee/area_state.cpp @@ -103,10 +103,12 @@ void log_area_animation_summary(const game::AreaAnimationsInfo& info) { for (const auto& animation : info.animations) { if (animation.kind != game::AreaAnimationKind::None) { LOG_DEBUG("ARE animation {}: kind={}, resref={}, name=\"{}\", pos=({}, {}), objPos=({}, " - "{}), shown={}, lightSource={}", + "{}), frame=({}x{} c{},{} valid={}), shown={}, lightSource={}", info.areaResrefView(), game::area_animation_kind_name(animation.kind), animation.resrefView(), animation.nameView(), animation.x, animation.y, - animation.objX, animation.objY, animation.isShown(), animation.isLightSource()); + animation.objX, animation.objY, animation.frameWidth, animation.frameHeight, + animation.frameCenterX, animation.frameCenterY, animation.frameValid, + animation.isShown(), animation.isLightSource()); continue; } if (unclassifiedListed < kMaxUnclassifiedResrefsLogged && !animation.resrefView().empty() && diff --git a/src/iee/game/are_animations.cpp b/src/iee/game/are_animations.cpp index 86c093e..b064d89 100644 --- a/src/iee/game/are_animations.cpp +++ b/src/iee/game/are_animations.cpp @@ -312,10 +312,24 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& // Live resrefs keep their authored casing (e.g. "flamblu2"); the // geometry table and palette match on the uppercased form. const auto upper = upper_copy(resref); + point.reserved1 = upper.find("BLU") != std::string::npos ? 1.0f : 0.0f; // palette id + if (animation.frameValid) { + // Engine-native geometry: RenderBam draws the frame's top-left at + // (m_pos - center); the flame's bottom-center follows. + point.x += static_cast(animation.frameWidth) / 2.0f - + static_cast(animation.frameCenterX); + point.y += static_cast(animation.frameHeight) - + static_cast(animation.frameCenterY); + point.height = static_cast(animation.frameHeight); + point.halfWidth = + (std::max)(static_cast(animation.frameWidth) / 2.0f, 1.5f); + return point; + } + // Fallback for the first refresh before the object rendered once + // (CVidCell::m_pFrame still null) and for the disk parser. const auto& geometry = flame_geometry_for(upper); point.x += geometry.dx; point.y += geometry.dy; - point.reserved1 = upper.find("BLU") != std::string::npos ? 1.0f : 0.0f; // palette id point.height = geometry.height; point.halfWidth = geometry.halfWidth; return point; diff --git a/src/iee/game/are_animations.h b/src/iee/game/are_animations.h index 5748161..87bc634 100644 --- a/src/iee/game/are_animations.h +++ b/src/iee/game/are_animations.h @@ -39,6 +39,14 @@ struct AreaAnimationInfo { // parser mirrors x/y. std::int32_t objX{}; std::int32_t objY{}; + // The engine's cached current BAM frame entry (CVidCell::m_pFrame) — the + // exact geometry RenderBam draws with. Valid only when the memory walk + // could read it (the object has rendered at least once). + bool frameValid{}; + std::int16_t frameWidth{}; + std::int16_t frameHeight{}; + std::int16_t frameCenterX{}; + std::int16_t frameCenterY{}; std::int16_t height{}; std::uint32_t schedule{}; std::uint32_t flags{}; diff --git a/src/iee/game/object_statics.cpp b/src/iee/game/object_statics.cpp index bac5a21..cec8769 100644 --- a/src/iee/game/object_statics.cpp +++ b/src/iee/game/object_statics.cpp @@ -105,6 +105,23 @@ bool collect_area_static_animations(const ObjectArrayGlobals& globals, const CGa info.objX = objectPos.x; info.objY = objectPos.y; } + // The engine caches the current frame entry it renders with + // (CVidCell::m_pFrame); mirror its geometry so no per-resref table is + // needed. Null until the object has rendered once — the render-thread + // re-refresh after a transition sees it populated. + void* framePointer = nullptr; + frameTableEntry_st frame{}; + if (core::safe_read( + objectBytes + offsetof(CGameStatic, m_vidCell) + offsetof(CVidCell, m_pFrame), + framePointer) && + framePointer && core::safe_read(framePointer, frame) && frame.nWidth > 0 && + frame.nHeight > 0) { + info.frameValid = true; + info.frameWidth = static_cast(frame.nWidth); + info.frameHeight = static_cast(frame.nHeight); + info.frameCenterX = frame.nCenterX; + info.frameCenterY = frame.nCenterY; + } out.animations.push_back(info); if (out.animations.size() >= kMaxAreaAnimationRecords) { break; diff --git a/src/iee/game/runtime_types_x64.h b/src/iee/game/runtime_types_x64.h index af506e0..86a2010 100644 --- a/src/iee/game/runtime_types_x64.h +++ b/src/iee/game/runtime_types_x64.h @@ -415,11 +415,14 @@ namespace iee::game { // Runtime object created from each ARE animation record. The engine keeps // the raw authored 76-byte record (CAreaFileStaticObject in the PDB, our // ARE_Animation_st) embedded at +0x60; script state (e.g. the shown flag - // toggled by StaticStart) mutates it in place. + // toggled by StaticStart) mutates it in place. m_vidCell caches the + // current BAM frame entry (m_pFrame) that RenderBam draws with. struct CGameStatic { CGameObject baseclass_0{}; ARE_Animation_st m_header{}; - std::array _tail{}; + std::byte _pad0[4]{}; + CVidCell m_vidCell{}; + std::array _tail{}; }; // CGameObjectArray's static entry table: object id in the low word, @@ -538,6 +541,9 @@ namespace iee::game { static_assert(offsetof(CGameObject, m_pArea) == 0x18); static_assert(sizeof(CGameObject) == 0x60); static_assert(offsetof(CGameStatic, m_header) == 0x60); + static_assert(offsetof(CGameStatic, m_vidCell) == 0xB0); + static_assert(offsetof(CVidCell, m_pFrame) == 0x128); + static_assert(sizeof(CVidCell) == 0x138); static_assert(sizeof(CGameStatic) == 0x368); static_assert(sizeof(CGameObjectArrayEntry) == 0x10); static_assert(offsetof(CGameObjectArrayEntry, m_objectPtr) == 0x8); diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 0d0fee1..cb788d8 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1573,8 +1573,15 @@ void test_collect_area_static_animations() { CGameArea areaB{}; std::array statics{}; + frameTableEntry_st liveFrame{}; + liveFrame.nWidth = 12; + liveFrame.nHeight = 40; + liveFrame.nCenterX = 6; + liveFrame.nCenterY = 30; + statics[0].baseclass_0.m_objectType = kGameObjectTypeStatic; statics[0].baseclass_0.m_pArea = &areaA; + statics[0].m_vidCell.m_pFrame = &liveFrame; statics[0].m_header.rrAnimation = {'F', 'L', 'A', 'M', 'B', 'I', 'G', 0}; const char fireName[] = "FLAMBIG"; std::memcpy(statics[0].m_header.szName.data(), fireName, sizeof(fireName) - 1); @@ -1609,6 +1616,10 @@ void test_collect_area_static_animations() { expect_true(!out.animations.empty() && out.animations[0].x == 320 && out.animations[0].isShown(), "Collected records should carry the live header fields"); + expect_true(!out.animations.empty() && out.animations[0].frameValid && + out.animations[0].frameWidth == 12 && out.animations[0].frameHeight == 40 && + out.animations[0].frameCenterX == 6 && out.animations[0].frameCenterY == 30, + "The walk mirrors the engine's cached CVidCell frame geometry"); AreaAnimationsInfo invalidOut{}; expect_true(!collect_area_static_animations(ObjectArrayGlobals{}, &areaA, invalidOut), @@ -1645,6 +1656,29 @@ void test_build_area_effect_points() { add(AreaAnimationKind::Wildlife, "FISH3S", 50, true); // no effect kind: excluded add(AreaAnimationKind::Water, "SPLASH", 60, true); // water path: excluded + // Engine-native geometry wins when the walk read the live frame entry. + { + AreaAnimationsInfo live{}; + AreaAnimationInfo animation{}; + animation.kind = AreaAnimationKind::Fire; + animation.objX = 100; + animation.objY = 200; + animation.flags = kAreAnimationFlagIsShown; + const char liveResref[] = "flamblu2"; + for (std::size_t c = 0; liveResref[c] != '\0'; ++c) animation.resref[c] = liveResref[c]; + animation.frameValid = true; + animation.frameWidth = 8; + animation.frameHeight = 15; + animation.frameCenterX = 0; + animation.frameCenterY = 0; + live.animations.push_back(animation); + const auto livePoints = build_area_effect_points(live); + expect_true(livePoints.size() == 1 && livePoints[0].x == 104.0f && + livePoints[0].y == 215.0f && livePoints[0].height == 15.0f && + livePoints[0].halfWidth == 4.0f && livePoints[0].reserved1 == 1.0f, + "Live CVidCell frame geometry overrides the fallback table"); + } + const auto points = build_area_effect_points(info); expect_eq(points.size(), std::size_t{5}, "Shown fire/light + replaceable smoke become points"); expect_true(!points.empty() && points[0].kind == 1.0f && points[0].x == 20.0f && From 6707f864b26a1b839f44b9a3e761f0a470dded64 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 18 Jul 2026 23:14:37 +0200 Subject: [PATCH 13/57] Point effects: anchor flame at the object position, no positional offset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ARE object position is already the flame's on-screen base — ALIGN markers at raw objPos land on the sconce bowls, so every per-family dx/dy offset was the source of the persistent shift. Placement now uses objPos directly; only the authored size varies per family. Also records the finding that CVidCell::m_pFrame stays null for replaced flames: the Render-suppression hook returns before RenderBam populates the frame cache (chicken-and-egg), so the size table is the normal path and the live-frame read is an opportunistic upgrade only. --- src/iee/game/are_animations.cpp | 27 +++++++++++---------------- tests/iee_tests.cpp | 10 +++++----- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/iee/game/are_animations.cpp b/src/iee/game/are_animations.cpp index b064d89..99d87ec 100644 --- a/src/iee/game/are_animations.cpp +++ b/src/iee/game/are_animations.cpp @@ -309,29 +309,24 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& point.halfWidth = 10.0f; return point; } - // Live resrefs keep their authored casing (e.g. "flamblu2"); the - // geometry table and palette match on the uppercased form. + // The ARE object position is already the flame's base point on screen + // (ALIGN markers at raw objPos land exactly on the sconce bowls) — so + // no positional offset is applied; the flame body grows upward from + // the anchor. Only the authored SIZE varies per family. Live frame + // geometry (when present) gives size directly; otherwise the table. + // Note: while the effect suppresses the engine draw, CVidCell::m_pFrame + // stays null, so the table is the normal path for replaced flames. const auto upper = upper_copy(resref); point.reserved1 = upper.find("BLU") != std::string::npos ? 1.0f : 0.0f; // palette id if (animation.frameValid) { - // Engine-native geometry: RenderBam draws the frame's top-left at - // (m_pos - center); the flame's bottom-center follows. - point.x += static_cast(animation.frameWidth) / 2.0f - - static_cast(animation.frameCenterX); - point.y += static_cast(animation.frameHeight) - - static_cast(animation.frameCenterY); point.height = static_cast(animation.frameHeight); point.halfWidth = (std::max)(static_cast(animation.frameWidth) / 2.0f, 1.5f); - return point; + } else { + const auto& geometry = flame_geometry_for(upper); + point.height = geometry.height; + point.halfWidth = geometry.halfWidth; } - // Fallback for the first refresh before the object rendered once - // (CVidCell::m_pFrame still null) and for the disk parser. - const auto& geometry = flame_geometry_for(upper); - point.x += geometry.dx; - point.y += geometry.dy; - point.height = geometry.height; - point.halfWidth = geometry.halfWidth; return point; } case AreaAnimationKind::Smoke: { diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index cb788d8..79a8dc7 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1673,10 +1673,10 @@ void test_build_area_effect_points() { animation.frameCenterY = 0; live.animations.push_back(animation); const auto livePoints = build_area_effect_points(live); - expect_true(livePoints.size() == 1 && livePoints[0].x == 104.0f && - livePoints[0].y == 215.0f && livePoints[0].height == 15.0f && + expect_true(livePoints.size() == 1 && livePoints[0].x == 100.0f && + livePoints[0].y == 200.0f && livePoints[0].height == 15.0f && livePoints[0].halfWidth == 4.0f && livePoints[0].reserved1 == 1.0f, - "Live CVidCell frame geometry overrides the fallback table"); + "Live CVidCell frame geometry sizes the flame; the anchor is unshifted"); } const auto points = build_area_effect_points(info); @@ -1685,9 +1685,9 @@ void test_build_area_effect_points() { points[0].height == 27.0f && points[0].halfWidth == 7.0f, "Fire points come first with authored BAM geometry"); expect_true(points.size() >= 2 && points[1].kind == 1.0f && points[1].reserved1 == 1.0f && - points[1].x == 29.0f && points[1].y == 115.0f && + points[1].x == 25.0f && points[1].y == 100.0f && points[1].height == 15.0f && points[1].halfWidth == 4.0f, - "Blue flames carry the palette id and the frame-box offset from the object pos"); + "Blue flames carry the palette id and the authored footprint at the unshifted anchor"); expect_true(points.size() >= 3 && points[2].kind == 1.0f && points[2].reserved1 == 2.0f, "Overlay fires become glow-only points"); expect_true(points.size() >= 4 && points[3].kind == 4.0f, From 49975b2b3ad1d87f6f95cf582a79028834d207f0 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 18 Jul 2026 23:36:09 +0200 Subject: [PATCH 14/57] Point effects: subtract the m_posZ elevation from the flame anchor CGameStatic::RenderBam draws at y = m_pos.y - m_posZ; mounted flames (wall sconces) carry authored Z, so anchoring at raw m_pos.y left them hanging below the art by exactly z pixels. The walk now reads the live m_posZ (+0x14, already modeled), the disk mirror loads the ARE height field, and point packing applies the same subtraction. objZ joins the per-animation debug log so field logs show the elevation directly. --- docs/are-animation-detection.md | 10 ++++++++-- src/iee/area_state.cpp | 5 +++-- src/iee/game/are_animations.cpp | 8 ++++++-- src/iee/game/are_animations.h | 5 +++++ src/iee/game/object_statics.cpp | 4 ++++ tests/iee_tests.cpp | 8 ++++++-- 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/docs/are-animation-detection.md b/docs/are-animation-detection.md index a9134ce..2254534 100644 --- a/docs/are-animation-detection.md +++ b/docs/are-animation-detection.md @@ -21,7 +21,7 @@ fire/smoke/light entries are packed into a 32-slot `uIeePoints[]` uniform array (`build_area_effect_points`, fire prioritized under capacity pressure, per-resref flame scale) and fed through the existing uniform bridge. The shader draws textured **replacements**, not just halos: shaped flame bodies -eroded by rising FBM noise with a blackbody color ramp, heat shimmer, and +eroded by rising FBM noise with a blackbody color ramp and night-adaptive cast light; noise-billowed smoke plumes that wander and dissipate with altitude; steady candle/glow halos. The tileable FBM octaves ship as `iee_effects_noise.rgba` (unit 6, generated in-repo — see @@ -34,6 +34,11 @@ vanilla, and any resolution failure just keeps the engine draws. Gated by composite over sprites (true bloom) remain future P4 work consuming the same points. +Placement follows the engine's own draw rule: `CGameStatic::RenderBam` +anchors at the live `CGameObject::m_pos` with the screen Y lifted by the +`m_posZ` elevation (`y = m_pos.y - m_posZ`) — mounted flames (wall sconces) +carry authored Z, so the packed point subtracts it the same way. + Scope boundary: **water bodies are not ARE animations.** Rivers/lakes/sea are WED overlays and stay on the existing `wed_runtime`/`tile_liquid` path. ARE animations cover point phenomena only (the fountain *spray*, not the pool). @@ -94,7 +99,8 @@ and cross-checked against the binary's disassembly: engine's own loader. - `CGameStatic` derives from the 0x60-byte `CGameObject` base (`m_objectType` +0x8 — statics are type `0x30`/`'0'`; `m_pos` +0xC; - `m_pArea` +0x18) and **retains the raw authored record at +0x60** + `m_posZ` +0x14; `m_pArea` +0x18) and **retains the raw authored record at + +0x60** (`m_header`). Script actions (`StaticStart` show/hide, palette swaps) mutate it in place, so the walk sees live state. - Objects resolve through `CGameObjectArray`'s **static** globals: diff --git a/src/iee/area_state.cpp b/src/iee/area_state.cpp index 0e85972..515c553 100644 --- a/src/iee/area_state.cpp +++ b/src/iee/area_state.cpp @@ -103,10 +103,11 @@ void log_area_animation_summary(const game::AreaAnimationsInfo& info) { for (const auto& animation : info.animations) { if (animation.kind != game::AreaAnimationKind::None) { LOG_DEBUG("ARE animation {}: kind={}, resref={}, name=\"{}\", pos=({}, {}), objPos=({}, " - "{}), frame=({}x{} c{},{} valid={}), shown={}, lightSource={}", + "{}, z={}), frame=({}x{} c{},{} valid={}), shown={}, lightSource={}", info.areaResrefView(), game::area_animation_kind_name(animation.kind), animation.resrefView(), animation.nameView(), animation.x, animation.y, - animation.objX, animation.objY, animation.frameWidth, animation.frameHeight, + animation.objX, animation.objY, animation.objZ, animation.frameWidth, + animation.frameHeight, animation.frameCenterX, animation.frameCenterY, animation.frameValid, animation.isShown(), animation.isLightSource()); continue; diff --git a/src/iee/game/are_animations.cpp b/src/iee/game/are_animations.cpp index 99d87ec..c312334 100644 --- a/src/iee/game/are_animations.cpp +++ b/src/iee/game/are_animations.cpp @@ -294,9 +294,12 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& -> std::optional { AreaEffectPoint point{}; // The engine renders from the live object position, not the header - // coordinates; both are logged so a divergence stays visible. + // coordinates; both are logged so a divergence stays visible. RenderBam's + // screen Y is m_pos.y - m_posZ (elevation): a wall-mounted sconce with + // authored Z draws that many pixels above its map position, so the same + // subtraction anchors the replacement flame on the art. point.x = static_cast(animation.objX); - point.y = static_cast(animation.objY); + point.y = static_cast(animation.objY - animation.objZ); const float base = static_cast(static_cast(animation.kind)); const auto resref = animation.resrefView(); switch (animation.kind) { @@ -369,6 +372,7 @@ AreaAnimationInfo make_area_animation_info(const ARE_Animation_st& record) noexc animation.y = record.nY; animation.objX = record.nX; animation.objY = record.nY; + animation.objZ = record.nHeight; animation.height = record.nHeight; animation.schedule = record.nSchedule; animation.flags = record.nFlags; diff --git a/src/iee/game/are_animations.h b/src/iee/game/are_animations.h index 87bc634..d13c2dc 100644 --- a/src/iee/game/are_animations.h +++ b/src/iee/game/are_animations.h @@ -39,6 +39,11 @@ struct AreaAnimationInfo { // parser mirrors x/y. std::int32_t objX{}; std::int32_t objY{}; + // Live CGameObject::m_posZ. RenderBam's screen Y is m_pos.y - m_posZ (the + // elevation subtraction), so a mounted flame (wall sconce) draws above its + // map position. Loaded from the ARE record's height field; the memory walk + // overrides it with the live value. + std::int32_t objZ{}; // The engine's cached current BAM frame entry (CVidCell::m_pFrame) — the // exact geometry RenderBam draws with. Valid only when the memory walk // could read it (the object has rendered at least once). diff --git a/src/iee/game/object_statics.cpp b/src/iee/game/object_statics.cpp index cec8769..c7b118f 100644 --- a/src/iee/game/object_statics.cpp +++ b/src/iee/game/object_statics.cpp @@ -105,6 +105,10 @@ bool collect_area_static_animations(const ObjectArrayGlobals& globals, const CGa info.objX = objectPos.x; info.objY = objectPos.y; } + std::int32_t objectPosZ = 0; + if (core::safe_read(objectBytes + offsetof(CGameObject, m_posZ), objectPosZ)) { + info.objZ = objectPosZ; + } // The engine caches the current frame entry it renders with // (CVidCell::m_pFrame); mirror its geometry so no per-resref table is // needed. Null until the object has rendered once — the render-thread diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 79a8dc7..916f30b 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1581,6 +1581,7 @@ void test_collect_area_static_animations() { statics[0].baseclass_0.m_objectType = kGameObjectTypeStatic; statics[0].baseclass_0.m_pArea = &areaA; + statics[0].baseclass_0.m_posZ = 25; statics[0].m_vidCell.m_pFrame = &liveFrame; statics[0].m_header.rrAnimation = {'F', 'L', 'A', 'M', 'B', 'I', 'G', 0}; const char fireName[] = "FLAMBIG"; @@ -1616,6 +1617,8 @@ void test_collect_area_static_animations() { expect_true(!out.animations.empty() && out.animations[0].x == 320 && out.animations[0].isShown(), "Collected records should carry the live header fields"); + expect_true(!out.animations.empty() && out.animations[0].objZ == 25, + "The walk mirrors the live m_posZ elevation"); expect_true(!out.animations.empty() && out.animations[0].frameValid && out.animations[0].frameWidth == 12 && out.animations[0].frameHeight == 40 && out.animations[0].frameCenterX == 6 && out.animations[0].frameCenterY == 30, @@ -1663,6 +1666,7 @@ void test_build_area_effect_points() { animation.kind = AreaAnimationKind::Fire; animation.objX = 100; animation.objY = 200; + animation.objZ = 30; // mounted sconce: RenderBam draws at y - z animation.flags = kAreAnimationFlagIsShown; const char liveResref[] = "flamblu2"; for (std::size_t c = 0; liveResref[c] != '\0'; ++c) animation.resref[c] = liveResref[c]; @@ -1674,9 +1678,9 @@ void test_build_area_effect_points() { live.animations.push_back(animation); const auto livePoints = build_area_effect_points(live); expect_true(livePoints.size() == 1 && livePoints[0].x == 100.0f && - livePoints[0].y == 200.0f && livePoints[0].height == 15.0f && + livePoints[0].y == 170.0f && livePoints[0].height == 15.0f && livePoints[0].halfWidth == 4.0f && livePoints[0].reserved1 == 1.0f, - "Live CVidCell frame geometry sizes the flame; the anchor is unshifted"); + "Live frame geometry sizes the flame; the anchor subtracts the Z elevation"); } const auto points = build_area_effect_points(info); From 9dff69639d712156d7828182e6d88aca58a7106b Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 27 Aug 2026 01:22:30 +0200 Subject: [PATCH 15/57] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1b05f8..031d880 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -111,7 +111,7 @@ jobs: "iee-textures/README.md" "iee-textures/iee_effects_noise.rgba" "iee-textures/iee_water_dudv.rgba" - "iee-textures/iee_water_foam.rgba" + "iee-textures/iee_water_foam.rgba" "iee-textures/iee_water_normal.rgba" ) | Sort-Object $actual = Get-ChildItem $root -File -Recurse | ForEach-Object { From 62b22e2053bd8a7ca59ba0bd60def25cb85211d6 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 27 Aug 2026 01:39:03 +0200 Subject: [PATCH 16/57] Support cross-compiling the Windows DLL with mingw-w64 Enables building InfinityEngine-Enhancer.dll from Linux via a mingw-w64 toolchain, for local Proton-based iteration without a Windows host. Fixes three MSVC-only assumptions the cross build exposed: - release_bundle unconditionally copied $, which hard-errors at CMake generate time for non-MSVC linkers (mingw has no PDB). Gated behind if(MSVC); the MSVC/CI path is unaffected. - opengl_types.cpp implicitly converted FARPROC to void*, an MSVC extension GCC rejects under -fpermissive-off. Made explicit. - shader_probe.cpp called _ReturnAddress(), which mingw-w64 declares for source compatibility but never defines, causing an unresolved symbol at link time. Added a caller_return_address() wrapper that uses __builtin_return_address(0) on GCC/Clang. Adds cmake/toolchains/mingw-w64.cmake for the cross build itself. --- CMakeLists.txt | 17 +++++++++++++---- cmake/toolchains/mingw-w64.cmake | 18 ++++++++++++++++++ src/iee/game/opengl_types.cpp | 4 ++-- src/iee/shader_probe.cpp | 14 ++++++++++++-- 4 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 cmake/toolchains/mingw-w64.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index ef31e0a..3c6a360 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,17 +191,26 @@ if(IEE_BUILD_WINDOWS_DLL) set(IEE_RELEASE_BUNDLE_DIR "${CMAKE_BINARY_DIR}/release-bundle") set(IEE_SYMBOLS_DIR "${CMAKE_BINARY_DIR}/symbols") - add_custom_target(release_bundle + set(IEE_BUNDLE_COMMANDS COMMAND "${CMAKE_COMMAND}" -E rm -rf "${IEE_RELEASE_BUNDLE_DIR}" - COMMAND "${CMAKE_COMMAND}" -E rm -rf "${IEE_SYMBOLS_DIR}" COMMAND "${CMAKE_COMMAND}" -E make_directory "${IEE_RELEASE_BUNDLE_DIR}" - COMMAND "${CMAKE_COMMAND}" -E make_directory "${IEE_SYMBOLS_DIR}" COMMAND "${CMAKE_COMMAND}" -E copy "$" "${IEE_RELEASE_BUNDLE_DIR}/InfinityEngine-Enhancer.dll" - COMMAND "${CMAKE_COMMAND}" -E copy "$" "${IEE_SYMBOLS_DIR}/InfinityEngine-Enhancer.pdb" COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_SOURCE_DIR}/tools/InfinityEngine-Enhancer.sample.ini" "${IEE_RELEASE_BUNDLE_DIR}/InfinityEngine-Enhancer.sample.ini" COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_SOURCE_DIR}/assets/override" "${IEE_RELEASE_BUNDLE_DIR}/override" COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_SOURCE_DIR}/tools/M_IEEE.lua" "${IEE_RELEASE_BUNDLE_DIR}/override/M_IEEE.lua" COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_SOURCE_DIR}/assets/game-textures" "${IEE_RELEASE_BUNDLE_DIR}/iee-textures" + ) + if(MSVC) + # TARGET_PDB_FILE only resolves for linkers that emit a PDB. mingw-w64 + # cross builds have no equivalent, so keep symbol export MSVC-only. + list(APPEND IEE_BUNDLE_COMMANDS + COMMAND "${CMAKE_COMMAND}" -E rm -rf "${IEE_SYMBOLS_DIR}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${IEE_SYMBOLS_DIR}" + COMMAND "${CMAKE_COMMAND}" -E copy "$" "${IEE_SYMBOLS_DIR}/InfinityEngine-Enhancer.pdb" + ) + endif() + add_custom_target(release_bundle + ${IEE_BUNDLE_COMMANDS} DEPENDS InfinityEngine-Enhancer VERBATIM ) diff --git a/cmake/toolchains/mingw-w64.cmake b/cmake/toolchains/mingw-w64.cmake new file mode 100644 index 0000000..1b3a52d --- /dev/null +++ b/cmake/toolchains/mingw-w64.cmake @@ -0,0 +1,18 @@ +# Cross-compile the Windows DLL target from Linux with mingw-w64. +# Usage: +# cmake -S . -B build-mingw -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake \ +# -DIEE_BUILD_WINDOWS_DLL=ON -DBUILD_TESTING=OFF +# cmake --build build-mingw --target release_bundle + +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) +set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++) +set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) + +set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/src/iee/game/opengl_types.cpp b/src/iee/game/opengl_types.cpp index 9aaed1d..1d4e91f 100644 --- a/src/iee/game/opengl_types.cpp +++ b/src/iee/game/opengl_types.cpp @@ -53,7 +53,7 @@ bool check_error(const char* operation) noexcept { // GetProcAddress on the opengl32.dll module handle instead. static void* get_gl1_proc_address(HMODULE opengl32, const char* name) noexcept { if (!opengl32) return nullptr; - return GetProcAddress(opengl32, name); + return reinterpret_cast(GetProcAddress(opengl32, name)); } // Load an extension entry point. Try wglGetProcAddress first (correct path @@ -75,7 +75,7 @@ static void* get_ext_proc_address(HMODULE opengl32, const char* name) noexcept { return proc; } } - return GetProcAddress(opengl32, name); + return reinterpret_cast(GetProcAddress(opengl32, name)); } HGLRC current_context() noexcept { diff --git a/src/iee/shader_probe.cpp b/src/iee/shader_probe.cpp index ebb122e..a8d37a6 100644 --- a/src/iee/shader_probe.cpp +++ b/src/iee/shader_probe.cpp @@ -618,10 +618,20 @@ void use_program(unsigned program, std::uintptr_t caller, bool isArb) { } } +// _ReturnAddress() is an MSVC intrinsic; mingw-w64 declares it in intrin.h +// for source compatibility but never defines it, so cross builds hit an +// unresolved symbol at link time. GCC/Clang expose the same value via the +// __builtin_return_address() builtin instead. +#if defined(_MSC_VER) && !defined(__clang__) +static inline void* caller_return_address() noexcept { return _ReturnAddress(); } +#else +static inline void* caller_return_address() noexcept { return __builtin_return_address(0); } +#endif + static void APIENTRY detour_glUseProgram(unsigned program) noexcept { bool forwarded = false; try { - const auto caller = reinterpret_cast(_ReturnAddress()); + const auto caller = reinterpret_cast(caller_return_address()); forwarded = true; g_glUseProgramHook.original()(program); use_program(program, caller, false); @@ -633,7 +643,7 @@ static void APIENTRY detour_glUseProgram(unsigned program) noexcept { static void APIENTRY detour_glUseProgramObjectARB(unsigned program) noexcept { bool forwarded = false; try { - const auto caller = reinterpret_cast(_ReturnAddress()); + const auto caller = reinterpret_cast(caller_return_address()); forwarded = true; g_glUseProgramObjectARBHook.original()(program); use_program(program, caller, true); From a3423fb47c54478e2b9dc99d6081fafce18c497e Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 27 Aug 2026 02:19:39 +0200 Subject: [PATCH 17/57] Fix authored flame replacement anchors --- src/iee/game/are_animations.cpp | 16 ++++++++++------ tests/iee_tests.cpp | 13 +++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/iee/game/are_animations.cpp b/src/iee/game/are_animations.cpp index c312334..3cd7f2b 100644 --- a/src/iee/game/are_animations.cpp +++ b/src/iee/game/are_animations.cpp @@ -312,23 +312,27 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& point.halfWidth = 10.0f; return point; } - // The ARE object position is already the flame's base point on screen - // (ALIGN markers at raw objPos land exactly on the sconce bowls) — so - // no positional offset is applied; the flame body grows upward from - // the anchor. Only the authored SIZE varies per family. Live frame - // geometry (when present) gives size directly; otherwise the table. + // RenderBam places the frame's top-left at objectPos - frameCenter, + // while the procedural flame grows upward from its bottom-center. Move + // the point to that authored bottom-center before suppressing the BAM. // Note: while the effect suppresses the engine draw, CVidCell::m_pFrame - // stays null, so the table is the normal path for replaced flames. + // normally stays null, so the reviewed per-resref table is the normal + // path for replaced flames. const auto upper = upper_copy(resref); point.reserved1 = upper.find("BLU") != std::string::npos ? 1.0f : 0.0f; // palette id if (animation.frameValid) { point.height = static_cast(animation.frameHeight); point.halfWidth = (std::max)(static_cast(animation.frameWidth) / 2.0f, 1.5f); + point.x += static_cast(animation.frameWidth) / 2.0f - + static_cast(animation.frameCenterX); + point.y += static_cast(animation.frameHeight - animation.frameCenterY); } else { const auto& geometry = flame_geometry_for(upper); point.height = geometry.height; point.halfWidth = geometry.halfWidth; + point.x += geometry.dx; + point.y += geometry.dy; } return point; } diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 916f30b..daf49cf 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1677,21 +1677,22 @@ void test_build_area_effect_points() { animation.frameCenterY = 0; live.animations.push_back(animation); const auto livePoints = build_area_effect_points(live); - expect_true(livePoints.size() == 1 && livePoints[0].x == 100.0f && - livePoints[0].y == 170.0f && livePoints[0].height == 15.0f && + expect_true(livePoints.size() == 1 && livePoints[0].x == 104.0f && + livePoints[0].y == 185.0f && livePoints[0].height == 15.0f && livePoints[0].halfWidth == 4.0f && livePoints[0].reserved1 == 1.0f, - "Live frame geometry sizes the flame; the anchor subtracts the Z elevation"); + "Live frame geometry moves the object origin to the flame bottom-center"); } const auto points = build_area_effect_points(info); expect_eq(points.size(), std::size_t{5}, "Shown fire/light + replaceable smoke become points"); expect_true(!points.empty() && points[0].kind == 1.0f && points[0].x == 20.0f && - points[0].height == 27.0f && points[0].halfWidth == 7.0f, + points[0].y == 115.0f && points[0].height == 27.0f && + points[0].halfWidth == 7.0f, "Fire points come first with authored BAM geometry"); expect_true(points.size() >= 2 && points[1].kind == 1.0f && points[1].reserved1 == 1.0f && - points[1].x == 25.0f && points[1].y == 100.0f && + points[1].x == 29.0f && points[1].y == 115.0f && points[1].height == 15.0f && points[1].halfWidth == 4.0f, - "Blue flames carry the palette id and the authored footprint at the unshifted anchor"); + "Blue flames carry their palette, footprint, and authored bottom-center offset"); expect_true(points.size() >= 3 && points[2].kind == 1.0f && points[2].reserved1 == 2.0f, "Overlay fires become glow-only points"); expect_true(points.size() >= 4 && points[3].kind == 4.0f, From 3c2b6a61142f43b032e7f1bd480af00d825c0909 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 27 Aug 2026 02:45:13 +0200 Subject: [PATCH 18/57] Keep authored effects visible in alignment mode --- src/iee/hooks.cpp | 2 +- src/iee/shader_probe.cpp | 4 ++++ src/iee/shader_probe.h | 3 +++ src/iee/shader_uniform_bridge.cpp | 5 +++++ src/iee/shader_uniform_bridge.h | 3 +++ 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index 2ca9d35..740e382 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -284,7 +284,7 @@ static void detour_draw_color_tone(int mode) { static void detour_static_render(void* thisPtr, void* area, void* vidMode) { try { if (g_ctx && g_ctx->cfg.enablePointEffects && g_ctx->cfg.enableWaterEffect && - probe::override_effect_enabled() && thisPtr) { + probe::override_effect_replacement_enabled() && thisPtr) { game::ARE_Animation_st header{}; const auto* headerAddress = reinterpret_cast(thisPtr) + offsetof(game::CGameStatic, m_header); diff --git a/src/iee/shader_probe.cpp b/src/iee/shader_probe.cpp index a8d37a6..975cf07 100644 --- a/src/iee/shader_probe.cpp +++ b/src/iee/shader_probe.cpp @@ -1090,6 +1090,10 @@ void set_override_effect_enabled(bool enabled) noexcept { uniforms::set_effect_e bool override_effect_enabled() noexcept { return uniforms::effect_enabled(); } +bool override_effect_replacement_enabled() noexcept { + return uniforms::effect_replacement_enabled(); +} + void set_area_world_size(float widthPx, float heightPx) noexcept { uniforms::set_world_size(widthPx, heightPx); } diff --git a/src/iee/shader_probe.h b/src/iee/shader_probe.h index b5fd3b6..1aec6d5 100644 --- a/src/iee/shader_probe.h +++ b/src/iee/shader_probe.h @@ -25,6 +25,9 @@ void on_frame_tick(float secondsSinceStart) noexcept; // Hotkey cycle target: uIeeEnabled value 0=off / 1=effect on / 2=alignment debug. void set_override_effect_enabled(bool enabled) noexcept; [[nodiscard]] bool override_effect_enabled() noexcept; +// Normal ON state only; false in ALIGN so authored BAMs remain visible under +// placement markers. +[[nodiscard]] bool override_effect_replacement_enabled() noexcept; // Published by area_state at area load; consumed by the uniform feed. void set_area_world_size(float widthPx, float heightPx) noexcept; diff --git a/src/iee/shader_uniform_bridge.cpp b/src/iee/shader_uniform_bridge.cpp index 1710d18..d20fdb9 100644 --- a/src/iee/shader_uniform_bridge.cpp +++ b/src/iee/shader_uniform_bridge.cpp @@ -123,6 +123,11 @@ void set_effect_enabled(bool enabled) noexcept { bool effect_enabled() noexcept { return g_effectValue.load(std::memory_order_relaxed) >= 0.5f; } +bool effect_replacement_enabled() noexcept { + const float value = g_effectValue.load(std::memory_order_relaxed); + return value >= 0.5f && value < 1.5f; +} + float cycle_debug_effect() noexcept { const float current = g_effectValue.load(std::memory_order_relaxed); const float next = current < 0.5f ? 1.0f : (current < 1.5f ? 2.0f : 0.0f); diff --git a/src/iee/shader_uniform_bridge.h b/src/iee/shader_uniform_bridge.h index 8d26b2c..2269799 100644 --- a/src/iee/shader_uniform_bridge.h +++ b/src/iee/shader_uniform_bridge.h @@ -71,6 +71,9 @@ void reset() noexcept; void set_time(float secondsSinceStart) noexcept; void set_effect_enabled(bool enabled) noexcept; [[nodiscard]] bool effect_enabled() noexcept; +// True only for the normal effect-rendering state. ALIGN keeps the shader +// diagnostics active but must leave authored BAMs visible for comparison. +[[nodiscard]] bool effect_replacement_enabled() noexcept; [[nodiscard]] float cycle_debug_effect() noexcept; void set_world_size(float widthPx, float heightPx) noexcept; void set_water_tint(float r, float g, float b) noexcept; From 73474b4bd5b6b21eaf489a330cc53bcd9f1756fb Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 27 Aug 2026 02:57:05 +0200 Subject: [PATCH 19/57] Constrain flame body to authored BAM envelope --- assets/override/fpSEAM.glsl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/assets/override/fpSEAM.glsl b/assets/override/fpSEAM.glsl index e2161bb..e5d036c 100644 --- a/assets/override/fpSEAM.glsl +++ b/assets/override/fpSEAM.glsl @@ -335,14 +335,18 @@ void main() bool bodyless = palette > 1.5; // --- flame body (replaces the engine's BAM flame) --- - if (!bodyless && offs.y < 6.0 && offs.y > -fh * 1.3 && abs(offs.x) < fw * 2.6) + // Keep the luminous core inside the authored BAM envelope: p.xy is + // the proven bottom attachment point, so no body pixels belong below + // it and the tip should not exceed the authored frame height. + if (!bodyless && offs.y <= 0.0 && offs.y > -fh && abs(offs.x) < fw * 2.6) { - float v = clamp(-offs.y / fh, 0.0, 1.2); // 0 base -> 1 tip + float v = clamp(-offs.y / fh, 0.0, 1.0); // 0 base -> 1 tip float widthAt = fw * (1.05 - 0.60 * min(v, 1.0)); float xr = offs.x / max(widthAt, 1.0); float radial = 1.0 - clamp(xr * xr, 0.0, 1.0); - float base = smoothstep(8.0, -1.0, offs.y); - float column = radial * base * smoothstep(1.20, 0.50, v); + float base = 1.0 - smoothstep(-2.0, 0.0, offs.y); + float tip = 1.0 - smoothstep(0.65, 1.0, v); + float column = radial * base * tip; // Rising noise erodes the column: calm base, ragged tip. // Sample scale follows the flame size so small flames keep // visible structure. From ce063dd37e3cc7d11f2a2f06d2a448530b68223c Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 27 Aug 2026 03:04:09 +0200 Subject: [PATCH 20/57] Taper procedural flame tips beyond core envelope --- assets/override/fpSEAM.glsl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/assets/override/fpSEAM.glsl b/assets/override/fpSEAM.glsl index e5d036c..81ec1ae 100644 --- a/assets/override/fpSEAM.glsl +++ b/assets/override/fpSEAM.glsl @@ -337,15 +337,16 @@ void main() // --- flame body (replaces the engine's BAM flame) --- // Keep the luminous core inside the authored BAM envelope: p.xy is // the proven bottom attachment point, so no body pixels belong below - // it and the tip should not exceed the authored frame height. - if (!bodyless && offs.y <= 0.0 && offs.y > -fh && abs(offs.x) < fw * 2.6) + // it. A low-opacity procedural tip may taper above the authored core; + // evaluate beyond fh so the animation never clips at a hard top edge. + if (!bodyless && offs.y <= 0.0 && offs.y > -fh * 1.3 && abs(offs.x) < fw * 2.6) { - float v = clamp(-offs.y / fh, 0.0, 1.0); // 0 base -> 1 tip + float v = clamp(-offs.y / fh, 0.0, 1.3); // 0 base -> 1 authored tip float widthAt = fw * (1.05 - 0.60 * min(v, 1.0)); float xr = offs.x / max(widthAt, 1.0); float radial = 1.0 - clamp(xr * xr, 0.0, 1.0); float base = 1.0 - smoothstep(-2.0, 0.0, offs.y); - float tip = 1.0 - smoothstep(0.65, 1.0, v); + float tip = 1.0 - smoothstep(0.72, 1.24, v); float column = radial * base * tip; // Rising noise erodes the column: calm base, ragged tip. // Sample scale follows the flame size so small flames keep From 3a055a23ea023378b3cebc72afe0a1adf0cf3331 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 3 Oct 2026 20:03:31 +0200 Subject: [PATCH 21/57] docs: add graphics uplift feasibility roadmap (2026-10-03) --- ...26-10-03-graphics-uplift-roadmap-design.md | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md diff --git a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md new file mode 100644 index 0000000..6255514 --- /dev/null +++ b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md @@ -0,0 +1,228 @@ +# Graphics Uplift Roadmap — Feasibility and Order + +Date: 2026-10-03 +Status: Feasibility record and ordering. Each item still needs its own spec +before implementation. +Target: BGEE 2.7.3.x (x64, Proton), EEex-loaded DLL, EEex Uncap FPS enabled. + +This document supersedes the *scope decisions* of +[2026-06-10-graphics-enhancement-roadmap-design.md](2026-06-10-graphics-enhancement-roadmap-design.md) +where they conflict (see [Reversed decisions](#reversed-decisions)). The +architecture rules in that document and in `docs/architecture.md` still hold: +manifest-only addresses, GL state guard, fail closed, shader delivery through +the game `override/` directory plus DLL uniform feeding. + +## 1. Evidence base + +All RVAs below are for the installed Steam `Baldur.exe` 2.7.3. Its CodeView +record (GUID `B671EED8-BE5E-4979-A639-3E707E611587`, age 75) matches +`IE_2.7.3.0_Win64_debug/Baldur.pdb`, so PDB symbols are authoritative for this +build. Decompiles came from a headless Ghidra project with the PDB applied +(location recorded in the workspace `AGENTS.md`). Everything here is static +analysis; items marked **gate** need DLL-side logging before they are relied on. + +### 1.1 Timing + +| Fact | Source | +|---|---| +| Logic ticks at 30 Hz | `CChitin::TIMER_UPDATES_PER_SECOND` (RVA `0x65CC5C`) = 30 | +| VEF/VVC frame pacing is derived from 30 | `CVisualEffect::MAX_FRAME_RATE` (RVA `0x59EC20`) = 30, used in `CVEFVidCell::FrameAdvance` `0x254440` | +| Rendering runs faster than logic | EEex Uncap FPS (`CChitin::Override_Update`), limit 240 in the local config | +| EEex smooths only the viewport | `EEex_UncapFPS_Patch.lua` hooks scroll, zoom, screen shake, tooltips; no sprite hooks. `EEex.dll` exports no tick-progress value | +| EEex also detours `CVidTile::RenderTexture` | `Hook-CVidTile::RenderTexture()-FirstInstruction` — same function this mod hooks | + +### 1.2 Sprite and effect draw path + +Every paletted BAM frame is rebuilt on the CPU on every rendered frame: + +``` +CVidCell::Render3d (0x424780 / 0x424AE0) + GetFrame (vtable, 0x4118A0) m_nCurrentSequence/m_nCurrentFrame -> m_pFrame + CVidPalette::Realize -> rgbTempPal live palette (tints, range affects) applied here + DrawLockSurface (GL: 0x42CAF0) slot in the streaming atlas fx[0] nearest / fx[1] linear + CVidCell::Blt8To32 (0x426C00) 8-bit -> RGBA into the atlas texels + CVidCell::RenderTexture (0x425530) blend mode from flags, DrawQuad +``` + +Characters and effects composite several cells into one surface first: + +``` +CInfinity::FXPrep 0x29E1B0 -> FXLock 0x29E190 + FXRender 0x29E260 per cell body, helmet, shield, weapon (order by facing) + FXRenderClippingPolys 0x29E4C0 wall polygons rasterised into the surface +FXUnlock 0x29EA40 -> FXBltFromClipped 0x29DFF0 -> CVidMode::FXBltToBack +``` + +Consequences: + +- The GPU receives one RGBA image per sprite with the live palette already + applied. Runtime palette mutation is therefore **not** an obstacle for + anything done at or after `Blt8To32`. +- Sprites share a streaming atlas. Any shader kernel that samples neighbours + must clamp to the quad's own rectangle. +- BAM v2 (PVRZ-backed) frames take `CVidCell::RenderPVR` (`0x4250D0`) instead + and skip the CPU composite. +- The sprite shadow is one palette index (`CVidPalette::SHADOW_ENTRY`), + resolved in `Render3d` before the blit. +- Wall occlusion can use dithered spans + (`CVidPoly::DrawHLineDithered32` `0x42DB50`, "Always Dither" option). + +Per-object render entry points that route into the path above: + +| Object | Function | RVA | +|---|---|---| +| Creature | `CGameSprite::Render` | `0x36BA50` | +| Character animation | `CGameAnimationTypeCharacter::Render` (+16 sibling types) | `0x32C240` | +| VEF/VVC effect cell | `CVEFVidCell::Render` | `0x254B00` | +| Effect animation | `CGameAnimationTypeEffect::Render` | `0x32D020` | +| Projectile | `CProjectileBAM::Render` | `0x233F20` | +| Fireball | `CGameFireball3d::Render` | `0x1EAF90` | +| Ambient static | `CGameStatic::Render` (already hooked) | `0x1F27D0` | +| Particles (GL points) | `CParticle::Render` | `0x425BE0` | +| Rain / snow | `CRainStorm::Render` / `CSnowStorm::Render` | `0x25CD20` / `0x25CDD0` | + +### 1.3 Frame composition order + +`CGameArea::Render` (`0x189360`): `CInfinity::Render` (tiles) → +`RenderEdgeFade` → object lists (virtual `Render`) → `RenderTransitions` → +`RenderAOE` → `RenderFog` → `PostRender`. UI draws afterwards, so a bracket +around this function excludes the UI by construction. + +`CInfinity::RenderFog` (`0x2A1B60`) draws fog as untextured, alpha-blended +triangles per 64 px tile inside one `DrawBegin`/`DrawEnd`. It is safe to +bracket; `CVisibilityMap::BltFogOWar3d` (`0x257520`) must still never be +detoured. + +### 1.4 Engine light value + +`CInfinity::Render` multiplies every tile by `m_rgbGlobalLighting` +(`GetGlobalLighting` `0x29F890`: time-of-day, overcast and lightning colours +combined) and hands the same colour to sprites via `CVidMode::rgbGlobalTint`. +`CGameArea::GetTintColor` (`0x182ED0`) adds the per-position lightmap tint for +sprites. This is a flat colour, not a light simulation. + +### 1.5 Engine shaders + +`chitin.key` lists `FPSPRITE`, `FPCRSPRT`, `fpDraw`, `fpSeam`, `FPSELECT`, +`FPFONT`, `fpTone`, `fpYUV`, `FPYUVGRY`, `fpCatRom`, `vpDraw`, `vpBlit`, +`vpYUV` (and `FPIT1L/M/S`). Originals are extractable from the BIFs for +interface contracts. Extracted sources are Beamdog content and stay out of +this repository. + +## 2. Items + +Each item lists what it does, where it attaches, and the main risk. + +### A. Position smoothing + +Sprites move in 30 Hz steps while the camera scrolls at display rate. Lerp +each sprite's draw position between ticks inside a `CGameSprite::Render` hook +(swap position in, call original, restore). + +- Timing: the hook measures tick boundaries itself (for example from + `CGameSprite::AIUpdate` `0x3447F0` or `CInfGame::SynchronousUpdate` + `0x292010`). +- **Gate:** confirm the previous-tick position source. `m_posOld` exists but + its use in `Render` looks like a motion trail; do not assume semantics. +- Risk: selection circles, health bars and markers must move with the sprite. + +### B. `fpSprite` replacement + +One override shader that covers every character and effect quad. + +- B1 upscaling kernel (pixel-art family; `FPCRSPRT` holds the developers' own + abandoned xBR attempt as a reference). +- B2 premultiplied-alpha halo fix (all three parts together, per the 2026-06-10 + design §4.8). +- B3 soft shadows: tag `SHADOW_ENTRY` pixels at blit time, blur in shader. +- B4 smooth wall occlusion: replace the dithered stipple with alpha. +- Risk: atlas neighbour bleed; **gate** on per-quad UV rectangle availability. + +### C. Effect replacement framework + +Hook the per-object render, look the resref up in a table, then pass through +or suppress and draw our own program at the object's position. This is the +shipped `CGameStatic::Render` + `fpSEAM` point-effect pattern, generalised. + +- C1 fix the existing water / smoke / fire replacements (current branch). +- C2 extend to VEF/VVC, projectiles, fireballs; first batch is the + highest-frequency effects, the table grows from logs of unreplaced resrefs. +- C3 effect-cast light: publish active effect positions into the existing + point set so effects light tiles and sprites. +- C4 particles and weather (`CParticle`, rain, snow) as shader passes. +- **Gate:** resref and world-position field offsets per class (PDB types / + EEex docs → `build_manifest`). +- Risk: "all effects" is content work; it is never finished, only prioritised. + +### D. Offscreen world pass + +- D1 soft fog of war: render `RenderFog` into an offscreen target, blur + (radius scaled by zoom), composite. +- D2 bloom on the world pass only. +- D3 water reflections of sprites and effects. +- Precondition already met: the engine binds no FBOs (gate V5, 2026-06-11). + +### E. Companion textures per tile page + +A sibling texture per PVRZ page, generated offline alongside the upscale. + +- E1 normal maps → per-pixel relighting from the point set (C3). +- E2 wall-polygon occlusion for those lights, and light-directed sprite + shadows. The polygons are the ones `FXRenderClippingPolys` consumes. +- E3 "living maps": foliage sway and window/banner flicker from offline masks. +- Risk: quality of colour-derived normals and segmentation masks on painted + art. Prototype offline on a few areas before any loader work. + +### F. Animation frame in-betweening + +At the sprite render, decode frame N and N+1 with the same live palette and +blend in our own shader by tick progress. + +- Costs one extra CPU blit per sprite and a small animation latency. +- Crossfade ghosts on fast limb motion; motion-compensated blending is + plausible at ~100 px but unproven on this art. +- Starts as an offline spike on extracted BAM frames. No DLL work until the + spike shows acceptable quality. +- PVRZ-backed sprites need a separate path. + +### G. Small polish + +Anti-aliased selection rings / AOE markers / path lines (`fpSELECT`), cutscene +scaling kernel (`fpYUV`), offline upscaling of palette-stable portraits and +item icons. + +## 3. Order + +| # | Item | Why here | +|---|---|---| +| 1 | C1 | Existing work on the branch; make it correct before building on it | +| 2 | A | One hook, most certain perceived gain | +| 3 | B1, B2 | Sprites are the most visible mismatch against 4x maps | +| 4 | C2, C3 | Effects plus the light they cast | +| 5 | D1, D2 | Shared offscreen plumbing | +| 6 | B3, B4 | Ride on the `fpSprite` replacement | +| 7 | F spike | Quality question answered offline first | +| 8 | E1 → E2 → E3 | Largest visual jump, largest asset cost | +| 9 | C4, D3, G | Polish | + +## 4. Reversed decisions + +| 2026-06-10 decision | Now | Reason | +|---|---|---| +| Frame interpolation rejected as a content-mod problem (§10.8) | Item F, spike first | Frames are composited on the CPU with the live palette; the palette-mutation objection does not apply at the blit | +| Per-VFX work dropped as disproportionate (§3, §10.7) | Item C | Per-object `Render` hooks give resref + position directly; the pattern already ships for statics | +| Weather polish dropped (§10.7) | Item C4, low priority | Same framework, little extra plumbing | +| Offline ESRGAN rejected for creature sprites (§10.8) | Still rejected | Runtime kernel in `fpSprite` (B1) instead | + +Day/night map blending stays out of scope: the engine holds one tileset at a +time. + +## 5. Standing constraints + +- Every new address or offset goes through `build_manifest`; unresolved → + feature disabled, game untouched. +- Every item is INI-gated and A/B-toggleable at runtime. +- New hooks must coexist with EEex's detours (notably + `CVidTile::RenderTexture` and `CInfinity::Render` screen-shake hooks). +- Validation is CI build + in-game check under Proton; host tests cover the + parsing and packing logic. From 8ed5e12a2eaeeef938da6b3c85a2059dcb5f920d Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 3 Oct 2026 20:10:56 +0200 Subject: [PATCH 22/57] Point effects: place fire/smoke from the BAM frame table CVidCell::m_pFrame is cleared at the end of every Render3d, so the walk's 'live frame geometry' read was never valid and every replaced flame used the hand-tuned per-resref table (default geometry for unknown resrefs, no geometry at all for smoke). Geometry now comes from the engine's parsed BAM (CVidCell::pRes -> CResCell): the union of the current cycle's frame draw boxes, mirrored like RenderBam when the record's mirror flag is set. Fire and smoke both anchor at the box's bottom-centre and take its size. The frame table only exists once the engine has loaded the BAM, which happens on first draw, so the CGameStatic::Render hook keeps the engine draw until the box is captured. Boxes are cached per live object, cleared at LoadArea, and new captures republish the point set. --- docs/are-animation-detection.md | 29 ++++- ...26-10-03-graphics-uplift-roadmap-design.md | 18 ++- src/iee/area_state.cpp | 83 ++++++++++++- src/iee/area_state.h | 8 ++ src/iee/game/are_animations.cpp | 69 ++++++++--- src/iee/game/are_animations.h | 33 +++-- src/iee/game/file_formats.h | 12 ++ src/iee/game/object_statics.cpp | 114 +++++++++++++++--- src/iee/game/object_statics.h | 8 ++ src/iee/game/runtime_types_x64.h | 35 +++++- src/iee/hooks.cpp | 7 +- tests/iee_tests.cpp | 96 ++++++++++++--- 12 files changed, 434 insertions(+), 78 deletions(-) diff --git a/docs/are-animation-detection.md b/docs/are-animation-detection.md index 2254534..0c3240f 100644 --- a/docs/are-animation-detection.md +++ b/docs/are-animation-detection.md @@ -34,10 +34,31 @@ vanilla, and any resolution failure just keeps the engine draws. Gated by composite over sprites (true bloom) remain future P4 work consuming the same points. -Placement follows the engine's own draw rule: `CGameStatic::RenderBam` -anchors at the live `CGameObject::m_pos` with the screen Y lifted by the -`m_posZ` elevation (`y = m_pos.y - m_posZ`) — mounted flames (wall sconces) -carry authored Z, so the packed point subtracts it the same way. +Placement follows the engine's own draw rule (`CGameStatic::RenderBam`, +RVA `0x1F2B50` on 2.7.3): each frame's top-left is drawn at +`(m_pos.x - centerX, m_pos.y - m_posZ - centerY)`, with the box flipped +around `m_pos.x` when the record's mirror flag (bit 11) is set. The +replacement therefore takes its base point and size from the BAM's own frame +table: + +- `read_static_bam_envelope` walks `CVidCell::pRes` (`CResCell`: frame + table, cycle table, V1 frame lookup list) and unions the draw boxes of the + current cycle — or of every cycle when the record draws all sequences + (bit 9). The union is stable across the animation; a single frame is not. +- The point is the box's bottom-centre, its height and half-width are the + box's. Fire and smoke both use it. +- `CVidCell::m_pFrame` is **not** a usable source: `CVidCell::Render3d` + clears it at the end of every draw, so it is null whenever we look. (An + earlier revision read it and silently always fell back to the hand-tuned + table, which is why unknown flame resrefs and all smoke were misplaced.) +- The frame table exists only while the engine has the BAM loaded, and the + engine loads it on first draw. The `CGameStatic::Render` hook therefore + keeps the engine draw until `area::static_envelope_ready` has captured the + box, then suppresses. Captured boxes are cached per live object (cleared at + `LoadArea`) because the engine may release a BAM it no longer draws, and + new captures trigger a throttled republish of the point set. +- The per-resref `kFlameGeometry` table is now only the fallback for the + frames before a static's first engine draw. Scope boundary: **water bodies are not ARE animations.** Rivers/lakes/sea are WED overlays and stay on the existing `wed_runtime`/`tile_liquid` path. ARE diff --git a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md index 6255514..d33a3d0 100644 --- a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md +++ b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md @@ -141,10 +141,20 @@ One override shader that covers every character and effect quad. ### C. Effect replacement framework Hook the per-object render, look the resref up in a table, then pass through -or suppress and draw our own program at the object's position. This is the -shipped `CGameStatic::Render` + `fpSEAM` point-effect pattern, generalised. - -- C1 fix the existing water / smoke / fire replacements (current branch). +or suppress and draw the replacement. The hook seam is the shipped +`CGameStatic::Render` pattern, generalised. + +Where the replacement is drawn is a separate choice. Today fire and smoke are +painted inside `fpSEAM` (the tile pass), which has structural limits: they +sit under every object, are multiplied by the global light colour, and cost +a per-pixel loop over the point set. Drawing them with our own program at +the engine's final blit (`CVidMode::FXBltToBack` `0x41D540`) would remove +those limits, but needs the engine's internal batch flush pinned first. +Owner decision 2026-10-03: stay in `fpSEAM` for now and fix placement there. + +- C1 fix placement and size of the existing smoke / fire replacements + (current branch): geometry now comes from the BAM frame table, see + `docs/are-animation-detection.md`. - C2 extend to VEF/VVC, projectiles, fireballs; first batch is the highest-frequency effects, the table grows from logs of unreplaced resrefs. - C3 effect-cast light: publish active effect positions into the existing diff --git a/src/iee/area_state.cpp b/src/iee/area_state.cpp index 515c553..90df4bb 100644 --- a/src/iee/area_state.cpp +++ b/src/iee/area_state.cpp @@ -2,12 +2,14 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include @@ -44,6 +46,17 @@ std::atomic g_nextAreaGpuGeneration{1}; std::mutex g_areaRefreshCommitMutex; std::atomic g_areaRefreshGeneration{0}; +// Authored BAM draw boxes of the current area's replaced statics, keyed by +// the live CGameStatic. Captured on the render thread once the engine has +// loaded each BAM; kept because the engine may release the resource again +// while we suppress its draws. Cleared at every LoadArea (object addresses +// are reused across areas). +std::mutex g_staticEnvelopeMutex; +std::unordered_map g_staticEnvelopes; +std::atomic g_staticEnvelopesDirty{false}; +constexpr std::size_t kMaxStaticEnvelopes = game::kMaxAreaAnimationRecords; +constexpr auto kEnvelopeRepublishInterval = std::chrono::milliseconds(100); + // Render-thread-owned GL state. unsigned g_areaTexture{}; HGLRC g_areaTextureContext{}; @@ -103,13 +116,12 @@ void log_area_animation_summary(const game::AreaAnimationsInfo& info) { for (const auto& animation : info.animations) { if (animation.kind != game::AreaAnimationKind::None) { LOG_DEBUG("ARE animation {}: kind={}, resref={}, name=\"{}\", pos=({}, {}), objPos=({}, " - "{}, z={}), frame=({}x{} c{},{} valid={}), shown={}, lightSource={}", + "{}, z={}), box=({},{} .. {},{} valid={}), shown={}, lightSource={}", info.areaResrefView(), game::area_animation_kind_name(animation.kind), animation.resrefView(), animation.nameView(), animation.x, animation.y, - animation.objX, animation.objY, animation.objZ, animation.frameWidth, - animation.frameHeight, - animation.frameCenterX, animation.frameCenterY, animation.frameValid, - animation.isShown(), animation.isLightSource()); + animation.objX, animation.objY, animation.objZ, animation.envelope.left, + animation.envelope.top, animation.envelope.right, animation.envelope.bottom, + animation.envelope.valid, animation.isShown(), animation.isLightSource()); continue; } if (unclassifiedListed < kMaxUnclassifiedResrefsLogged && !animation.resrefView().empty() && @@ -187,6 +199,29 @@ void refresh_area_animations(AppContext& ctx, const game::CGameArea* area, return; } info.areaResref = areaResref; + // Merge with the render-thread envelope cache in both directions: a + // direct read seeds the cache, a cached box covers a released resource. + std::size_t replaced = 0; + std::size_t boxed = 0; + { + std::lock_guard envelopeLock(g_staticEnvelopeMutex); + for (auto& animation : info.animations) { + if (!animation.object || + !game::should_replace_animation_draw(animation.resrefView(), animation.kind)) { + continue; + } + ++replaced; + if (animation.envelope.valid) { + if (g_staticEnvelopes.size() < kMaxStaticEnvelopes) { + g_staticEnvelopes.insert_or_assign(animation.object, animation.envelope); + } + } else if (const auto cached = g_staticEnvelopes.find(animation.object); + cached != g_staticEnvelopes.end()) { + animation.envelope = cached->second; + } + if (animation.envelope.valid) ++boxed; + } + } auto snapshot = std::make_shared(std::move(info)); // Pack the shader point set outside the commit lock; publish it together @@ -215,6 +250,9 @@ void refresh_area_animations(AppContext& ctx, const game::CGameArea* area, logSummary = g_lastLoggedAreResref != areaResref; if (logSummary) g_lastLoggedAreResref = areaResref; } + LOG_DEBUG("ARE effect points: {} published, {} of {} replaced statics use their authored " + "BAM box", + effectPoints.size(), boxed, replaced); if (logSummary) { log_area_animation_summary(*snapshot); LOG_INFO("ARE effect points published: {} (pointEffects={})", effectPoints.size(), @@ -543,8 +581,43 @@ void refresh_wed_cache(AppContext& ctx, void* infGame) { } } +bool static_envelope_ready(const void* staticObject) noexcept { + if (!staticObject) return false; + try { + std::lock_guard envelopeLock(g_staticEnvelopeMutex); + if (g_staticEnvelopes.contains(staticObject)) return true; + game::BamEnvelope envelope{}; + if (g_staticEnvelopes.size() >= kMaxStaticEnvelopes || + !game::read_static_bam_envelope(staticObject, envelope)) { + return false; + } + g_staticEnvelopes.emplace(staticObject, envelope); + g_staticEnvelopesDirty.store(true, std::memory_order_release); + return true; + } catch (...) { + return false; + } +} + +void republish_area_animations_if_dirty(AppContext& ctx) noexcept { + if (!g_staticEnvelopesDirty.load(std::memory_order_acquire)) return; + // Statics come into view in bursts; coalesce them into one object walk. + static std::chrono::steady_clock::time_point lastRepublish{}; + const auto now = std::chrono::steady_clock::now(); + if (now - lastRepublish < kEnvelopeRepublishInterval) return; + lastRepublish = now; + g_staticEnvelopesDirty.store(false, std::memory_order_release); + refresh_area_animations(ctx, ctx.activeArea.load(), + g_areaRefreshGeneration.load(std::memory_order_acquire)); +} + void reset_gpu_area_state() noexcept { try { + { + std::lock_guard envelopeLock(g_staticEnvelopeMutex); + g_staticEnvelopes.clear(); + g_staticEnvelopesDirty.store(false, std::memory_order_release); + } std::lock_guard commitLock(g_areaRefreshCommitMutex); g_areaRefreshGeneration.fetch_add(1, std::memory_order_acq_rel); queue_no_liquid_snapshot(); diff --git a/src/iee/area_state.h b/src/iee/area_state.h index 0f6762e..a115182 100644 --- a/src/iee/area_state.h +++ b/src/iee/area_state.h @@ -30,6 +30,14 @@ bool read_view_transform(const game::CGameArea* area, ViewTransform& out); // Re-resolves the active area after LoadArea and caches its parsed WED into ctx. void refresh_wed_cache(AppContext& ctx, void* infGame); +// Render thread. True once the authored BAM draw box of a replaced static is +// known (cached, or readable now because the engine has loaded its BAM). The +// CGameStatic::Render hook keeps the engine draw until then, so the first +// engine draw loads the resource the box is read from. +bool static_envelope_ready(const void* staticObject) noexcept; +// Render thread. Rebuilds the effect points after new draw boxes were +// captured; a no-op otherwise. +void republish_area_animations_if_dirty(AppContext& ctx) noexcept; // Publishes an immediate CPU-side no-liquid generation. The next render // thread flush replaces any previous area's GPU mask before drawing. void reset_gpu_area_state() noexcept; diff --git a/src/iee/game/are_animations.cpp b/src/iee/game/are_animations.cpp index 3cd7f2b..7e3ff76 100644 --- a/src/iee/game/are_animations.cpp +++ b/src/iee/game/are_animations.cpp @@ -170,6 +170,10 @@ bool AreaAnimationInfo::isShown() const noexcept { return (flags & kAreAnimationFlagIsShown) != 0; } +bool AreaAnimationInfo::isMirrored() const noexcept { + return (flags & kAreAnimationFlagMirror) != 0; +} + bool AreaAnimationInfo::isLightSource() const noexcept { return (flags & kAreAnimationFlagNotLightSource) == 0; } @@ -286,6 +290,46 @@ bool should_replace_animation_draw(std::string_view resref, AreaAnimationKind ki return false; } +namespace { +// Moves the point to the bottom-center of the authored draw box and sizes it +// to that box, mirroring the X range the way RenderBam does. False when the +// envelope is unknown or degenerate. +bool apply_envelope(const AreaAnimationInfo& animation, AreaEffectPoint& point) noexcept { + const auto& envelope = animation.envelope; + if (!envelope.valid || envelope.right <= envelope.left || envelope.bottom <= envelope.top) { + return false; + } + const float left = animation.isMirrored() ? -static_cast(envelope.right) + : static_cast(envelope.left); + const float right = animation.isMirrored() ? -static_cast(envelope.left) + : static_cast(envelope.right); + point.x += (left + right) / 2.0f; + point.y += static_cast(envelope.bottom); + point.height = static_cast(envelope.bottom - envelope.top); + point.halfWidth = (std::max)((right - left) / 2.0f, 1.5f); + return true; +} +} // namespace + +void extend_bam_envelope(BamEnvelope& envelope, const frameTableEntry_st& frame) noexcept { + if (frame.nWidth == 0 || frame.nHeight == 0) return; + const auto clamp16 = [](int value) { + return static_cast((std::clamp)(value, -32768, 32767)); + }; + const auto left = clamp16(-frame.nCenterX); + const auto top = clamp16(-frame.nCenterY); + const auto right = clamp16(static_cast(frame.nWidth) - frame.nCenterX); + const auto bottom = clamp16(static_cast(frame.nHeight) - frame.nCenterY); + if (!envelope.valid) { + envelope = {true, left, top, right, bottom}; + return; + } + envelope.left = (std::min)(envelope.left, left); + envelope.top = (std::min)(envelope.top, top); + envelope.right = (std::max)(envelope.right, right); + envelope.bottom = (std::max)(envelope.bottom, bottom); +} + std::vector build_area_effect_points(const AreaAnimationsInfo& info) { std::vector points; points.reserve((std::min)(info.animations.size(), kMaxAreaEffectPoints)); @@ -312,26 +356,15 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& point.halfWidth = 10.0f; return point; } - // RenderBam places the frame's top-left at objectPos - frameCenter, - // while the procedural flame grows upward from its bottom-center. Move - // the point to that authored bottom-center before suppressing the BAM. - // Note: while the effect suppresses the engine draw, CVidCell::m_pFrame - // normally stays null, so the reviewed per-resref table is the normal - // path for replaced flames. const auto upper = upper_copy(resref); point.reserved1 = upper.find("BLU") != std::string::npos ? 1.0f : 0.0f; // palette id - if (animation.frameValid) { - point.height = static_cast(animation.frameHeight); - point.halfWidth = - (std::max)(static_cast(animation.frameWidth) / 2.0f, 1.5f); - point.x += static_cast(animation.frameWidth) / 2.0f - - static_cast(animation.frameCenterX); - point.y += static_cast(animation.frameHeight - animation.frameCenterY); - } else { + if (!apply_envelope(animation, point)) { + // BAM not loaded yet: reviewed per-resref table until the engine + // has drawn the object once. const auto& geometry = flame_geometry_for(upper); point.height = geometry.height; point.halfWidth = geometry.halfWidth; - point.x += geometry.dx; + point.x += animation.isMirrored() ? -geometry.dx : geometry.dx; point.y += geometry.dy; } return point; @@ -341,8 +374,10 @@ std::vector build_area_effect_points(const AreaAnimationsInfo& return std::nullopt; // authored plume art stays; nothing to add } point.kind = base; - point.height = 170.0f; - point.halfWidth = 11.0f; + if (!apply_envelope(animation, point)) { + point.height = 170.0f; + point.halfWidth = 11.0f; + } return point; } case AreaAnimationKind::Light: { diff --git a/src/iee/game/are_animations.h b/src/iee/game/are_animations.h index d13c2dc..696a2aa 100644 --- a/src/iee/game/are_animations.h +++ b/src/iee/game/are_animations.h @@ -31,6 +31,23 @@ enum class AreaAnimationKind : int { [[nodiscard]] AreaAnimationKind classify_area_animation(std::string_view resref, std::string_view name) noexcept; +struct frameTableEntry_st; + +// Union of a BAM cycle's frame draw boxes, relative to the object anchor and +// unmirrored: RenderBam draws each frame's top-left at (anchor - center), so +// a frame spans [-cx, w - cx] x [-cy, h - cy]. Stable across the animation, +// unlike any single frame. +struct BamEnvelope { + bool valid{}; + std::int16_t left{}; + std::int16_t top{}; + std::int16_t right{}; + std::int16_t bottom{}; +}; + +// Grows `envelope` by one frame's draw box. Empty frames are ignored. +void extend_bam_envelope(BamEnvelope& envelope, const frameTableEntry_st& frame) noexcept; + struct AreaAnimationInfo { std::uint16_t x{}; std::uint16_t y{}; @@ -44,14 +61,13 @@ struct AreaAnimationInfo { // map position. Loaded from the ARE record's height field; the memory walk // overrides it with the live value. std::int32_t objZ{}; - // The engine's cached current BAM frame entry (CVidCell::m_pFrame) — the - // exact geometry RenderBam draws with. Valid only when the memory walk - // could read it (the object has rendered at least once). - bool frameValid{}; - std::int16_t frameWidth{}; - std::int16_t frameHeight{}; - std::int16_t frameCenterX{}; - std::int16_t frameCenterY{}; + // Authored draw box from the BAM frame table. Valid only once the engine + // has loaded the BAM (the object has rendered at least once); until then + // the per-resref table is the fallback. + BamEnvelope envelope{}; + // The live CGameStatic this record came from (memory walk only). Identity + // key for the render-thread envelope cache; never dereferenced host-side. + const void* object{}; std::int16_t height{}; std::uint32_t schedule{}; std::uint32_t flags{}; @@ -65,6 +81,7 @@ struct AreaAnimationInfo { [[nodiscard]] std::string_view nameView() const noexcept; [[nodiscard]] bool isShown() const noexcept; [[nodiscard]] bool isLightSource() const noexcept; + [[nodiscard]] bool isMirrored() const noexcept; }; struct AreaAnimationsInfo { diff --git a/src/iee/game/file_formats.h b/src/iee/game/file_formats.h index e8e4881..a3729c2 100644 --- a/src/iee/game/file_formats.h +++ b/src/iee/game/file_formats.h @@ -187,6 +187,10 @@ namespace iee::game { inline constexpr std::uint32_t kAreAnimationFlagNoShadow = 1u << 1; inline constexpr std::uint32_t kAreAnimationFlagNotLightSource = 1u << 2; inline constexpr std::uint32_t kAreAnimationFlagDrawAsBackground = 1u << 8; + // RenderBam also draws every secondary cell (one per BAM sequence). + inline constexpr std::uint32_t kAreAnimationFlagAllSequences = 1u << 9; + // RenderBam flips the frame box around the object X (CInfinity::MIRROR_FX). + inline constexpr std::uint32_t kAreAnimationFlagMirror = 1u << 11; inline constexpr std::uint32_t kAreAnimationFlagUseWbm = 1u << 13; inline constexpr std::uint32_t kAreAnimationFlagUsePvrz = 1u << 15; @@ -226,6 +230,13 @@ namespace iee::game { std::uint32_t ___u4{}; }; + // BAM cycle entry; identical shape in V1 and V2. V1 indexes the frame + // lookup list from nStartingFrame, V2 indexes the frame table directly. + struct sequenceTableEntry_st { + std::int16_t nFrames{}; + std::uint16_t nStartingFrame{}; + }; + struct st_tiledef { std::int32_t nTile{}; std::int32_t nUsageCount{}; @@ -256,5 +267,6 @@ namespace iee::game { static_assert(sizeof(BAMHEADERV2) == 0x20); static_assert(sizeof(frame) == 0x18); static_assert(sizeof(frameTableEntry_st) == 0xC); + static_assert(sizeof(sequenceTableEntry_st) == 0x4); static_assert(sizeof(st_tiledef) == 0x18); } diff --git a/src/iee/game/object_statics.cpp b/src/iee/game/object_statics.cpp index c7b118f..670578a 100644 --- a/src/iee/game/object_statics.cpp +++ b/src/iee/game/object_statics.cpp @@ -60,6 +60,99 @@ bool decode_object_array_globals(const std::byte* function, std::size_t windowSi return true; } +namespace { +// Authored flame/smoke cycles are short; anything beyond these bounds is +// treated as a stale or foreign pointer rather than walked. +constexpr std::size_t kMaxEnvelopeSequences = 64; +constexpr std::size_t kMaxEnvelopeFramesPerSequence = 256; +constexpr std::uint16_t kMaxEnvelopeFrameDimension = 1024; +constexpr std::uint16_t kBamNoFrame = 0xFFFF; + +bool read_envelope_frame(const CResCell& cell, std::size_t frameIndex, std::size_t frameCount, + BamEnvelope& envelope) noexcept { + if (frameIndex >= frameCount) return false; + frameTableEntry_st frame{}; + if (!core::safe_read(cell.m_pFrames + frameIndex, frame)) return false; + if (frame.nWidth > kMaxEnvelopeFrameDimension || frame.nHeight > kMaxEnvelopeFrameDimension) { + return false; + } + extend_bam_envelope(envelope, frame); + return true; +} +} // namespace + +bool read_static_bam_envelope(const void* staticObject, BamEnvelope& out) noexcept { + out = {}; + if (!staticObject) return false; + try { + const auto* objectBytes = static_cast(staticObject); + CVidCell vidCell{}; + ARE_Animation_st header{}; + if (!core::safe_read(objectBytes + offsetof(CGameStatic, m_vidCell), vidCell) || + !core::safe_read(objectBytes + offsetof(CGameStatic, m_header), header) || + !vidCell.pRes) { + return false; + } + CResCell cell{}; + if (!core::safe_read(vidCell.pRes, cell) || !cell.baseclass_0.bLoaded || + !cell.baseclass_0.pData || !cell.m_pFrames || !cell.m_pSequences) { + return false; + } + + // V2 (PVRZ-backed) cycles index the frame table directly; V1 goes + // through the frame lookup list. + std::size_t frameCount = 0; + std::size_t sequenceCount = 0; + bool direct = false; + if (cell.m_pBamHeaderV2) { + BAMHEADERV2 v2{}; + if (!core::safe_read(cell.m_pBamHeaderV2, v2)) return false; + frameCount = v2.nFrames; + sequenceCount = v2.nSequences; + direct = true; + } else if (cell.m_pBamHeader && cell.m_pFrameList) { + bamHeader_st v1{}; + if (!core::safe_read(cell.m_pBamHeader, v1)) return false; + frameCount = v1.nFrames; + sequenceCount = v1.nSequences; + } + if (frameCount == 0 || sequenceCount == 0) return false; + + std::size_t first = vidCell.m_nCurrentSequence < sequenceCount ? vidCell.m_nCurrentSequence : 0; + std::size_t last = first + 1; + if ((header.nFlags & kAreAnimationFlagAllSequences) != 0) { + first = 0; + last = (std::min)(sequenceCount, kMaxEnvelopeSequences); + } + + BamEnvelope envelope{}; + for (std::size_t sequence = first; sequence < last; ++sequence) { + sequenceTableEntry_st entry{}; + if (!core::safe_read(cell.m_pSequences + sequence, entry)) return false; + if (entry.nFrames <= 0 || entry.nStartingFrame == kBamNoFrame) continue; + const auto count = + (std::min)(static_cast(entry.nFrames), kMaxEnvelopeFramesPerSequence); + for (std::size_t offset = 0; offset < count; ++offset) { + std::size_t frameIndex = static_cast(entry.nStartingFrame) + offset; + if (!direct) { + if (frameIndex >= cell.m_nFrameList) return false; + std::uint16_t listed = kBamNoFrame; + if (!core::safe_read(cell.m_pFrameList + frameIndex, listed)) return false; + if (listed == kBamNoFrame) continue; + frameIndex = listed; + } + if (!read_envelope_frame(cell, frameIndex, frameCount, envelope)) return false; + } + } + if (!envelope.valid) return false; + out = envelope; + return true; + } catch (...) { + out = {}; + return false; + } +} + bool collect_area_static_animations(const ObjectArrayGlobals& globals, const CGameArea* area, AreaAnimationsInfo& out) noexcept { out = {}; @@ -109,23 +202,10 @@ bool collect_area_static_animations(const ObjectArrayGlobals& globals, const CGa if (core::safe_read(objectBytes + offsetof(CGameObject, m_posZ), objectPosZ)) { info.objZ = objectPosZ; } - // The engine caches the current frame entry it renders with - // (CVidCell::m_pFrame); mirror its geometry so no per-resref table is - // needed. Null until the object has rendered once — the render-thread - // re-refresh after a transition sees it populated. - void* framePointer = nullptr; - frameTableEntry_st frame{}; - if (core::safe_read( - objectBytes + offsetof(CGameStatic, m_vidCell) + offsetof(CVidCell, m_pFrame), - framePointer) && - framePointer && core::safe_read(framePointer, frame) && frame.nWidth > 0 && - frame.nHeight > 0) { - info.frameValid = true; - info.frameWidth = static_cast(frame.nWidth); - info.frameHeight = static_cast(frame.nHeight); - info.frameCenterX = frame.nCenterX; - info.frameCenterY = frame.nCenterY; - } + info.object = entry.m_objectPtr; + // Valid only once the engine has loaded the BAM; the render-thread + // envelope cache covers statics whose resource was released since. + (void)read_static_bam_envelope(entry.m_objectPtr, info.envelope); out.animations.push_back(info); if (out.animations.size() >= kMaxAreaAnimationRecords) { break; diff --git a/src/iee/game/object_statics.h b/src/iee/game/object_statics.h index 127da07..9acc278 100644 --- a/src/iee/game/object_statics.h +++ b/src/iee/game/object_statics.h @@ -29,6 +29,14 @@ inline constexpr std::size_t kObjectArrayMaxEntries = 0x8000; [[nodiscard]] bool decode_object_array_globals(const std::byte* function, std::size_t windowSize, ObjectArrayGlobals& out) noexcept; +// Reads the authored draw box of a live CGameStatic's BAM straight from the +// engine's parsed frame table (CVidCell::pRes -> CResCell): the union over +// the current cycle, or over every cycle when the record draws all sequences. +// Bounded safe reads only. Returns false until the engine has loaded the BAM +// (first draw) and for anything that fails validation. +[[nodiscard]] bool read_static_bam_envelope(const void* staticObject, + BamEnvelope& out) noexcept; + // Walks the engine object array and collects the authored static-animation // records (CGameStatic::m_header) owned by `area` into classified entries. // Returns false when the array is unresolved or unreadable; an area with no diff --git a/src/iee/game/runtime_types_x64.h b/src/iee/game/runtime_types_x64.h index 86a2010..dce29ce 100644 --- a/src/iee/game/runtime_types_x64.h +++ b/src/iee/game/runtime_types_x64.h @@ -71,6 +71,27 @@ namespace iee::game { std::array m_resRef{}; }; + // Parsed BAM resource (EEex docs, 176 bytes). The table pointers are set + // by CResCell::Parse on Demand and are only meaningful while the base + // resource is loaded. + struct CResCell { + CRes baseclass_0{}; + void *pUncompressedData{}; + std::uint32_t nUncompressedSize{}; + std::byte _pad0[4]{}; + bamHeader_st *m_pBamHeader{}; + BAMHEADERV2 *m_pBamHeaderV2{}; + void *m_pQuads{}; + frameTableEntry_st *m_pFrames{}; + sequenceTableEntry_st *m_pSequences{}; + std::uint16_t *m_pFrameList{}; + std::uint16_t m_nFrameList{}; + std::byte _pad1[6]{}; + void *m_pPalette{}; + std::int32_t m_bParsing{}; + std::byte _pad2[4]{}; + }; + struct CResPVR { CRes baseclass_0{}; std::int32_t texture{}; @@ -145,7 +166,9 @@ namespace iee::game { struct CVidCell { void *vfptr{}; CVidImage baseclass_0{}; - std::array baseclass_1{}; + // CResHelper: the resource pointer, then its resref. + CResCell *pRes{}; + CResRef cResRef{}; std::int16_t m_nCurrentFrame{}; std::uint16_t m_nCurrentSequence{}; std::int32_t m_nAnimType{}; @@ -415,8 +438,9 @@ namespace iee::game { // Runtime object created from each ARE animation record. The engine keeps // the raw authored 76-byte record (CAreaFileStaticObject in the PDB, our // ARE_Animation_st) embedded at +0x60; script state (e.g. the shown flag - // toggled by StaticStart) mutates it in place. m_vidCell caches the - // current BAM frame entry (m_pFrame) that RenderBam draws with. + // toggled by StaticStart) mutates it in place. m_vidCell::m_pFrame is + // only non-null during a draw (Render3d clears it on exit), so authored + // geometry is read from the BAM frame table behind m_vidCell::pRes. struct CGameStatic { CGameObject baseclass_0{}; ARE_Animation_st m_header{}; @@ -491,6 +515,11 @@ namespace iee::game { static_assert(sizeof(CSize) == 0x8); static_assert(sizeof(CRect) == 0x10); static_assert(sizeof(CRes) == 0x58); + static_assert(sizeof(CResCell) == 0xB0); + static_assert(offsetof(CResCell, m_pBamHeader) == 0x68); + static_assert(offsetof(CResCell, m_pFrames) == 0x80); + static_assert(offsetof(CResCell, m_nFrameList) == 0x98); + static_assert(offsetof(CVidCell, pRes) == 0x108); static_assert(sizeof(CResRef) == 0x8); static_assert(sizeof(CResPVR) == 0x70); static_assert(sizeof(CResTileSet) == 0x60); diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index 740e382..06c59ce 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -209,6 +209,7 @@ void publish_view_state(bool force = false, bool flushGpuUpload = true) { area::refresh_wed_cache(*g_ctx, infGame); } } + area::republish_area_animations_if_dirty(*g_ctx); if (!g_ctx->wed.load()) { return; } @@ -292,7 +293,11 @@ static void detour_static_render(void* thisPtr, void* area, void* vidMode) { (header.nFlags & (game::kAreAnimationFlagUseWbm | game::kAreAnimationFlagUsePvrz)) == 0) { const auto info = game::make_area_animation_info(header); - if (game::should_replace_animation_draw(info.resrefView(), info.kind)) { + // Suppress only once the authored draw box is known: the engine's + // first draw loads the BAM that box is read from, and the point then + // lands exactly where RenderBam would have drawn. + if (game::should_replace_animation_draw(info.resrefView(), info.kind) && + area::static_envelope_ready(thisPtr)) { return; // replaced by the shader's point effects } } diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index daf49cf..386c6b9 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1573,16 +1573,33 @@ void test_collect_area_static_animations() { CGameArea areaB{}; std::array statics{}; - frameTableEntry_st liveFrame{}; - liveFrame.nWidth = 12; - liveFrame.nHeight = 40; - liveFrame.nCenterX = 6; - liveFrame.nCenterY = 30; + // A loaded two-cycle BAM V1: cycle 0 has two frames whose boxes differ, + // cycle 1 a wider one. The lookup list maps cycle slots to frame entries. + std::array bamFrames{}; + bamFrames[0] = {12, 40, 6, 30, 0}; // box -6,-30 .. 6,10 + bamFrames[1] = {16, 44, 9, 36, 0}; // box -9,-36 .. 7,8 + bamFrames[2] = {60, 10, 30, 5, 0}; // box -30,-5 .. 30,5 + std::array bamSequences{}; + bamSequences[0] = {2, 0}; + bamSequences[1] = {1, 2}; + std::array bamFrameList{1, 0, 2}; + bamHeader_st bamHeader{}; + bamHeader.nFrames = 3; + bamHeader.nSequences = 2; + std::byte bamData{}; + CResCell bam{}; + bam.baseclass_0.pData = &bamData; + bam.baseclass_0.bLoaded = true; + bam.m_pBamHeader = &bamHeader; + bam.m_pFrames = bamFrames.data(); + bam.m_pSequences = bamSequences.data(); + bam.m_pFrameList = bamFrameList.data(); + bam.m_nFrameList = static_cast(bamFrameList.size()); statics[0].baseclass_0.m_objectType = kGameObjectTypeStatic; statics[0].baseclass_0.m_pArea = &areaA; statics[0].baseclass_0.m_posZ = 25; - statics[0].m_vidCell.m_pFrame = &liveFrame; + statics[0].m_vidCell.pRes = &bam; statics[0].m_header.rrAnimation = {'F', 'L', 'A', 'M', 'B', 'I', 'G', 0}; const char fireName[] = "FLAMBIG"; std::memcpy(statics[0].m_header.szName.data(), fireName, sizeof(fireName) - 1); @@ -1619,10 +1636,34 @@ void test_collect_area_static_animations() { "Collected records should carry the live header fields"); expect_true(!out.animations.empty() && out.animations[0].objZ == 25, "The walk mirrors the live m_posZ elevation"); - expect_true(!out.animations.empty() && out.animations[0].frameValid && - out.animations[0].frameWidth == 12 && out.animations[0].frameHeight == 40 && - out.animations[0].frameCenterX == 6 && out.animations[0].frameCenterY == 30, - "The walk mirrors the engine's cached CVidCell frame geometry"); + expect_true(!out.animations.empty() && out.animations[0].object == &statics[0], + "The walk records the live object as the envelope cache key"); + const auto box = out.animations.empty() ? BamEnvelope{} : out.animations[0].envelope; + expect_true(box.valid && box.left == -9 && box.top == -36 && box.right == 7 && box.bottom == 10, + "The walk unions the current cycle's frame boxes from the BAM frame table"); + + BamEnvelope direct{}; + statics[0].m_header.nFlags |= kAreAnimationFlagAllSequences; + expect_true(read_static_bam_envelope(&statics[0], direct) && direct.left == -30 && + direct.top == -36 && direct.right == 30 && direct.bottom == 10, + "A draw-all-sequences record unions every cycle"); + statics[0].m_header.nFlags &= ~kAreAnimationFlagAllSequences; + + bam.baseclass_0.bLoaded = false; + expect_true(!read_static_bam_envelope(&statics[0], direct) && !direct.valid, + "An unloaded BAM has no trustworthy frame table"); + bam.baseclass_0.bLoaded = true; + bam.m_nFrameList = 1; + expect_true(!read_static_bam_envelope(&statics[0], direct), + "A cycle that overruns the frame lookup list fails closed"); + bam.m_nFrameList = static_cast(bamFrameList.size()); + bamFrames[1].nWidth = 5000; + expect_true(!read_static_bam_envelope(&statics[0], direct), + "An implausible frame size fails closed"); + bamFrames[1].nWidth = 16; + expect_true(!read_static_bam_envelope(nullptr, direct) && + !read_static_bam_envelope(&statics[1], direct), + "A null object or a cell without a resource yields no envelope"); AreaAnimationsInfo invalidOut{}; expect_true(!collect_area_static_animations(ObjectArrayGlobals{}, &areaA, invalidOut), @@ -1670,17 +1711,34 @@ void test_build_area_effect_points() { animation.flags = kAreAnimationFlagIsShown; const char liveResref[] = "flamblu2"; for (std::size_t c = 0; liveResref[c] != '\0'; ++c) animation.resref[c] = liveResref[c]; - animation.frameValid = true; - animation.frameWidth = 8; - animation.frameHeight = 15; - animation.frameCenterX = 0; - animation.frameCenterY = 0; + animation.envelope = {true, -2, -12, 6, 3}; // off-centre box, 8 wide, 15 tall live.animations.push_back(animation); - const auto livePoints = build_area_effect_points(live); - expect_true(livePoints.size() == 1 && livePoints[0].x == 104.0f && - livePoints[0].y == 185.0f && livePoints[0].height == 15.0f && + auto livePoints = build_area_effect_points(live); + expect_true(livePoints.size() == 1 && livePoints[0].x == 102.0f && + livePoints[0].y == 173.0f && livePoints[0].height == 15.0f && livePoints[0].halfWidth == 4.0f && livePoints[0].reserved1 == 1.0f, - "Live frame geometry moves the object origin to the flame bottom-center"); + "The authored box moves the object origin to the flame bottom-center"); + + live.animations[0].flags |= kAreAnimationFlagMirror; + livePoints = build_area_effect_points(live); + expect_true(livePoints.size() == 1 && livePoints[0].x == 98.0f && + livePoints[0].y == 173.0f && livePoints[0].halfWidth == 4.0f, + "A mirrored record flips the box around the object X like RenderBam"); + + AreaAnimationInfo smoke{}; + smoke.kind = AreaAnimationKind::Smoke; + smoke.objX = 300; + smoke.objY = 400; + smoke.flags = kAreAnimationFlagIsShown; + const char smokeResref[] = "CHIMSMK"; + for (std::size_t c = 0; smokeResref[c] != '\0'; ++c) smoke.resref[c] = smokeResref[c]; + smoke.envelope = {true, -20, -90, 10, -10}; + live.animations = {smoke}; + livePoints = build_area_effect_points(live); + expect_true(livePoints.size() == 1 && livePoints[0].x == 295.0f && + livePoints[0].y == 390.0f && livePoints[0].height == 80.0f && + livePoints[0].halfWidth == 15.0f, + "Smoke plumes take their base and size from the authored box too"); } const auto points = build_area_effect_points(info); From e99372e64edfa4914e3547b78bfee8c8b1816442 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 3 Oct 2026 20:16:04 +0200 Subject: [PATCH 23/57] tests: avoid initializer-list vector assignment (GCC 13 -Werror false positive) --- tests/iee_tests.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 386c6b9..e93d083 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1733,7 +1733,8 @@ void test_build_area_effect_points() { const char smokeResref[] = "CHIMSMK"; for (std::size_t c = 0; smokeResref[c] != '\0'; ++c) smoke.resref[c] = smokeResref[c]; smoke.envelope = {true, -20, -90, 10, -10}; - live.animations = {smoke}; + live.animations.clear(); + live.animations.push_back(smoke); livePoints = build_area_effect_points(live); expect_true(livePoints.size() == 1 && livePoints[0].x == 295.0f && livePoints[0].y == 390.0f && livePoints[0].height == 80.0f && From 8f42cb589ed18cb6894f8fe1e27d5a6437020261 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 3 Oct 2026 20:35:40 +0200 Subject: [PATCH 24/57] Add sprite movement smoothing between logic ticks The engine moves creatures on its 30 Hz logic tick while EEex's uncapped renderer draws many frames per tick, so sprites step while the camera scrolls smoothly. SpriteMotionTracker (host-tested) slides each sprite from its last shown position to its newest logic position over the observed tick interval. Hooks on CGameSprite::Render, RenderMarkers and RenderHealthBar write the smoothed position into CGameObject::m_pos for the engine call and restore the logic position afterwards. Off by default ([Rendering] SmoothSpriteMovement); the three targets are manifest patterns for 2.7.3 and resolve as a set or not at all. --- CMakeLists.txt | 1 + docs/architecture.md | 11 ++ ...26-10-03-graphics-uplift-roadmap-design.md | 19 ++- src/iee/core/config.cpp | 3 + src/iee/core/config.h | 1 + src/iee/game/build_manifest.cpp | 12 +- src/iee/game/build_manifest.h | 9 ++ src/iee/game/game_addrs.cpp | 34 +++++- src/iee/game/game_addrs.h | 5 + src/iee/game/sprite_motion.cpp | 49 ++++++++ src/iee/game/sprite_motion.h | 57 +++++++++ src/iee/hooks.cpp | 110 ++++++++++++++++++ tests/iee_tests.cpp | 49 ++++++++ tools/InfinityEngine-Enhancer.sample.ini | 3 + 14 files changed, 355 insertions(+), 8 deletions(-) create mode 100644 src/iee/game/sprite_motion.cpp create mode 100644 src/iee/game/sprite_motion.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c6a360..78c9ff3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ add_library(iee_common STATIC src/iee/game/object_statics.cpp src/iee/game/resref_runtime.cpp src/iee/game/shader_override.cpp + src/iee/game/sprite_motion.cpp src/iee/game/tile_liquid.cpp src/iee/game/tile_upscale.cpp src/iee/game/tis_palette.cpp diff --git a/docs/architecture.md b/docs/architecture.md index aea2d12..2d71264 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -69,6 +69,17 @@ The supported runtime is intentionally narrow: one EEex-loaded Windows DLL, one - The classification table grows from runtime logs of unclassified resrefs, not speculation. See [are-animation-detection.md](are-animation-detection.md). +`src/iee/game/sprite_motion.*` + +- Host-safe `SpriteMotionTracker`: per-sprite slide from the last shown + position to the newest logic position over one observed tick interval. +- Consumed by the sprite render hooks in `hooks.cpp` (`CGameSprite::Render`, + `RenderMarkers`, `RenderHealthBar`), which write the smoothed position into + `CGameObject::m_pos` for the duration of the engine call and restore the + logic position afterwards. Gated by `[Rendering] SmoothSpriteMovement` + (default off); the three targets resolve as a set or not at all, and only + the 2.7.3 manifest carries their patterns. + `src/iee/game/object_statics.*` - Host-safe decode of the `CGameObjectArray` globals out of the engine's diff --git a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md index d33a3d0..1649507 100644 --- a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md +++ b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md @@ -119,12 +119,19 @@ Sprites move in 30 Hz steps while the camera scrolls at display rate. Lerp each sprite's draw position between ticks inside a `CGameSprite::Render` hook (swap position in, call original, restore). -- Timing: the hook measures tick boundaries itself (for example from - `CGameSprite::AIUpdate` `0x3447F0` or `CInfGame::SynchronousUpdate` - `0x292010`). -- **Gate:** confirm the previous-tick position source. `m_posOld` exists but - its use in `Render` looks like a motion trail; do not assume semantics. -- Risk: selection circles, health bars and markers must move with the sprite. +- Implemented 2026-10-03 behind `[Rendering] SmoothSpriteMovement` (default + off, 2.7.3 only); in-game validation pending. +- No engine "previous position" is used: `SpriteMotionTracker` observes each + sprite's `m_pos` at render time, treats a change as a logic tick, and slides + from the last shown position over the interval between the last two + changes (30 Hz assumed until observed). No tick hook is needed. +- `CGameSprite::RenderMarkers` (`0x36F170`) and `RenderHealthBar` + (`0x36E820`) are wrapped with the same swap and share one per-frame + timestamp, so circles and bars stay glued to the sprite. +- Known limits: positions are whole world pixels; projectiles and effects + still step at the logic rate; anything else that reads `m_pos` during + render (floating text, action icons drawn outside the three hooks) is not + smoothed. ### B. `fpSprite` replacement diff --git a/src/iee/core/config.cpp b/src/iee/core/config.cpp index 24c96d4..b311618 100644 --- a/src/iee/core/config.cpp +++ b/src/iee/core/config.cpp @@ -94,6 +94,8 @@ static void apply_kv(EngineConfig& cfg, const std::string& section, const std::s assign_float(cfg.maxAnisotropy); else if (iequals(key, "LODBias")) assign_float(cfg.lodBias); + else if (iequals(key, "SmoothSpriteMovement")) + assign_bool(cfg.smoothSpriteMovement); return; } @@ -189,6 +191,7 @@ bool ConfigManager::save(const std::filesystem::path& path, const EngineConfig& write_bool(f, "EnableAnisotropicFiltering", cfg.enableAnisotropicFiltering); f << "MaxAnisotropy = " << cfg.maxAnisotropy << "\n"; f << "LODBias = " << cfg.lodBias << "\n"; + write_bool(f, "SmoothSpriteMovement", cfg.smoothSpriteMovement); write_section(f, "Detection"); write_bool(f, "AreaAnimationScan", cfg.enableAreaAnimationScan); diff --git a/src/iee/core/config.h b/src/iee/core/config.h index 50f1d98..63ac7c0 100644 --- a/src/iee/core/config.h +++ b/src/iee/core/config.h @@ -7,6 +7,7 @@ struct EngineConfig { bool enableAnisotropicFiltering = false; float maxAnisotropy = 8.0f; float lodBias = -0.25f; + bool smoothSpriteMovement = false; bool dumpEngineShaders = false; bool enableDebugHotkeys = false; diff --git a/src/iee/game/build_manifest.cpp b/src/iee/game/build_manifest.cpp index 0d03452..2190ef0 100644 --- a/src/iee/game/build_manifest.cpp +++ b/src/iee/game/build_manifest.cpp @@ -185,8 +185,14 @@ constexpr BuildManifest kKnownBuilds[] = { // CGameStatic::Render (offline-verified unique at 0x1F27D0). "40 55 56 57 48 83 EC 50 48 8B 05 ? ? ? ? 48 33 C4 48 89 44 24 30 " "48 8B 05 ? ? ? ? 49 8B F0 48 8B EA 48 8B F9", + // CGameSprite::Render / RenderMarkers / RenderHealthBar. PDB-named + // on the Steam 2.7.3.0 binary; each offline-verified unique. + "48 89 5C 24 10 55 56 57 41 54 41 55 41 56 41 57 48 8D 6C 24 A0 48 81 EC 60 01 00 00 " + "48 8B 05 ? ? ? ? 48 33 C4 48 89 45 20 45 33 E4", + "40 55 53 56 57 41 56 48 8B EC 48 83 EC 60 48 8B 41 18 4C 8B F2 48 8B F1", + "4C 8B DC 55 57 49 8D AB 48 FF FF FF 48 81 EC A8 01 00 00", }, - {0x27EBD0, 0x4257C0, 0x276700, 0x1F27D0}, + {0x27EBD0, 0x4257C0, 0x276700, 0x1F27D0, 0x36BA50, 0x36F170, 0x36E820}, {0x100, 0x1DC, 0x14, 0x6590, 0x6598, 0x65F8}, {{ {"CRes_Demand", 0x36, BranchInstructionKind::CallRel32, 0xE8, 1, 5, true}, @@ -221,6 +227,10 @@ static_assert(validate_pattern_format(kKnownBuilds[1].patterns.renderTexture), "2.7.3 RenderTexture pattern format is invalid"); static_assert(validate_pattern_format(kKnownBuilds[1].patterns.objectArrayGetShare), "2.7.3 GetShare pattern format is invalid"); +static_assert(validate_pattern_format(kKnownBuilds[1].patterns.spriteRender) && + validate_pattern_format(kKnownBuilds[1].patterns.spriteRenderMarkers) && + validate_pattern_format(kKnownBuilds[1].patterns.spriteRenderHealthBar), + "2.7.3 sprite render pattern format is invalid"); static_assert(kKnownBuilds[1].validate(), "2.7.3 build manifest is invalid"); } // namespace diff --git a/src/iee/game/build_manifest.h b/src/iee/game/build_manifest.h index 54ff179..5978fa3 100644 --- a/src/iee/game/build_manifest.h +++ b/src/iee/game/build_manifest.h @@ -40,6 +40,12 @@ struct PatternSet { // fire/smoke BAM draws while the fpSEAM point effects replace them. Empty // or non-unique keeps the engine draws (effects stay additive). std::string_view staticRender{}; + // Optional: CGameSprite::Render and the two per-sprite overlay renders that + // read the same position. Hooked together for sprite movement smoothing; + // any empty or non-unique entry leaves the feature off. + std::string_view spriteRender{}; + std::string_view spriteRenderMarkers{}; + std::string_view spriteRenderHealthBar{}; }; struct ReferenceRvas { @@ -48,6 +54,9 @@ struct ReferenceRvas { // Diagnostic only; 0 means "not yet observed on this build". std::uintptr_t objectArrayGetShare{}; std::uintptr_t staticRender{}; + std::uintptr_t spriteRender{}; + std::uintptr_t spriteRenderMarkers{}; + std::uintptr_t spriteRenderHealthBar{}; }; struct RuntimeOffsets { diff --git a/src/iee/game/game_addrs.cpp b/src/iee/game/game_addrs.cpp index 2aaa1ab..2588128 100644 --- a/src/iee/game/game_addrs.cpp +++ b/src/iee/game/game_addrs.cpp @@ -13,7 +13,6 @@ namespace iee::game { bool resolve_addresses(GameAddresses &out, const core::EngineConfig &cfg, const BuildManifest &manifest) { - (void) cfg; out = {}; auto moduleInfo = core::get_module_span(nullptr); if (!moduleInfo || !moduleInfo->base || !moduleInfo->size) { @@ -80,6 +79,39 @@ namespace iee::game { } } + // Optional targets: sprite movement smoothing. Resolved only when + // requested, and only as a complete set. + if (cfg.smoothSpriteMovement) { + const auto resolveOptional = [&](const char *name, std::string_view pattern, + std::uintptr_t referenceRva) -> std::uintptr_t { + if (pattern.empty()) return 0; + std::size_t matches = 0; + auto target = reinterpret_cast( + core::find_unique_in_module(nullptr, pattern, &matches)); + recover(name, target, matches, referenceRva, pattern); + if (!target) LOG_WARN("{} pattern matched {} times", name, matches); + return target; + }; + const auto &patterns = manifest.patterns; + const auto &rvas = manifest.referenceRvas; + out.SpriteRender = + resolveOptional("CGameSprite::Render", patterns.spriteRender, rvas.spriteRender); + out.SpriteRenderMarkers = resolveOptional( + "CGameSprite::RenderMarkers", patterns.spriteRenderMarkers, rvas.spriteRenderMarkers); + out.SpriteRenderHealthBar = + resolveOptional("CGameSprite::RenderHealthBar", patterns.spriteRenderHealthBar, + rvas.spriteRenderHealthBar); + if (out.SpriteRender && out.SpriteRenderMarkers && out.SpriteRenderHealthBar) { + LOG_INFO("Sprite render targets resolved at RVA 0x{:X} / 0x{:X} / 0x{:X}", + out.SpriteRender - moduleBase, out.SpriteRenderMarkers - moduleBase, + out.SpriteRenderHealthBar - moduleBase); + } else { + out.SpriteRender = out.SpriteRenderMarkers = out.SpriteRenderHealthBar = 0; + LOG_WARN("Sprite movement smoothing disabled: this build has no complete set of " + "sprite render targets"); + } + } + const bool success = out.LoadArea && out.RenderTexture; if (success) { diff --git a/src/iee/game/game_addrs.h b/src/iee/game/game_addrs.h index 3a3f16e..356cd57 100644 --- a/src/iee/game/game_addrs.h +++ b/src/iee/game/game_addrs.h @@ -14,6 +14,11 @@ namespace iee::game { // Optional (0 when the pattern did not resolve uniquely): the point // effects then stay additive instead of replacing the engine draws. std::uintptr_t StaticRender = 0; + // Optional, all-or-nothing: sprite movement smoothing needs every + // render that reads the sprite position. + std::uintptr_t SpriteRender = 0; + std::uintptr_t SpriteRenderMarkers = 0; + std::uintptr_t SpriteRenderHealthBar = 0; bool initialized = false; }; diff --git a/src/iee/game/sprite_motion.cpp b/src/iee/game/sprite_motion.cpp new file mode 100644 index 0000000..ee1a4b5 --- /dev/null +++ b/src/iee/game/sprite_motion.cpp @@ -0,0 +1,49 @@ +#include "sprite_motion.h" + +#include +#include +#include +#include + +namespace iee::game { +MotionPoint SpriteMotionTracker::shown(const State& state, double now) noexcept { + const double progress = std::clamp((now - state.changedAt) / state.interval, 0.0, 1.0); + const auto mix = [progress](std::int32_t from, std::int32_t to) { + return static_cast(std::lround(from + (to - from) * progress)); + }; + return {mix(state.from.x, state.to.x), mix(state.from.y, state.to.y)}; +} + +void SpriteMotionTracker::prune(double now) { + for (auto it = states_.begin(); it != states_.end();) { + it = now - it->second.lastSeen > kStaleSeconds ? states_.erase(it) : std::next(it); + } +} + +MotionPoint SpriteMotionTracker::sample(const void* key, MotionPoint current, double now) { + auto [it, inserted] = states_.try_emplace(key); + auto& state = it->second; + if (inserted) { + state = {current, current, now, kDefaultInterval, now}; + if (states_.size() > kPruneThreshold) prune(now); + return current; + } + + state.lastSeen = now; + if (current != state.to) { + const auto jump = (std::max)(std::abs(current.x - state.to.x), std::abs(current.y - state.to.y)); + if (jump > kSnapDistance) { + state.from = current; + } else { + // Start from what is on screen now so an early tick never jumps back. + state.from = shown(state, now); + const double observed = now - state.changedAt; + state.interval = + observed >= kMinInterval && observed <= kMaxInterval ? observed : kDefaultInterval; + } + state.to = current; + state.changedAt = now; + } + return shown(state, now); +} +} // namespace iee::game diff --git a/src/iee/game/sprite_motion.h b/src/iee/game/sprite_motion.h new file mode 100644 index 0000000..e6d28ff --- /dev/null +++ b/src/iee/game/sprite_motion.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include + +namespace iee::game { +struct MotionPoint { + std::int32_t x{}; + std::int32_t y{}; + + friend bool operator==(const MotionPoint&, const MotionPoint&) = default; +}; + +// Render-rate smoothing of positions that the engine only updates on its +// logic tick. Each key (a live sprite) is drawn sliding from where it was +// last shown to its newest logic position over one observed tick interval, +// so the picture trails the logic by at most one tick. +// +// Host-safe and single-threaded: the caller owns the clock and the thread. +class SpriteMotionTracker { + public: + // Logic-tick interval assumed until two consecutive moves are observed. + static constexpr double kDefaultInterval = 1.0 / 30.0; + // Observed intervals outside this range are not a steady walk (first step + // after standing still, a hitch) and fall back to the default. + static constexpr double kMinInterval = 1.0 / 90.0; + static constexpr double kMaxInterval = 1.0 / 14.0; + // A larger single-tick move is a teleport or a reused object address and + // is shown immediately. + static constexpr std::int32_t kSnapDistance = 48; + static constexpr std::size_t kPruneThreshold = 1024; + static constexpr double kStaleSeconds = 5.0; + + // `current` is the engine's position for `key` at monotonic time `now` + // (seconds). Returns the position to draw at. Repeated calls with the same + // arguments return the same result. + [[nodiscard]] MotionPoint sample(const void* key, MotionPoint current, double now); + + void clear() noexcept { states_.clear(); } + [[nodiscard]] std::size_t size() const noexcept { return states_.size(); } + + private: + struct State { + MotionPoint from{}; + MotionPoint to{}; + double changedAt{}; + double interval{kDefaultInterval}; + double lastSeen{}; + }; + + [[nodiscard]] static MotionPoint shown(const State& state, double now) noexcept; + void prune(double now); + + std::unordered_map states_; +}; +} // namespace iee::game diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index 06c59ce..6dd32c6 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -17,6 +18,8 @@ #include "iee/frame_hook.h" #include "iee/game/game_types.h" #include "iee/game/renderer.h" +#include "iee/game/runtime_types_x64.h" +#include "iee/game/sprite_motion.h" #include "iee/shader_probe.h" namespace iee::hooks { @@ -33,8 +36,16 @@ static core::Hook g_loadAreaHook; static core::Hook g_renderTextureHook; static core::Hook g_drawColorToneHook; static core::Hook g_staticRenderHook; +// CGameSprite::Render(area, vidMode) and the (vidMode) overlay renders share +// one pass-through signature; the unused third argument is harmless on x64. +using SpriteRenderFn = void (*)(void*, void*, void*); +static core::Hook g_spriteRenderHook; +static core::Hook g_spriteMarkersHook; +static core::Hook g_spriteHealthBarHook; static AppContext* g_ctx = nullptr; +// Raised by LoadArea; the sprite-smoothing tracker clears itself on its own thread. +static std::atomic g_spriteMotionReset{false}; namespace { void record_render_performance(bool enabled, bool handled, long long elapsedTicks) noexcept { @@ -237,6 +248,7 @@ static void* detour_load_area(void* thisPtr, void* pAreaNameString, unsigned cha area::reset_gpu_area_state(); ctx.reset_area_state(); features::request_tile_render_state_reset(); + g_spriteMotionReset.store(true, std::memory_order_release); game::request_texture_configuration_cache_reset(); } catch (const std::exception& e) { LOG_ERROR("LoadArea pre-dispatch failed; continuing with the engine path: {}", e.what()); @@ -278,6 +290,77 @@ static void detour_draw_color_tone(int mode) { g_drawColorToneHook.original()(mode); } +// Sprite movement smoothing. The engine moves creatures on its logic tick +// (30 Hz) while EEex's uncapped renderer draws many frames per tick, so every +// render that reads the sprite position is wrapped: the smoothed position is +// written into CGameObject::m_pos for the duration of the engine call and the +// logic position is restored afterwards. All of it runs on the engine's main +// thread; LoadArea only raises the reset flag. +static game::SpriteMotionTracker g_spriteMotion; +static int g_spritePositionSwapDepth = 0; + +// One timestamp per presented frame, so a sprite, its selection circle and +// its health bar are all placed at the same smoothed position. +static double sprite_motion_frame_seconds() noexcept { + static unsigned long long lastFrame = ~0ull; + static double seconds = 0.0; + const auto frameNumber = frame::frame_count(); + if (frameNumber != lastFrame || frameNumber == 0) { + lastFrame = frameNumber; + static const double frequency = [] { + LARGE_INTEGER value{}; + QueryPerformanceFrequency(&value); + return static_cast(value.QuadPart); + }(); + LARGE_INTEGER counter{}; + QueryPerformanceCounter(&counter); + seconds = static_cast(counter.QuadPart) / frequency; + } + return seconds; +} + +static void call_with_smoothed_position(core::Hook& hook, void* sprite, void* a, + void* b) { + const auto original = hook.original(); + // Nested sprite renders already see the smoothed position. + if (!sprite || !g_ctx || g_spritePositionSwapDepth > 0) { + original(sprite, a, b); + return; + } + auto* position = reinterpret_cast(static_cast(sprite) + + offsetof(game::CGameObject, m_pos)); + game::CPoint logic{}; + bool swapped = false; + try { + if (g_spriteMotionReset.exchange(false, std::memory_order_acq_rel)) g_spriteMotion.clear(); + if (core::safe_read(position, logic)) { + const auto shown = + g_spriteMotion.sample(sprite, {logic.x, logic.y}, sprite_motion_frame_seconds()); + if (shown.x != logic.x || shown.y != logic.y) { + position->x = shown.x; + position->y = shown.y; + swapped = true; + } + } + } catch (...) { + // Smoothing is cosmetic; any doubt draws at the logic position. + } + ++g_spritePositionSwapDepth; + original(sprite, a, b); + --g_spritePositionSwapDepth; + if (swapped) *position = logic; +} + +static void detour_sprite_render(void* sprite, void* a, void* b) { + call_with_smoothed_position(g_spriteRenderHook, sprite, a, b); +} +static void detour_sprite_markers(void* sprite, void* a, void* b) { + call_with_smoothed_position(g_spriteMarkersHook, sprite, a, b); +} +static void detour_sprite_health_bar(void* sprite, void* a, void* b) { + call_with_smoothed_position(g_spriteHealthBarHook, sprite, a, b); +} + // CGameStatic::Render hook: while the fpSEAM point effects are active, the // authored fire/smoke BAM draws are replaced by our textured effects, so the // engine's own little flame/puff loops are skipped. Everything else (lights, @@ -397,6 +480,27 @@ bool install_all(AppContext& ctx) { } } + if (ctx.cfg.smoothSpriteMovement && ctx.addrs.SpriteRender && ctx.addrs.SpriteRenderMarkers && + ctx.addrs.SpriteRenderHealthBar) { + try { + g_spriteRenderHook.create(reinterpret_cast(ctx.addrs.SpriteRender), + reinterpret_cast(&detour_sprite_render)); + g_spriteMarkersHook.create(reinterpret_cast(ctx.addrs.SpriteRenderMarkers), + reinterpret_cast(&detour_sprite_markers)); + g_spriteHealthBarHook.create(reinterpret_cast(ctx.addrs.SpriteRenderHealthBar), + reinterpret_cast(&detour_sprite_health_bar)); + g_spriteRenderHook.enable(); + g_spriteMarkersHook.enable(); + g_spriteHealthBarHook.enable(); + LOG_INFO("Sprite movement smoothing hooks installed"); + } catch (...) { + (void)g_spriteHealthBarHook.remove(); + (void)g_spriteMarkersHook.remove(); + (void)g_spriteRenderHook.remove(); + LOG_WARN("Sprite movement smoothing hooks failed; creatures move at the logic rate"); + } + } + g_loadAreaHook.enable(); LOG_INFO("LoadArea hook enabled"); @@ -438,6 +542,9 @@ void uninstall_all() noexcept { } catch (...) { } + (void)g_spriteHealthBarHook.remove(); + (void)g_spriteMarkersHook.remove(); + (void)g_spriteRenderHook.remove(); (void)g_staticRenderHook.remove(); (void)g_drawColorToneHook.remove(); (void)g_renderTextureHook.remove(); @@ -458,6 +565,9 @@ void prepare_for_shutdown() noexcept { // state are torn down. MinHook itself stays initialized until // uninstall_all(), after every MinHook-backed subsystem has removed its // hooks. + (void)g_spriteHealthBarHook.disable(); + (void)g_spriteMarkersHook.disable(); + (void)g_spriteRenderHook.disable(); (void)g_staticRenderHook.disable(); (void)g_drawColorToneHook.disable(); (void)g_renderTextureHook.disable(); diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index e93d083..f7bc24b 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -20,6 +20,7 @@ #include "iee/core/performance_samples.h" #include "iee/features/tile_render.h" #include "iee/game/are_animations.h" +#include "iee/game/sprite_motion.h" #include "iee/game/area_texture.h" #include "iee/game/object_statics.h" #include "iee/game/build_manifest.h" @@ -1674,6 +1675,53 @@ void test_collect_area_static_animations() { "A negative max index must fail closed"); } +void test_sprite_motion_tracker() { + using namespace iee::game; + const double tick = SpriteMotionTracker::kDefaultInterval; + int a = 0; + int b = 0; + SpriteMotionTracker tracker; + + expect_true(tracker.sample(&a, {100, 200}, 10.0) == MotionPoint{100, 200}, + "A first sighting is drawn where the engine has it"); + expect_true(tracker.sample(&a, {100, 200}, 10.5) == MotionPoint{100, 200}, + "A stationary sprite is never moved"); + + // First step after standing still: slides over the default tick. + expect_true(tracker.sample(&a, {110, 200}, 11.0) == MotionPoint{100, 200}, + "A new logic position starts from what was on screen"); + expect_true(tracker.sample(&a, {110, 200}, 11.0 + tick / 2) == MotionPoint{105, 200}, + "Half a tick later the sprite is half way"); + expect_true(tracker.sample(&a, {110, 200}, 11.0 + tick / 2) == MotionPoint{105, 200}, + "Sampling twice in one frame is stable"); + expect_true(tracker.sample(&a, {110, 200}, 11.0 + tick * 3) == MotionPoint{110, 200}, + "It settles on the logic position and stays there"); + + // Steady walk at a slower logic rate: the observed interval is adopted. + const double slow = 1.0 / 20.0; + (void)tracker.sample(&a, {120, 200}, 12.0); + (void)tracker.sample(&a, {130, 200}, 12.0 + slow); + expect_true(tracker.sample(&a, {130, 200}, 12.0 + slow * 1.5) == MotionPoint{125, 200}, + "The slide spans the interval between the last two moves"); + + // A tick that arrives early continues from the on-screen position. + (void)tracker.sample(&b, {0, 0}, 20.0); + (void)tracker.sample(&b, {10, 0}, 21.0); + expect_true(tracker.sample(&b, {20, 0}, 21.0 + tick / 2) == MotionPoint{5, 0}, + "An early move never jumps backwards or forwards"); + + expect_true(tracker.sample(&b, {500, 500}, 22.0) == MotionPoint{500, 500}, + "A teleport-sized move is shown immediately"); + + tracker.clear(); + expect_eq(tracker.size(), std::size_t{0}, "clear() forgets every sprite"); + for (std::size_t i = 0; i <= SpriteMotionTracker::kPruneThreshold; ++i) { + (void)tracker.sample(reinterpret_cast(i + 1), {0, 0}, 30.0); + } + (void)tracker.sample(&a, {0, 0}, 30.0 + SpriteMotionTracker::kStaleSeconds + 1.0); + expect_eq(tracker.size(), std::size_t{1}, "Sprites not drawn for a while are forgotten"); +} + void test_build_area_effect_points() { using namespace iee::game; @@ -1848,6 +1896,7 @@ int main() { test_parse_are_animations(); test_decode_object_array_globals(); test_collect_area_static_animations(); + test_sprite_motion_tracker(); test_build_area_effect_points(); test_config_detection_section(); diff --git a/tools/InfinityEngine-Enhancer.sample.ini b/tools/InfinityEngine-Enhancer.sample.ini index a1d9005..cef5b12 100644 --- a/tools/InfinityEngine-Enhancer.sample.ini +++ b/tools/InfinityEngine-Enhancer.sample.ini @@ -10,6 +10,9 @@ PerformanceLogs = false EnableAnisotropicFiltering = false MaxAnisotropy = 8.0 LODBias = -0.25 +; Draw creatures gliding between the engine's logic ticks instead of stepping +; at the logic rate. Only useful with EEex "Uncap FPS"; BGEE 2.7.3 only. +SmoothSpriteMovement = false [Detection] ; Collect the current area's authored ambient animations from engine memory From 291e12e4982a3cc05dfe3b78978ba74047f99e16 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 3 Oct 2026 20:39:20 +0200 Subject: [PATCH 25/57] Point effects: do not bound the BAM frame lookup by m_nFrameList CResCell::Parse never sets m_nFrameList, so it is always 0 in the engine and the bound rejected every V1 BAM: no draw box was ever captured, the engine flame kept drawing and the replacement showed only as a halo. The listed index is still checked against the frame count and every read is bounded. --- src/iee/game/object_statics.cpp | 4 +++- src/iee/game/runtime_types_x64.h | 2 +- tests/iee_tests.cpp | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/iee/game/object_statics.cpp b/src/iee/game/object_statics.cpp index 670578a..43b5e8b 100644 --- a/src/iee/game/object_statics.cpp +++ b/src/iee/game/object_statics.cpp @@ -135,7 +135,9 @@ bool read_static_bam_envelope(const void* staticObject, BamEnvelope& out) noexce for (std::size_t offset = 0; offset < count; ++offset) { std::size_t frameIndex = static_cast(entry.nStartingFrame) + offset; if (!direct) { - if (frameIndex >= cell.m_nFrameList) return false; + // CResCell::Parse never fills m_nFrameList (it stays 0), so the + // lookup list has no usable length; the frame-count check on the + // listed index and the bounded reads are the guard. std::uint16_t listed = kBamNoFrame; if (!core::safe_read(cell.m_pFrameList + frameIndex, listed)) return false; if (listed == kBamNoFrame) continue; diff --git a/src/iee/game/runtime_types_x64.h b/src/iee/game/runtime_types_x64.h index dce29ce..0839c4b 100644 --- a/src/iee/game/runtime_types_x64.h +++ b/src/iee/game/runtime_types_x64.h @@ -85,7 +85,7 @@ namespace iee::game { frameTableEntry_st *m_pFrames{}; sequenceTableEntry_st *m_pSequences{}; std::uint16_t *m_pFrameList{}; - std::uint16_t m_nFrameList{}; + std::uint16_t m_nFrameList{}; // not set by CResCell::Parse; do not rely on it std::byte _pad1[6]{}; void *m_pPalette{}; std::int32_t m_bParsing{}; diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index f7bc24b..bcdac2d 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1595,7 +1595,7 @@ void test_collect_area_static_animations() { bam.m_pFrames = bamFrames.data(); bam.m_pSequences = bamSequences.data(); bam.m_pFrameList = bamFrameList.data(); - bam.m_nFrameList = static_cast(bamFrameList.size()); + bam.m_nFrameList = 0; // as in the engine: CResCell::Parse leaves it unset statics[0].baseclass_0.m_objectType = kGameObjectTypeStatic; statics[0].baseclass_0.m_pArea = &areaA; @@ -1654,10 +1654,10 @@ void test_collect_area_static_animations() { expect_true(!read_static_bam_envelope(&statics[0], direct) && !direct.valid, "An unloaded BAM has no trustworthy frame table"); bam.baseclass_0.bLoaded = true; - bam.m_nFrameList = 1; + bamFrameList[0] = 7; expect_true(!read_static_bam_envelope(&statics[0], direct), - "A cycle that overruns the frame lookup list fails closed"); - bam.m_nFrameList = static_cast(bamFrameList.size()); + "A lookup entry beyond the frame table fails closed"); + bamFrameList[0] = 1; bamFrames[1].nWidth = 5000; expect_true(!read_static_bam_envelope(&statics[0], direct), "An implausible frame size fails closed"); From 04f4315483a4cfcaa5d6f988a75fc2832d357152 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 3 Oct 2026 20:39:20 +0200 Subject: [PATCH 26/57] Sprite smoothing: restore the logic position only if unchanged If the engine moves the sprite while the smoothed position is written, its value is newer and must win. Also log the thread of the first sprite render and of LoadArea so the single-thread assumption can be checked from a field log. --- src/iee/hooks.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index 6dd32c6..3d8ab3d 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -242,7 +242,8 @@ static void* detour_load_area(void* thisPtr, void* pAreaNameString, unsigned cha auto& ctx = *g_ctx; try { core::advance_readability_cache_epoch(); - LOG_DEBUG("LoadArea called - resetting scale detection for new area"); + LOG_INFO("LoadArea called on thread {} - resetting scale detection for new area", + GetCurrentThreadId()); ctx.infGame.store(thisPtr, std::memory_order_relaxed); // Invalidate any older refresh before clearing its published CPU state. area::reset_gpu_area_state(); @@ -330,15 +331,24 @@ static void call_with_smoothed_position(core::Hook& hook, void* auto* position = reinterpret_cast(static_cast(sprite) + offsetof(game::CGameObject, m_pos)); game::CPoint logic{}; + std::int32_t shownX = 0; + std::int32_t shownY = 0; bool swapped = false; try { + static bool threadLogged = false; + if (!threadLogged) { + threadLogged = true; + LOG_INFO("Sprite smoothing: first sprite render on thread {}", GetCurrentThreadId()); + } if (g_spriteMotionReset.exchange(false, std::memory_order_acq_rel)) g_spriteMotion.clear(); if (core::safe_read(position, logic)) { const auto shown = g_spriteMotion.sample(sprite, {logic.x, logic.y}, sprite_motion_frame_seconds()); if (shown.x != logic.x || shown.y != logic.y) { - position->x = shown.x; - position->y = shown.y; + shownX = shown.x; + shownY = shown.y; + position->x = shownX; + position->y = shownY; swapped = true; } } @@ -348,7 +358,9 @@ static void call_with_smoothed_position(core::Hook& hook, void* ++g_spritePositionSwapDepth; original(sprite, a, b); --g_spritePositionSwapDepth; - if (swapped) *position = logic; + // Restore only our own write: if the engine moved the sprite meanwhile, its + // value is the newer logic position and must win. + if (swapped && position->x == shownX && position->y == shownY) *position = logic; } static void detour_sprite_render(void* sprite, void* a, void* b) { From b0e19615d25438fbe93402e621d7a3c8609991a3 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 3 Oct 2026 21:02:16 +0200 Subject: [PATCH 27/57] Sprite smoothing: cover BAM projectiles and forget stale keys CProjectileBAM::Render gets the same position swap with its own tracker and a larger snap limit, since projectiles cover far more ground per tick. The tracker now starts over for a key that was not sampled for half a second, so an object returning to view, or a new object at a reused address, never slides from a stale position. --- ...26-10-03-graphics-uplift-roadmap-design.md | 10 ++++- src/iee/game/build_manifest.cpp | 7 +++- src/iee/game/build_manifest.h | 4 ++ src/iee/game/game_addrs.cpp | 2 + src/iee/game/game_addrs.h | 1 + src/iee/game/sprite_motion.cpp | 7 +++- src/iee/game/sprite_motion.h | 10 ++++- src/iee/hooks.cpp | 39 +++++++++++++++---- tests/iee_tests.cpp | 15 ++++++- 9 files changed, 80 insertions(+), 15 deletions(-) diff --git a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md index 1649507..88de10b 100644 --- a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md +++ b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md @@ -128,8 +128,14 @@ each sprite's draw position between ticks inside a `CGameSprite::Render` hook - `CGameSprite::RenderMarkers` (`0x36F170`) and `RenderHealthBar` (`0x36E820`) are wrapped with the same swap and share one per-frame timestamp, so circles and bars stay glued to the sprite. -- Known limits: positions are whole world pixels; projectiles and effects - still step at the logic rate; anything else that reads `m_pos` during +- In-game result 2026-10-03 (owner): works, visibly smoother. The field log + shows sprite renders and `LoadArea` on the same thread, so the position + swap cannot race the logic tick. +- `CProjectileBAM::Render` (`0x233F20`) uses the same swap with its own + tracker and a larger snap limit; the tracker also starts over for a key not + sampled for 0.5 s (off screen, or a new object at a reused address). +- Known limits: positions are whole world pixels; other projectile classes + and spell effects still step at the logic rate; anything else that reads `m_pos` during render (floating text, action icons drawn outside the three hooks) is not smoothed. diff --git a/src/iee/game/build_manifest.cpp b/src/iee/game/build_manifest.cpp index 2190ef0..2cf4769 100644 --- a/src/iee/game/build_manifest.cpp +++ b/src/iee/game/build_manifest.cpp @@ -191,8 +191,10 @@ constexpr BuildManifest kKnownBuilds[] = { "48 8B 05 ? ? ? ? 48 33 C4 48 89 45 20 45 33 E4", "40 55 53 56 57 41 56 48 8B EC 48 83 EC 60 48 8B 41 18 4C 8B F2 48 8B F1", "4C 8B DC 55 57 49 8D AB 48 FF FF FF 48 81 EC A8 01 00 00", + // CProjectileBAM::Render (PDB-named, offline-verified unique). + "4C 8B DC 55 41 54 41 56 49 8D 6B A1 48 81 EC C0 00 00 00", }, - {0x27EBD0, 0x4257C0, 0x276700, 0x1F27D0, 0x36BA50, 0x36F170, 0x36E820}, + {0x27EBD0, 0x4257C0, 0x276700, 0x1F27D0, 0x36BA50, 0x36F170, 0x36E820, 0x233F20}, {0x100, 0x1DC, 0x14, 0x6590, 0x6598, 0x65F8}, {{ {"CRes_Demand", 0x36, BranchInstructionKind::CallRel32, 0xE8, 1, 5, true}, @@ -229,7 +231,8 @@ static_assert(validate_pattern_format(kKnownBuilds[1].patterns.objectArrayGetSha "2.7.3 GetShare pattern format is invalid"); static_assert(validate_pattern_format(kKnownBuilds[1].patterns.spriteRender) && validate_pattern_format(kKnownBuilds[1].patterns.spriteRenderMarkers) && - validate_pattern_format(kKnownBuilds[1].patterns.spriteRenderHealthBar), + validate_pattern_format(kKnownBuilds[1].patterns.spriteRenderHealthBar) && + validate_pattern_format(kKnownBuilds[1].patterns.projectileBamRender), "2.7.3 sprite render pattern format is invalid"); static_assert(kKnownBuilds[1].validate(), "2.7.3 build manifest is invalid"); } // namespace diff --git a/src/iee/game/build_manifest.h b/src/iee/game/build_manifest.h index 5978fa3..7912c52 100644 --- a/src/iee/game/build_manifest.h +++ b/src/iee/game/build_manifest.h @@ -46,6 +46,9 @@ struct PatternSet { std::string_view spriteRender{}; std::string_view spriteRenderMarkers{}; std::string_view spriteRenderHealthBar{}; + // Optional: CProjectileBAM::Render (arrows, bolts, missiles). Smoothed with + // the same position swap; independent of the sprite set. + std::string_view projectileBamRender{}; }; struct ReferenceRvas { @@ -57,6 +60,7 @@ struct ReferenceRvas { std::uintptr_t spriteRender{}; std::uintptr_t spriteRenderMarkers{}; std::uintptr_t spriteRenderHealthBar{}; + std::uintptr_t projectileBamRender{}; }; struct RuntimeOffsets { diff --git a/src/iee/game/game_addrs.cpp b/src/iee/game/game_addrs.cpp index 2588128..8c80382 100644 --- a/src/iee/game/game_addrs.cpp +++ b/src/iee/game/game_addrs.cpp @@ -101,6 +101,8 @@ namespace iee::game { out.SpriteRenderHealthBar = resolveOptional("CGameSprite::RenderHealthBar", patterns.spriteRenderHealthBar, rvas.spriteRenderHealthBar); + out.ProjectileBamRender = resolveOptional( + "CProjectileBAM::Render", patterns.projectileBamRender, rvas.projectileBamRender); if (out.SpriteRender && out.SpriteRenderMarkers && out.SpriteRenderHealthBar) { LOG_INFO("Sprite render targets resolved at RVA 0x{:X} / 0x{:X} / 0x{:X}", out.SpriteRender - moduleBase, out.SpriteRenderMarkers - moduleBase, diff --git a/src/iee/game/game_addrs.h b/src/iee/game/game_addrs.h index 356cd57..8467448 100644 --- a/src/iee/game/game_addrs.h +++ b/src/iee/game/game_addrs.h @@ -19,6 +19,7 @@ namespace iee::game { std::uintptr_t SpriteRender = 0; std::uintptr_t SpriteRenderMarkers = 0; std::uintptr_t SpriteRenderHealthBar = 0; + std::uintptr_t ProjectileBamRender = 0; bool initialized = false; }; diff --git a/src/iee/game/sprite_motion.cpp b/src/iee/game/sprite_motion.cpp index ee1a4b5..b1a19c9 100644 --- a/src/iee/game/sprite_motion.cpp +++ b/src/iee/game/sprite_motion.cpp @@ -29,10 +29,15 @@ MotionPoint SpriteMotionTracker::sample(const void* key, MotionPoint current, do return current; } + const bool forgotten = now - state.lastSeen > kForgetSeconds; state.lastSeen = now; + if (forgotten) { + state = {current, current, now, kDefaultInterval, now}; + return current; + } if (current != state.to) { const auto jump = (std::max)(std::abs(current.x - state.to.x), std::abs(current.y - state.to.y)); - if (jump > kSnapDistance) { + if (jump > snapDistance_) { state.from = current; } else { // Start from what is on screen now so an early tick never jumps back. diff --git a/src/iee/game/sprite_motion.h b/src/iee/game/sprite_motion.h index e6d28ff..9eda569 100644 --- a/src/iee/game/sprite_motion.h +++ b/src/iee/game/sprite_motion.h @@ -20,6 +20,9 @@ struct MotionPoint { // Host-safe and single-threaded: the caller owns the clock and the thread. class SpriteMotionTracker { public: + explicit SpriteMotionTracker(std::int32_t snapDistance = kSnapDistance) noexcept + : snapDistance_(snapDistance) {} + // Logic-tick interval assumed until two consecutive moves are observed. static constexpr double kDefaultInterval = 1.0 / 30.0; // Observed intervals outside this range are not a steady walk (first step @@ -27,8 +30,12 @@ class SpriteMotionTracker { static constexpr double kMinInterval = 1.0 / 90.0; static constexpr double kMaxInterval = 1.0 / 14.0; // A larger single-tick move is a teleport or a reused object address and - // is shown immediately. + // is shown immediately. Creatures use the default; fast movers (projectiles) + // pass a larger limit. static constexpr std::int32_t kSnapDistance = 48; + // A key not sampled for this long was off screen or is a new object at a + // reused address: it starts over instead of sliding from a stale position. + static constexpr double kForgetSeconds = 0.5; static constexpr std::size_t kPruneThreshold = 1024; static constexpr double kStaleSeconds = 5.0; @@ -52,6 +59,7 @@ class SpriteMotionTracker { [[nodiscard]] static MotionPoint shown(const State& state, double now) noexcept; void prune(double now); + std::int32_t snapDistance_; std::unordered_map states_; }; } // namespace iee::game diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index 3d8ab3d..6e241c1 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -42,6 +42,7 @@ using SpriteRenderFn = void (*)(void*, void*, void*); static core::Hook g_spriteRenderHook; static core::Hook g_spriteMarkersHook; static core::Hook g_spriteHealthBarHook; +static core::Hook g_projectileBamHook; static AppContext* g_ctx = nullptr; // Raised by LoadArea; the sprite-smoothing tracker clears itself on its own thread. @@ -298,6 +299,8 @@ static void detour_draw_color_tone(int mode) { // logic position is restored afterwards. All of it runs on the engine's main // thread; LoadArea only raises the reset flag. static game::SpriteMotionTracker g_spriteMotion; +// Projectiles cover far more ground per tick than creatures. +static game::SpriteMotionTracker g_projectileMotion{192}; static int g_spritePositionSwapDepth = 0; // One timestamp per presented frame, so a sprite, its selection circle and @@ -320,8 +323,9 @@ static double sprite_motion_frame_seconds() noexcept { return seconds; } -static void call_with_smoothed_position(core::Hook& hook, void* sprite, void* a, - void* b) { +static void call_with_smoothed_position(core::Hook& hook, + game::SpriteMotionTracker& tracker, void* sprite, + void* a, void* b) { const auto original = hook.original(); // Nested sprite renders already see the smoothed position. if (!sprite || !g_ctx || g_spritePositionSwapDepth > 0) { @@ -340,10 +344,13 @@ static void call_with_smoothed_position(core::Hook& hook, void* threadLogged = true; LOG_INFO("Sprite smoothing: first sprite render on thread {}", GetCurrentThreadId()); } - if (g_spriteMotionReset.exchange(false, std::memory_order_acq_rel)) g_spriteMotion.clear(); + if (g_spriteMotionReset.exchange(false, std::memory_order_acq_rel)) { + g_spriteMotion.clear(); + g_projectileMotion.clear(); + } if (core::safe_read(position, logic)) { const auto shown = - g_spriteMotion.sample(sprite, {logic.x, logic.y}, sprite_motion_frame_seconds()); + tracker.sample(sprite, {logic.x, logic.y}, sprite_motion_frame_seconds()); if (shown.x != logic.x || shown.y != logic.y) { shownX = shown.x; shownY = shown.y; @@ -364,13 +371,17 @@ static void call_with_smoothed_position(core::Hook& hook, void* } static void detour_sprite_render(void* sprite, void* a, void* b) { - call_with_smoothed_position(g_spriteRenderHook, sprite, a, b); + call_with_smoothed_position(g_spriteRenderHook, g_spriteMotion, sprite, a, b); } static void detour_sprite_markers(void* sprite, void* a, void* b) { - call_with_smoothed_position(g_spriteMarkersHook, sprite, a, b); + call_with_smoothed_position(g_spriteMarkersHook, g_spriteMotion, sprite, a, b); } static void detour_sprite_health_bar(void* sprite, void* a, void* b) { - call_with_smoothed_position(g_spriteHealthBarHook, sprite, a, b); + call_with_smoothed_position(g_spriteHealthBarHook, g_spriteMotion, sprite, a, b); +} + +static void detour_projectile_bam_render(void* projectile, void* a, void* b) { + call_with_smoothed_position(g_projectileBamHook, g_projectileMotion, projectile, a, b); } // CGameStatic::Render hook: while the fpSEAM point effects are active, the @@ -513,6 +524,18 @@ bool install_all(AppContext& ctx) { } } + if (ctx.cfg.smoothSpriteMovement && ctx.addrs.ProjectileBamRender) { + try { + g_projectileBamHook.create(reinterpret_cast(ctx.addrs.ProjectileBamRender), + reinterpret_cast(&detour_projectile_bam_render)); + g_projectileBamHook.enable(); + LOG_INFO("Projectile movement smoothing hook installed"); + } catch (...) { + (void)g_projectileBamHook.remove(); + LOG_WARN("Projectile movement smoothing hook failed; projectiles move at the logic rate"); + } + } + g_loadAreaHook.enable(); LOG_INFO("LoadArea hook enabled"); @@ -554,6 +577,7 @@ void uninstall_all() noexcept { } catch (...) { } + (void)g_projectileBamHook.remove(); (void)g_spriteHealthBarHook.remove(); (void)g_spriteMarkersHook.remove(); (void)g_spriteRenderHook.remove(); @@ -577,6 +601,7 @@ void prepare_for_shutdown() noexcept { // state are torn down. MinHook itself stays initialized until // uninstall_all(), after every MinHook-backed subsystem has removed its // hooks. + (void)g_projectileBamHook.disable(); (void)g_spriteHealthBarHook.disable(); (void)g_spriteMarkersHook.disable(); (void)g_spriteRenderHook.disable(); diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index bcdac2d..6d2747a 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1705,14 +1705,25 @@ void test_sprite_motion_tracker() { "The slide spans the interval between the last two moves"); // A tick that arrives early continues from the on-screen position. - (void)tracker.sample(&b, {0, 0}, 20.0); + (void)tracker.sample(&b, {0, 0}, 20.9); (void)tracker.sample(&b, {10, 0}, 21.0); expect_true(tracker.sample(&b, {20, 0}, 21.0 + tick / 2) == MotionPoint{5, 0}, "An early move never jumps backwards or forwards"); - expect_true(tracker.sample(&b, {500, 500}, 22.0) == MotionPoint{500, 500}, + expect_true(tracker.sample(&b, {500, 500}, 21.3) == MotionPoint{500, 500}, "A teleport-sized move is shown immediately"); + // Back on screen after a gap (or a new object at a reused address). + expect_true(tracker.sample(&b, {510, 500}, 22.0 + SpriteMotionTracker::kForgetSeconds + 0.1) == + MotionPoint{510, 500}, + "A sprite not sampled for a while starts over instead of sliding"); + + SpriteMotionTracker fast{192}; + (void)fast.sample(&a, {0, 0}, 40.0); + (void)fast.sample(&a, {100, 0}, 40.1); + expect_true(fast.sample(&a, {100, 0}, 40.1 + tick / 2) == MotionPoint{50, 0}, + "A larger snap limit lets fast movers slide"); + tracker.clear(); expect_eq(tracker.size(), std::size_t{0}, "clear() forgets every sprite"); for (std::size_t i = 0; i <= SpriteMotionTracker::kPruneThreshold; ++i) { From a08c076503bb600d4e511dacb86b113ef2d75ee4 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 3 Oct 2026 22:13:24 +0200 Subject: [PATCH 28/57] Sprite smoothing: cover every moving object type The single projectile hook becomes a manifest table of per-object renders that draw from CGameObject::m_pos: BAM, scorcher, new-scorcher and sky-strike projectiles, fireballs, temporal objects, debris chunks and spell effect cells. Each resolves and hooks independently. --- ...26-10-03-graphics-uplift-roadmap-design.md | 15 ++++-- src/iee/game/build_manifest.cpp | 31 +++++++++-- src/iee/game/build_manifest.h | 17 ++++-- src/iee/game/game_addrs.cpp | 8 ++- src/iee/game/game_addrs.h | 5 +- src/iee/hooks.cpp | 53 +++++++++++++------ 6 files changed, 95 insertions(+), 34 deletions(-) diff --git a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md index 88de10b..db6e347 100644 --- a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md +++ b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md @@ -131,11 +131,16 @@ each sprite's draw position between ticks inside a `CGameSprite::Render` hook - In-game result 2026-10-03 (owner): works, visibly smoother. The field log shows sprite renders and `LoadArea` on the same thread, so the position swap cannot race the logic tick. -- `CProjectileBAM::Render` (`0x233F20`) uses the same swap with its own - tracker and a larger snap limit; the tracker also starts over for a key not - sampled for 0.5 s (off screen, or a new object at a reused address). -- Known limits: positions are whole world pixels; other projectile classes - and spell effects still step at the logic rate; anything else that reads `m_pos` during +- Moving non-creature objects use the same swap through a manifest table + (`BuildManifest::smoothedObjectRenders`), with their own tracker and a + larger snap limit: `CProjectileBAM`, `CProjectileScorcher`, + `CProjectileNewScorcher`, `CProjectileSkyStrike`, `CGameFireball3d`, + `CGameTemporal`, `CGameChunk`, `CVEFVidCell`. Each was confirmed to draw + from `CGameObject::m_pos`; `CProjectileSkyStrikeBAM` does not and is left + out. The tracker also starts over for a key not sampled for 0.5 s. +- Known limits: positions are whole world pixels; animation frames + themselves are not interpolated (item F); anything else that reads `m_pos` + during render (floating text, action icons drawn outside the three hooks) is not smoothed. diff --git a/src/iee/game/build_manifest.cpp b/src/iee/game/build_manifest.cpp index 2cf4769..99c2894 100644 --- a/src/iee/game/build_manifest.cpp +++ b/src/iee/game/build_manifest.cpp @@ -191,10 +191,8 @@ constexpr BuildManifest kKnownBuilds[] = { "48 8B 05 ? ? ? ? 48 33 C4 48 89 45 20 45 33 E4", "40 55 53 56 57 41 56 48 8B EC 48 83 EC 60 48 8B 41 18 4C 8B F2 48 8B F1", "4C 8B DC 55 57 49 8D AB 48 FF FF FF 48 81 EC A8 01 00 00", - // CProjectileBAM::Render (PDB-named, offline-verified unique). - "4C 8B DC 55 41 54 41 56 49 8D 6B A1 48 81 EC C0 00 00 00", }, - {0x27EBD0, 0x4257C0, 0x276700, 0x1F27D0, 0x36BA50, 0x36F170, 0x36E820, 0x233F20}, + {0x27EBD0, 0x4257C0, 0x276700, 0x1F27D0, 0x36BA50, 0x36F170, 0x36E820}, {0x100, 0x1DC, 0x14, 0x6590, 0x6598, 0x65F8}, {{ {"CRes_Demand", 0x36, BranchInstructionKind::CallRel32, 0xE8, 1, 5, true}, @@ -209,6 +207,24 @@ constexpr BuildManifest kKnownBuilds[] = { {"DrawEnd", 0x17A, BranchInstructionKind::CallRel32, 0xE8, 1, 5, true}, {"DrawPopState", 0x1AD, BranchInstructionKind::JmpRel32, 0xE9, 1, 5, true}, }}, + // PDB-named on the Steam 2.7.3.0 binary; each pattern offline-verified + // unique, each function confirmed to draw from CGameObject::m_pos. + {{ + {"CProjectileBAM::Render", "4C 8B DC 55 41 54 41 56 49 8D 6B A1 48 81 EC C0 00 00 00", + 0x233F20}, + {"CProjectileScorcher::Render", + "4C 8B DC 55 57 49 8D 6B A1 48 81 EC C8 00 00 00 48 8B 05 ? ? ? ? 48 33 C4 48 89 45 27", + 0x234A20}, + {"CProjectileNewScorcher::Render", "4C 8B DC 55 53 49 8D 6B A1 48 81 EC C8 00 00 00", + 0x234520}, + {"CProjectileSkyStrike::Render", "4C 8B DC 55 41 56 49 8D 6B A1 48 81 EC C8 00 00 00", + 0x234F50}, + {"CGameFireball3d::Render", + "48 89 5C 24 18 48 89 74 24 20 55 57 41 54 41 55 41 56 48 8D 6C 24 C9", 0x1EAF90}, + {"CGameTemporal::Render", "4C 8B DC 55 56 57 41 57 49 8D 6B A1 48 81 EC", 0x36CD80}, + {"CGameChunk::Render", "4C 8B DC 55 56 57 41 55 41 57 49 8D 6B A1", 0x36B7A0}, + {"CVEFVidCell::Render", "4C 8B DC 55 56 41 57 49 8D 6B A1 48 81 EC D0", 0x254B00}, + }}, }, }; @@ -231,9 +247,14 @@ static_assert(validate_pattern_format(kKnownBuilds[1].patterns.objectArrayGetSha "2.7.3 GetShare pattern format is invalid"); static_assert(validate_pattern_format(kKnownBuilds[1].patterns.spriteRender) && validate_pattern_format(kKnownBuilds[1].patterns.spriteRenderMarkers) && - validate_pattern_format(kKnownBuilds[1].patterns.spriteRenderHealthBar) && - validate_pattern_format(kKnownBuilds[1].patterns.projectileBamRender), + validate_pattern_format(kKnownBuilds[1].patterns.spriteRenderHealthBar), "2.7.3 sprite render pattern format is invalid"); +static_assert([] { + for (const auto& target : kKnownBuilds[1].smoothedObjectRenders) { + if (target.name && !validate_pattern_format(target.pattern)) return false; + } + return true; +}(), "2.7.3 smoothed object render pattern format is invalid"); static_assert(kKnownBuilds[1].validate(), "2.7.3 build manifest is invalid"); } // namespace diff --git a/src/iee/game/build_manifest.h b/src/iee/game/build_manifest.h index 7912c52..9b4c25f 100644 --- a/src/iee/game/build_manifest.h +++ b/src/iee/game/build_manifest.h @@ -46,9 +46,6 @@ struct PatternSet { std::string_view spriteRender{}; std::string_view spriteRenderMarkers{}; std::string_view spriteRenderHealthBar{}; - // Optional: CProjectileBAM::Render (arrows, bolts, missiles). Smoothed with - // the same position swap; independent of the sprite set. - std::string_view projectileBamRender{}; }; struct ReferenceRvas { @@ -60,7 +57,6 @@ struct ReferenceRvas { std::uintptr_t spriteRender{}; std::uintptr_t spriteRenderMarkers{}; std::uintptr_t spriteRenderHealthBar{}; - std::uintptr_t projectileBamRender{}; }; struct RuntimeOffsets { @@ -88,6 +84,16 @@ struct ExecutableVersion { } }; +// One per-object render (virtual Render(CGameArea*, CVidMode*) of a +// CGameObject subclass) that draws from CGameObject::m_pos and can therefore +// be movement-smoothed with the position swap. Each resolves independently. +struct ObjectRenderTarget { + const char* name{}; + std::string_view pattern{}; + std::uintptr_t referenceRva{}; +}; +inline constexpr std::size_t kMaxSmoothedObjectRenders = 8; + struct BuildManifest { std::string_view buildId{}; std::array supportedProductNames{}; @@ -96,6 +102,9 @@ struct BuildManifest { ReferenceRvas referenceRvas{}; RuntimeOffsets offsets{}; std::array renderTextureCallsites{}; + // Moving non-creature objects (projectiles, spell effect cells, debris). + // Unused slots stay empty. + std::array smoothedObjectRenders{}; [[nodiscard]] constexpr bool validate() const noexcept { if (buildId.empty() || supportedProductNames[0].empty() || executableVersion.major == 0 || diff --git a/src/iee/game/game_addrs.cpp b/src/iee/game/game_addrs.cpp index 8c80382..3bcdb66 100644 --- a/src/iee/game/game_addrs.cpp +++ b/src/iee/game/game_addrs.cpp @@ -101,8 +101,12 @@ namespace iee::game { out.SpriteRenderHealthBar = resolveOptional("CGameSprite::RenderHealthBar", patterns.spriteRenderHealthBar, rvas.spriteRenderHealthBar); - out.ProjectileBamRender = resolveOptional( - "CProjectileBAM::Render", patterns.projectileBamRender, rvas.projectileBamRender); + for (std::size_t index = 0; index < manifest.smoothedObjectRenders.size(); ++index) { + const auto &target = manifest.smoothedObjectRenders[index]; + if (!target.name) continue; + out.SmoothedObjectRenders[index] = + resolveOptional(target.name, target.pattern, target.referenceRva); + } if (out.SpriteRender && out.SpriteRenderMarkers && out.SpriteRenderHealthBar) { LOG_INFO("Sprite render targets resolved at RVA 0x{:X} / 0x{:X} / 0x{:X}", out.SpriteRender - moduleBase, out.SpriteRenderMarkers - moduleBase, diff --git a/src/iee/game/game_addrs.h b/src/iee/game/game_addrs.h index 8467448..0d063ab 100644 --- a/src/iee/game/game_addrs.h +++ b/src/iee/game/game_addrs.h @@ -1,4 +1,5 @@ #pragma once +#include #include namespace iee { @@ -19,7 +20,9 @@ namespace iee::game { std::uintptr_t SpriteRender = 0; std::uintptr_t SpriteRenderMarkers = 0; std::uintptr_t SpriteRenderHealthBar = 0; - std::uintptr_t ProjectileBamRender = 0; + // Optional, each independent; 0 where unresolved. Indexed like + // BuildManifest::smoothedObjectRenders. + std::array SmoothedObjectRenders{}; bool initialized = false; }; diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index 6e241c1..d2408d7 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -3,10 +3,12 @@ #include #include +#include #include #include #include #include +#include #include "app_context.h" #include "area_state.h" @@ -42,7 +44,7 @@ using SpriteRenderFn = void (*)(void*, void*, void*); static core::Hook g_spriteRenderHook; static core::Hook g_spriteMarkersHook; static core::Hook g_spriteHealthBarHook; -static core::Hook g_projectileBamHook; +static std::array, game::kMaxSmoothedObjectRenders> g_objectRenderHooks; static AppContext* g_ctx = nullptr; // Raised by LoadArea; the sprite-smoothing tracker clears itself on its own thread. @@ -299,8 +301,8 @@ static void detour_draw_color_tone(int mode) { // logic position is restored afterwards. All of it runs on the engine's main // thread; LoadArea only raises the reset flag. static game::SpriteMotionTracker g_spriteMotion; -// Projectiles cover far more ground per tick than creatures. -static game::SpriteMotionTracker g_projectileMotion{192}; +// Projectiles and effects cover far more ground per tick than creatures. +static game::SpriteMotionTracker g_objectMotion{192}; static int g_spritePositionSwapDepth = 0; // One timestamp per presented frame, so a sprite, its selection circle and @@ -346,7 +348,7 @@ static void call_with_smoothed_position(core::Hook& hook, } if (g_spriteMotionReset.exchange(false, std::memory_order_acq_rel)) { g_spriteMotion.clear(); - g_projectileMotion.clear(); + g_objectMotion.clear(); } if (core::safe_read(position, logic)) { const auto shown = @@ -380,9 +382,18 @@ static void detour_sprite_health_bar(void* sprite, void* a, void* b) { call_with_smoothed_position(g_spriteHealthBarHook, g_spriteMotion, sprite, a, b); } -static void detour_projectile_bam_render(void* projectile, void* a, void* b) { - call_with_smoothed_position(g_projectileBamHook, g_projectileMotion, projectile, a, b); +// One detour per manifest slot, so each knows which original to call. +template +static void detour_object_render(void* object, void* a, void* b) { + call_with_smoothed_position(g_objectRenderHooks[Slot], g_objectMotion, object, a, b); } +template +static constexpr std::array object_render_detours( + std::index_sequence) { + return {&detour_object_render...}; +} +static constexpr auto kObjectRenderDetours = + object_render_detours(std::make_index_sequence{}); // CGameStatic::Render hook: while the fpSEAM point effects are active, the // authored fire/smoke BAM draws are replaced by our textured effects, so the @@ -524,16 +535,24 @@ bool install_all(AppContext& ctx) { } } - if (ctx.cfg.smoothSpriteMovement && ctx.addrs.ProjectileBamRender) { - try { - g_projectileBamHook.create(reinterpret_cast(ctx.addrs.ProjectileBamRender), - reinterpret_cast(&detour_projectile_bam_render)); - g_projectileBamHook.enable(); - LOG_INFO("Projectile movement smoothing hook installed"); - } catch (...) { - (void)g_projectileBamHook.remove(); - LOG_WARN("Projectile movement smoothing hook failed; projectiles move at the logic rate"); + if (ctx.cfg.smoothSpriteMovement) { + std::size_t installed = 0; + for (std::size_t slot = 0; slot < g_objectRenderHooks.size(); ++slot) { + const auto target = ctx.addrs.SmoothedObjectRenders[slot]; + if (!target) continue; + try { + g_objectRenderHooks[slot].create(reinterpret_cast(target), + reinterpret_cast(kObjectRenderDetours[slot])); + g_objectRenderHooks[slot].enable(); + ++installed; + } catch (...) { + (void)g_objectRenderHooks[slot].remove(); + LOG_WARN("Object movement smoothing hook {} failed; that object type moves at the " + "logic rate", + ctx.manifest->smoothedObjectRenders[slot].name); + } } + LOG_INFO("Object movement smoothing hooks installed: {}", installed); } g_loadAreaHook.enable(); @@ -577,7 +596,7 @@ void uninstall_all() noexcept { } catch (...) { } - (void)g_projectileBamHook.remove(); + for (auto& hook : g_objectRenderHooks) (void)hook.remove(); (void)g_spriteHealthBarHook.remove(); (void)g_spriteMarkersHook.remove(); (void)g_spriteRenderHook.remove(); @@ -601,7 +620,7 @@ void prepare_for_shutdown() noexcept { // state are torn down. MinHook itself stays initialized until // uninstall_all(), after every MinHook-backed subsystem has removed its // hooks. - (void)g_projectileBamHook.disable(); + for (auto& hook : g_objectRenderHooks) (void)hook.disable(); (void)g_spriteHealthBarHook.disable(); (void)g_spriteMarkersHook.disable(); (void)g_spriteRenderHook.disable(); From 098f8f1acf0a8bd1915a1b00e2e8d530aebd7bfb Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 3 Oct 2026 23:03:07 +0200 Subject: [PATCH 29/57] Add draw-time animation interpolation from expanded BAMs Game logic keeps the original BAM and frame number. While a creature is drawn, each CVidCell the engine resolves a frame for is pointed at a private CResCell over an expanded BAM (original frame k at 2k, the in-between leading to k+1 at 2k+1) and at the frame for the current instant; the logic resource and frame are restored when the draw returns. Expanded BAMs are read from iee-interp/.bam on first use and validated end to end before the engine sees them. A BAM without one draws as before. Off by default ([Rendering] InterpolateAnimations), 2.7.3 only. --- CMakeLists.txt | 1 + src/iee/core/config.cpp | 3 + src/iee/core/config.h | 1 + src/iee/game/animation_interp.cpp | 235 +++++++++++++++++++++++ src/iee/game/animation_interp.h | 113 +++++++++++ src/iee/game/build_manifest.cpp | 12 +- src/iee/game/build_manifest.h | 10 + src/iee/game/game_addrs.cpp | 29 ++- src/iee/game/game_addrs.h | 4 + src/iee/hooks.cpp | 83 +++++++- tests/iee_tests.cpp | 154 +++++++++++++++ tools/InfinityEngine-Enhancer.sample.ini | 4 + 12 files changed, 642 insertions(+), 7 deletions(-) create mode 100644 src/iee/game/animation_interp.cpp create mode 100644 src/iee/game/animation_interp.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 78c9ff3..02b0cae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,6 +92,7 @@ add_library(iee_common STATIC src/iee/core/config.cpp src/iee/core/logger.cpp src/iee/core/pattern_scanner.cpp + src/iee/game/animation_interp.cpp src/iee/game/are_animations.cpp src/iee/game/area_texture.cpp src/iee/game/build_manifest.cpp diff --git a/src/iee/core/config.cpp b/src/iee/core/config.cpp index b311618..6e047bb 100644 --- a/src/iee/core/config.cpp +++ b/src/iee/core/config.cpp @@ -96,6 +96,8 @@ static void apply_kv(EngineConfig& cfg, const std::string& section, const std::s assign_float(cfg.lodBias); else if (iequals(key, "SmoothSpriteMovement")) assign_bool(cfg.smoothSpriteMovement); + else if (iequals(key, "InterpolateAnimations")) + assign_bool(cfg.interpolateAnimations); return; } @@ -192,6 +194,7 @@ bool ConfigManager::save(const std::filesystem::path& path, const EngineConfig& f << "MaxAnisotropy = " << cfg.maxAnisotropy << "\n"; f << "LODBias = " << cfg.lodBias << "\n"; write_bool(f, "SmoothSpriteMovement", cfg.smoothSpriteMovement); + write_bool(f, "InterpolateAnimations", cfg.interpolateAnimations); write_section(f, "Detection"); write_bool(f, "AreaAnimationScan", cfg.enableAreaAnimationScan); diff --git a/src/iee/core/config.h b/src/iee/core/config.h index 63ac7c0..3a91b3a 100644 --- a/src/iee/core/config.h +++ b/src/iee/core/config.h @@ -8,6 +8,7 @@ struct EngineConfig { float maxAnisotropy = 8.0f; float lodBias = -0.25f; bool smoothSpriteMovement = false; + bool interpolateAnimations = false; bool dumpEngineShaders = false; bool enableDebugHotkeys = false; diff --git a/src/iee/game/animation_interp.cpp b/src/iee/game/animation_interp.cpp new file mode 100644 index 0000000..f16dfe8 --- /dev/null +++ b/src/iee/game/animation_interp.cpp @@ -0,0 +1,235 @@ +#include "animation_interp.h" + +#include +#include +#include +#include +#include +#include + +#include "iee/core/pattern_scanner.h" + +namespace iee::game { +namespace { +constexpr std::uint32_t kBamSignature = 0x204D4142; // "BAM " +constexpr std::uint32_t kBamVersion1 = 0x20203156; // "V1 " +constexpr std::uint32_t kRawFrameFlag = 0x80000000u; +constexpr std::size_t kPaletteBytes = 256 * 4; +constexpr std::size_t kMaxExpandedBamBytes = 64u * 1024u * 1024u; +constexpr std::size_t kMaxSwapsPerScope = 32; + +// True when [offset, offset + count * stride) lies inside `size`. +bool in_bounds(std::size_t offset, std::size_t count, std::size_t stride, + std::size_t size) noexcept { + return offset <= size && count <= (size - offset) / stride; +} +} // namespace + +bool parse_expanded_bam(std::span image, ExpandedBamView& out) noexcept { + out = {}; + if (image.size() < sizeof(bamHeader_st)) return false; + bamHeader_st header{}; + std::memcpy(&header, image.data(), sizeof(header)); + if (header.nFileType != kBamSignature || header.nFileVersion != kBamVersion1 || + header.nFrames == 0 || header.nSequences == 0) { + return false; + } + const std::size_t size = image.size(); + const std::size_t framesOffset = header.nTableOffset; + if (!in_bounds(framesOffset, header.nFrames, sizeof(frameTableEntry_st), size)) return false; + const std::size_t sequencesOffset = framesOffset + header.nFrames * sizeof(frameTableEntry_st); + if (!in_bounds(sequencesOffset, header.nSequences, sizeof(sequenceTableEntry_st), size) || + !in_bounds(header.nPaletteOffset, kPaletteBytes, 1, size) || + header.nFrameListOffset > size) { + return false; + } + const std::size_t listEntries = (size - header.nFrameListOffset) / sizeof(std::uint16_t); + + for (std::size_t index = 0; index < header.nFrames; ++index) { + frameTableEntry_st frame{}; + std::memcpy(&frame, image.data() + framesOffset + index * sizeof(frame), sizeof(frame)); + if ((frame.___u4 & kRawFrameFlag) == 0) return false; + const std::size_t pixels = static_cast(frame.nWidth) * frame.nHeight; + if (!in_bounds(frame.___u4 & ~kRawFrameFlag, pixels, 1, size)) return false; + } + for (std::size_t index = 0; index < header.nSequences; ++index) { + sequenceTableEntry_st sequence{}; + std::memcpy(&sequence, image.data() + sequencesOffset + index * sizeof(sequence), + sizeof(sequence)); + if (sequence.nFrames < 0) return false; + const auto count = static_cast(sequence.nFrames); + if (count == 0) continue; + if (sequence.nStartingFrame > listEntries || count > listEntries - sequence.nStartingFrame) { + return false; + } + for (std::size_t slot = 0; slot < count; ++slot) { + std::uint16_t listed = 0; + std::memcpy(&listed, + image.data() + header.nFrameListOffset + + (sequence.nStartingFrame + slot) * sizeof(listed), + sizeof(listed)); + if (listed >= header.nFrames) return false; + } + } + + auto* base = image.data(); + out.header = reinterpret_cast(base); + out.frames = reinterpret_cast(base + framesOffset); + out.sequences = reinterpret_cast(base + sequencesOffset); + out.frameList = reinterpret_cast(base + header.nFrameListOffset); + out.palette = base + header.nPaletteOffset; + return true; +} + +std::uint16_t AnimationFrameTracker::sample(const void* key, std::uint16_t sequence, + std::int16_t frame, std::int16_t length, double now) { + auto [it, inserted] = states_.try_emplace(key); + auto& state = it->second; + const auto original = static_cast(frame * 2); + if (inserted || now - state.lastSeen > kForgetSeconds || sequence != state.sequence) { + state = {sequence, frame, -1, now, kDefaultInterval, now}; + if (inserted && states_.size() > kPruneThreshold) { + for (auto entry = states_.begin(); entry != states_.end();) { + entry = now - entry->second.lastSeen > kForgetSeconds ? states_.erase(entry) + : std::next(entry); + } + } + return original; + } + state.lastSeen = now; + if (frame != state.frame) { + const double observed = now - state.changedAt; + state.interval = + observed >= kMinInterval && observed <= kMaxInterval ? observed : kDefaultInterval; + state.previous = state.frame; + state.frame = frame; + state.changedAt = now; + } + const bool consecutive = + state.previous >= 0 && length > 1 && frame == (state.previous + 1) % length; + if (consecutive && now - state.changedAt < state.interval / 2.0) { + return static_cast(state.previous * 2 + 1); + } + return original; +} + +std::shared_ptr AnimationInterpolator::load( + const std::string& resref) { + if (const auto known = files_.find(resref); known != files_.end()) return known->second; + std::shared_ptr result; + std::error_code error; + const auto path = directory_ / (resref + ".bam"); + const auto size = std::filesystem::file_size(path, error); + if (!error && size >= sizeof(bamHeader_st) && size <= kMaxExpandedBamBytes) { + auto expanded = std::make_shared(); + expanded->image.resize(static_cast(size)); + std::ifstream file(path, std::ios::binary); + if (file.read(reinterpret_cast(expanded->image.data()), + static_cast(size)) && + parse_expanded_bam(expanded->image, expanded->view)) { + result = std::move(expanded); + ++loaded_; + } + } + files_.emplace(resref, result); + return result; +} + +AnimationInterpolator::Resource* AnimationInterpolator::resource_for(CResCell* original) { + if (const auto known = resources_.find(original); known != resources_.end()) { + return known->second.get(); + } + CResCell snapshot{}; + if (!core::safe_read(original, snapshot)) return nullptr; + // The original must be a loaded BAM V1: its tables are the logic's view and + // the template the private cell is copied from. Not cached until it is. + if (!snapshot.baseclass_0.bLoaded || !snapshot.baseclass_0.pData || !snapshot.m_pBamHeader || + snapshot.m_pBamHeaderV2 || !snapshot.m_pSequences || !snapshot.baseclass_0.resref) { + return nullptr; + } + // The name is an interned C string of at most 8 characters; read it one + // byte at a time so nothing past its terminator is touched. + std::array name{}; + for (std::size_t index = 0; index < 8; ++index) { + if (!core::safe_read(snapshot.baseclass_0.resref + index, name[index]) || !name[index]) { + name[index] = '\0'; + break; + } + } + std::string resref(name.data()); + for (auto& character : resref) { + character = static_cast(std::toupper(static_cast(character))); + } + + auto resource = std::make_unique(); + if (!resref.empty() && resref.find_first_of("/\\.:") == std::string::npos) { + resource->expanded = load(resref); + } + if (resource->expanded) { + const auto& view = resource->expanded->view; + bamHeader_st originalHeader{}; + if (!core::safe_read(snapshot.m_pBamHeader, originalHeader) || + originalHeader.nSequences != view.header->nSequences) { + resource->expanded.reset(); + } else { + resource->cell = snapshot; + resource->cell.baseclass_0.pData = resource->expanded->image.data(); + resource->cell.baseclass_0.nSize = static_cast(resource->expanded->image.size()); + resource->cell.baseclass_0.bWasMalloced = false; + resource->cell.baseclass_0.bLoaded = true; + resource->cell.pUncompressedData = nullptr; + resource->cell.nUncompressedSize = 0; + resource->cell.m_pBamHeader = view.header; + resource->cell.m_pBamHeaderV2 = nullptr; + resource->cell.m_pQuads = nullptr; + resource->cell.m_pFrames = view.frames; + resource->cell.m_pSequences = view.sequences; + resource->cell.m_pFrameList = view.frameList; + resource->cell.m_pPalette = view.palette; + } + } + return resources_.emplace(original, std::move(resource)).first->second.get(); +} + +void AnimationInterpolator::touch(CVidCell* cell, double now) noexcept { + if (depth_ <= 0 || !cell) return; + try { + for (const auto& swap : swaps_) { + if (swap.cell == cell) return; + } + if (swaps_.size() >= kMaxSwapsPerScope || !cell->pRes) return; + auto* original = cell->pRes; + auto* resource = resource_for(original); + if (!resource || !resource->expanded) return; + + const auto sequence = cell->m_nCurrentSequence; + const auto frame = cell->m_nCurrentFrame; + const auto& view = resource->expanded->view; + sequenceTableEntry_st logic{}; + if (sequence >= view.header->nSequences || + !core::safe_read(original->m_pSequences + sequence, logic) || logic.nFrames <= 0 || + frame < 0 || frame >= logic.nFrames || + view.sequences[sequence].nFrames != logic.nFrames * 2) { + return; + } + const auto shown = tracker_.sample(cell, sequence, frame, logic.nFrames, now); + swaps_.push_back({cell, original, frame}); + cell->pRes = &resource->cell; + cell->m_nCurrentFrame = static_cast(shown); + cell->m_pFrame = nullptr; + } catch (...) { + // Interpolation is cosmetic; any doubt draws the original frame. + } +} + +void AnimationInterpolator::end_scope() noexcept { + if (depth_ <= 0) return; + if (--depth_ > 0) return; + for (const auto& swap : swaps_) { + swap.cell->pRes = swap.original; + swap.cell->m_nCurrentFrame = swap.frame; + swap.cell->m_pFrame = nullptr; + } + swaps_.clear(); +} +} // namespace iee::game diff --git a/src/iee/game/animation_interp.h b/src/iee/game/animation_interp.h new file mode 100644 index 0000000..4629ea0 --- /dev/null +++ b/src/iee/game/animation_interp.h @@ -0,0 +1,113 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "runtime_types_x64.h" + +namespace iee::game { +// Table pointers into a validated expanded BAM V1 image, the same four that +// CResCell::Parse derives. +struct ExpandedBamView { + bamHeader_st* header{}; + frameTableEntry_st* frames{}; + sequenceTableEntry_st* sequences{}; + std::uint16_t* frameList{}; + void* palette{}; +}; + +// Validates an uncompressed BAM V1 image end to end (tables, lookup entries +// and every frame's raw pixel block inside the image) before the engine is +// ever pointed at it. Only raw frames are accepted: an RLE frame's length +// cannot be bounded without decoding it. +[[nodiscard]] bool parse_expanded_bam(std::span image, ExpandedBamView& out) noexcept; + +// Chooses which expanded frame to draw for a cell whose logic frame only +// changes on the engine's animation tick. An expanded cycle holds original +// frame k at 2k and the in-between leading to k+1 at 2k+1; for the first half +// of the observed tick interval after a step k-1 -> k the in-between is shown, +// then frame k. The picture therefore trails the logic by half a step and +// never guesses a frame the engine has not reached. +class AnimationFrameTracker { + public: + static constexpr double kDefaultInterval = 1.0 / 15.0; + static constexpr double kMinInterval = 1.0 / 90.0; + static constexpr double kMaxInterval = 1.0 / 4.0; + static constexpr double kForgetSeconds = 0.5; + static constexpr std::size_t kPruneThreshold = 4096; + + // `frame` is the logic frame within a cycle of `length` original frames. + [[nodiscard]] std::uint16_t sample(const void* key, std::uint16_t sequence, std::int16_t frame, + std::int16_t length, double now); + void clear() noexcept { states_.clear(); } + + private: + struct State { + std::uint16_t sequence{}; + std::int16_t frame{}; + std::int16_t previous{-1}; + double changedAt{}; + double interval{kDefaultInterval}; + double lastSeen{}; + }; + std::unordered_map states_; +}; + +// Draw-time substitution of expanded (frame-interpolated) BAMs. +// +// Game logic keeps the original BAM and frame number. While a creature is +// being drawn (a scope), each of its CVidCells is pointed at a private +// CResCell over the expanded image and at the in-between frame for the +// current instant; closing the scope puts the original resource and logic +// frame back. Expanded files are read from `directory/.bam` on first +// use; a BAM without one draws exactly as before. +// +// Single-threaded: every call must come from the engine's render thread. +class AnimationInterpolator { + public: + void set_directory(std::filesystem::path directory) { directory_ = std::move(directory); } + + void begin_scope() noexcept { ++depth_; } + // Restores every cell swapped since the matching begin_scope. + void end_scope() noexcept; + // Inside a scope: swap `cell` if it is not swapped yet and an expanded BAM + // exists for its resource. Outside a scope, or on any doubt, does nothing. + void touch(CVidCell* cell, double now) noexcept; + // Area change: forget per-cell animation timing (loaded files are kept). + void reset_timing() noexcept { tracker_.clear(); } + + [[nodiscard]] std::size_t loaded_count() const noexcept { return loaded_; } + + private: + struct Expanded { + std::vector image; + ExpandedBamView view; + }; + struct Resource { + std::shared_ptr expanded; // null: no usable expanded BAM + CResCell cell{}; // private copy pointing into `expanded` + }; + struct Swap { + CVidCell* cell{}; + CResCell* original{}; + std::int16_t frame{}; + }; + + Resource* resource_for(CResCell* original); + std::shared_ptr load(const std::string& resref); + + std::filesystem::path directory_; + int depth_{}; + std::vector swaps_; + std::unordered_map> resources_; + std::unordered_map> files_; + AnimationFrameTracker tracker_; + std::size_t loaded_{}; +}; +} // namespace iee::game diff --git a/src/iee/game/build_manifest.cpp b/src/iee/game/build_manifest.cpp index 99c2894..74c368a 100644 --- a/src/iee/game/build_manifest.cpp +++ b/src/iee/game/build_manifest.cpp @@ -191,8 +191,14 @@ constexpr BuildManifest kKnownBuilds[] = { "48 8B 05 ? ? ? ? 48 33 C4 48 89 45 20 45 33 E4", "40 55 53 56 57 41 56 48 8B EC 48 83 EC 60 48 8B 41 18 4C 8B F2 48 8B F1", "4C 8B DC 55 57 49 8D AB 48 FF FF FF 48 81 EC A8 01 00 00", + // CVidCell::GetFrame, then the shared GetCurrentCenterPoint / + // GetCurrentFrameSize body (PDB-named, offline-verified). + "40 53 48 83 EC 20 48 8B D9 48 8B 89 08 01 00 00 48 85 C9 0F", + "48 89 5C 24 10 48 89 74 24 18 57 48 83 EC 20 48 8B F9 33 DB 48 8B 89 08 01 00 00 " + "48 8B F2 48 85 C9 75 15 48 89 1A 33 C0", }, - {0x27EBD0, 0x4257C0, 0x276700, 0x1F27D0, 0x36BA50, 0x36F170, 0x36E820}, + {0x27EBD0, 0x4257C0, 0x276700, 0x1F27D0, 0x36BA50, 0x36F170, 0x36E820, 0x4118A0, 0x411660, + 0x411780}, {0x100, 0x1DC, 0x14, 0x6590, 0x6598, 0x65F8}, {{ {"CRes_Demand", 0x36, BranchInstructionKind::CallRel32, 0xE8, 1, 5, true}, @@ -247,7 +253,9 @@ static_assert(validate_pattern_format(kKnownBuilds[1].patterns.objectArrayGetSha "2.7.3 GetShare pattern format is invalid"); static_assert(validate_pattern_format(kKnownBuilds[1].patterns.spriteRender) && validate_pattern_format(kKnownBuilds[1].patterns.spriteRenderMarkers) && - validate_pattern_format(kKnownBuilds[1].patterns.spriteRenderHealthBar), + validate_pattern_format(kKnownBuilds[1].patterns.spriteRenderHealthBar) && + validate_pattern_format(kKnownBuilds[1].patterns.vidCellGetFrame) && + validate_pattern_format(kKnownBuilds[1].patterns.vidCellFrameAccessor), "2.7.3 sprite render pattern format is invalid"); static_assert([] { for (const auto& target : kKnownBuilds[1].smoothedObjectRenders) { diff --git a/src/iee/game/build_manifest.h b/src/iee/game/build_manifest.h index 9b4c25f..f3c3a65 100644 --- a/src/iee/game/build_manifest.h +++ b/src/iee/game/build_manifest.h @@ -46,6 +46,13 @@ struct PatternSet { std::string_view spriteRender{}; std::string_view spriteRenderMarkers{}; std::string_view spriteRenderHealthBar{}; + // Optional: CVidCell frame resolution, hooked for draw-time animation + // interpolation. GetFrame has its own signature. GetCurrentCenterPoint and + // GetCurrentFrameSize are byte-identical for their first 150+ bytes, so + // they share one pattern that is confirmed at each reference RVA instead of + // being searched for. + std::string_view vidCellGetFrame{}; + std::string_view vidCellFrameAccessor{}; }; struct ReferenceRvas { @@ -57,6 +64,9 @@ struct ReferenceRvas { std::uintptr_t spriteRender{}; std::uintptr_t spriteRenderMarkers{}; std::uintptr_t spriteRenderHealthBar{}; + std::uintptr_t vidCellGetFrame{}; + std::uintptr_t vidCellGetCurrentCenterPoint{}; + std::uintptr_t vidCellGetCurrentFrameSize{}; }; struct RuntimeOffsets { diff --git a/src/iee/game/game_addrs.cpp b/src/iee/game/game_addrs.cpp index 3bcdb66..214222e 100644 --- a/src/iee/game/game_addrs.cpp +++ b/src/iee/game/game_addrs.cpp @@ -81,7 +81,7 @@ namespace iee::game { // Optional targets: sprite movement smoothing. Resolved only when // requested, and only as a complete set. - if (cfg.smoothSpriteMovement) { + if (cfg.smoothSpriteMovement || cfg.interpolateAnimations) { const auto resolveOptional = [&](const char *name, std::string_view pattern, std::uintptr_t referenceRva) -> std::uintptr_t { if (pattern.empty()) return 0; @@ -101,7 +101,32 @@ namespace iee::game { out.SpriteRenderHealthBar = resolveOptional("CGameSprite::RenderHealthBar", patterns.spriteRenderHealthBar, rvas.spriteRenderHealthBar); - for (std::size_t index = 0; index < manifest.smoothedObjectRenders.size(); ++index) { + if (cfg.interpolateAnimations) { + // The twin accessors cannot be told apart by signature, so each + // is confirmed in full at its reference RVA on this + // identity-checked build. + const auto confirmTwin = [&](std::uintptr_t rva) -> std::uintptr_t { + if (!rva || patterns.vidCellFrameAccessor.empty()) return 0; + return reinterpret_cast( + core::confirm_pattern_with_patched_prologue( + nullptr, rva, patterns.vidCellFrameAccessor, 0)); + }; + out.VidCellGetFrame = resolveOptional("CVidCell::GetFrame", + patterns.vidCellGetFrame, + rvas.vidCellGetFrame); + out.VidCellGetCurrentCenterPoint = confirmTwin(rvas.vidCellGetCurrentCenterPoint); + out.VidCellGetCurrentFrameSize = confirmTwin(rvas.vidCellGetCurrentFrameSize); + if (!(out.VidCellGetFrame && out.VidCellGetCurrentCenterPoint && + out.VidCellGetCurrentFrameSize)) { + out.VidCellGetFrame = out.VidCellGetCurrentCenterPoint = + out.VidCellGetCurrentFrameSize = 0; + LOG_WARN("Animation interpolation disabled: this build has no complete set " + "of CVidCell frame targets"); + } + } + for (std::size_t index = 0; + cfg.smoothSpriteMovement && index < manifest.smoothedObjectRenders.size(); + ++index) { const auto &target = manifest.smoothedObjectRenders[index]; if (!target.name) continue; out.SmoothedObjectRenders[index] = diff --git a/src/iee/game/game_addrs.h b/src/iee/game/game_addrs.h index 0d063ab..1c603b2 100644 --- a/src/iee/game/game_addrs.h +++ b/src/iee/game/game_addrs.h @@ -20,6 +20,10 @@ namespace iee::game { std::uintptr_t SpriteRender = 0; std::uintptr_t SpriteRenderMarkers = 0; std::uintptr_t SpriteRenderHealthBar = 0; + // Optional, all-or-nothing: draw-time animation interpolation. + std::uintptr_t VidCellGetFrame = 0; + std::uintptr_t VidCellGetCurrentCenterPoint = 0; + std::uintptr_t VidCellGetCurrentFrameSize = 0; // Optional, each independent; 0 where unresolved. Indexed like // BuildManifest::smoothedObjectRenders. std::array SmoothedObjectRenders{}; diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index d2408d7..c786b7e 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include "app_context.h" @@ -19,6 +21,7 @@ #include "iee/features/tile_render.h" #include "iee/frame_hook.h" #include "iee/game/game_types.h" +#include "iee/game/animation_interp.h" #include "iee/game/renderer.h" #include "iee/game/runtime_types_x64.h" #include "iee/game/sprite_motion.h" @@ -45,6 +48,12 @@ static core::Hook g_spriteRenderHook; static core::Hook g_spriteMarkersHook; static core::Hook g_spriteHealthBarHook; static std::array, game::kMaxSmoothedObjectRenders> g_objectRenderHooks; +// CVidCell::GetFrame(), GetCurrentCenterPoint(CPoint&), GetCurrentFrameSize(CSize&): +// one pass-through signature, the unused argument is harmless on x64. +using VidCellAccessorFn = int (*)(void*, void*); +static core::Hook g_vidCellGetFrameHook; +static core::Hook g_vidCellCenterPointHook; +static core::Hook g_vidCellFrameSizeHook; static AppContext* g_ctx = nullptr; // Raised by LoadArea; the sprite-smoothing tracker clears itself on its own thread. @@ -303,6 +312,9 @@ static void detour_draw_color_tone(int mode) { static game::SpriteMotionTracker g_spriteMotion; // Projectiles and effects cover far more ground per tick than creatures. static game::SpriteMotionTracker g_objectMotion{192}; +// Draw-time animation interpolation state (see detour_sprite_render). +static game::AnimationInterpolator g_animationInterp; +static bool g_animationInterpActive = false; static int g_spritePositionSwapDepth = 0; // One timestamp per presented frame, so a sprite, its selection circle and @@ -330,7 +342,7 @@ static void call_with_smoothed_position(core::Hook& hook, void* a, void* b) { const auto original = hook.original(); // Nested sprite renders already see the smoothed position. - if (!sprite || !g_ctx || g_spritePositionSwapDepth > 0) { + if (!sprite || !g_ctx || g_spritePositionSwapDepth > 0 || !g_ctx->cfg.smoothSpriteMovement) { original(sprite, a, b); return; } @@ -349,6 +361,7 @@ static void call_with_smoothed_position(core::Hook& hook, if (g_spriteMotionReset.exchange(false, std::memory_order_acq_rel)) { g_spriteMotion.clear(); g_objectMotion.clear(); + g_animationInterp.reset_timing(); } if (core::safe_read(position, logic)) { const auto shown = @@ -372,8 +385,31 @@ static void call_with_smoothed_position(core::Hook& hook, if (swapped && position->x == shownX && position->y == shownY) *position = logic; } +// Animation interpolation. While a creature is being drawn, every CVidCell the +// engine resolves a frame for is pointed at its expanded BAM and the in-between +// frame for this instant (see game::AnimationInterpolator); the logic's +// resource and frame are restored when the draw returns. Main thread only. + static void detour_sprite_render(void* sprite, void* a, void* b) { + if (!g_animationInterpActive) { + call_with_smoothed_position(g_spriteRenderHook, g_spriteMotion, sprite, a, b); + return; + } + g_animationInterp.begin_scope(); call_with_smoothed_position(g_spriteRenderHook, g_spriteMotion, sprite, a, b); + g_animationInterp.end_scope(); +} +static int detour_vid_cell_get_frame(void* cell, void* unused) { + g_animationInterp.touch(static_cast(cell), sprite_motion_frame_seconds()); + return g_vidCellGetFrameHook.original()(cell, unused); +} +static int detour_vid_cell_center_point(void* cell, void* out) { + g_animationInterp.touch(static_cast(cell), sprite_motion_frame_seconds()); + return g_vidCellCenterPointHook.original()(cell, out); +} +static int detour_vid_cell_frame_size(void* cell, void* out) { + g_animationInterp.touch(static_cast(cell), sprite_motion_frame_seconds()); + return g_vidCellFrameSizeHook.original()(cell, out); } static void detour_sprite_markers(void* sprite, void* a, void* b) { call_with_smoothed_position(g_spriteMarkersHook, g_spriteMotion, sprite, a, b); @@ -514,7 +550,8 @@ bool install_all(AppContext& ctx) { } } - if (ctx.cfg.smoothSpriteMovement && ctx.addrs.SpriteRender && ctx.addrs.SpriteRenderMarkers && + if ((ctx.cfg.smoothSpriteMovement || ctx.cfg.interpolateAnimations) && + ctx.addrs.SpriteRender && ctx.addrs.SpriteRenderMarkers && ctx.addrs.SpriteRenderHealthBar) { try { g_spriteRenderHook.create(reinterpret_cast(ctx.addrs.SpriteRender), @@ -526,7 +563,40 @@ bool install_all(AppContext& ctx) { g_spriteRenderHook.enable(); g_spriteMarkersHook.enable(); g_spriteHealthBarHook.enable(); - LOG_INFO("Sprite movement smoothing hooks installed"); + LOG_INFO("Sprite render hooks installed (movement smoothing={})", + ctx.cfg.smoothSpriteMovement); + if (ctx.cfg.interpolateAnimations && ctx.addrs.VidCellGetFrame && + ctx.addrs.VidCellGetCurrentCenterPoint && ctx.addrs.VidCellGetCurrentFrameSize) { + try { + wchar_t executablePath[MAX_PATH]{}; + const auto length = GetModuleFileNameW(nullptr, executablePath, MAX_PATH); + if (length == 0 || length >= MAX_PATH) throw std::runtime_error("no game path"); + const auto directory = + std::filesystem::path(executablePath).parent_path() / "iee-interp"; + g_animationInterp.set_directory(directory); + g_vidCellGetFrameHook.create(reinterpret_cast(ctx.addrs.VidCellGetFrame), + reinterpret_cast(&detour_vid_cell_get_frame)); + g_vidCellCenterPointHook.create( + reinterpret_cast(ctx.addrs.VidCellGetCurrentCenterPoint), + reinterpret_cast(&detour_vid_cell_center_point)); + g_vidCellFrameSizeHook.create( + reinterpret_cast(ctx.addrs.VidCellGetCurrentFrameSize), + reinterpret_cast(&detour_vid_cell_frame_size)); + g_vidCellGetFrameHook.enable(); + g_vidCellCenterPointHook.enable(); + g_vidCellFrameSizeHook.enable(); + g_animationInterpActive = true; + LOG_INFO("Animation interpolation hooks installed (expanded BAMs from {})", + directory.string()); + } catch (...) { + g_animationInterpActive = false; + (void)g_vidCellFrameSizeHook.remove(); + (void)g_vidCellCenterPointHook.remove(); + (void)g_vidCellGetFrameHook.remove(); + LOG_WARN("Animation interpolation hooks failed; animations play their original " + "frames"); + } + } } catch (...) { (void)g_spriteHealthBarHook.remove(); (void)g_spriteMarkersHook.remove(); @@ -596,6 +666,10 @@ void uninstall_all() noexcept { } catch (...) { } + g_animationInterpActive = false; + (void)g_vidCellFrameSizeHook.remove(); + (void)g_vidCellCenterPointHook.remove(); + (void)g_vidCellGetFrameHook.remove(); for (auto& hook : g_objectRenderHooks) (void)hook.remove(); (void)g_spriteHealthBarHook.remove(); (void)g_spriteMarkersHook.remove(); @@ -620,6 +694,9 @@ void prepare_for_shutdown() noexcept { // state are torn down. MinHook itself stays initialized until // uninstall_all(), after every MinHook-backed subsystem has removed its // hooks. + (void)g_vidCellFrameSizeHook.disable(); + (void)g_vidCellCenterPointHook.disable(); + (void)g_vidCellGetFrameHook.disable(); for (auto& hook : g_objectRenderHooks) (void)hook.disable(); (void)g_spriteHealthBarHook.disable(); (void)g_spriteMarkersHook.disable(); diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 6d2747a..acc4b64 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -19,6 +19,7 @@ #include "iee/core/pattern_scanner.h" #include "iee/core/performance_samples.h" #include "iee/features/tile_render.h" +#include "iee/game/animation_interp.h" #include "iee/game/are_animations.h" #include "iee/game/sprite_motion.h" #include "iee/game/area_texture.h" @@ -1733,6 +1734,158 @@ void test_sprite_motion_tracker() { expect_eq(tracker.size(), std::size_t{1}, "Sprites not drawn for a while are forgotten"); } +// Builds an uncompressed BAM V1 image: `frames` raw 1x1 frames and the given +// cycles (lists of frame indices). +std::vector make_raw_bam(std::uint16_t frames, + const std::vector>& cycles) { + using namespace iee::game; + std::vector lookup; + for (const auto& cycle : cycles) lookup.insert(lookup.end(), cycle.begin(), cycle.end()); + bamHeader_st header{}; + header.nFileType = 0x204D4142; + header.nFileVersion = 0x20203156; + header.nFrames = frames; + header.nSequences = static_cast(cycles.size()); + header.nTableOffset = sizeof(bamHeader_st); + header.nPaletteOffset = static_cast( + header.nTableOffset + frames * sizeof(frameTableEntry_st) + + cycles.size() * sizeof(sequenceTableEntry_st)); + header.nFrameListOffset = header.nPaletteOffset + 1024; + const auto dataOffset = + static_cast(header.nFrameListOffset + lookup.size() * sizeof(std::uint16_t)); + std::vector image(dataOffset + frames); + std::memcpy(image.data(), &header, sizeof(header)); + for (std::uint16_t index = 0; index < frames; ++index) { + frameTableEntry_st frame{1, 1, 0, 0, (dataOffset + index) | 0x80000000u}; + std::memcpy(image.data() + header.nTableOffset + index * sizeof(frame), &frame, sizeof(frame)); + } + std::uint16_t start = 0; + for (std::size_t index = 0; index < cycles.size(); ++index) { + sequenceTableEntry_st sequence{static_cast(cycles[index].size()), start}; + start = static_cast(start + cycles[index].size()); + std::memcpy(image.data() + header.nTableOffset + frames * sizeof(frameTableEntry_st) + + index * sizeof(sequence), + &sequence, sizeof(sequence)); + } + if (!lookup.empty()) { + std::memcpy(image.data() + header.nFrameListOffset, lookup.data(), + lookup.size() * sizeof(std::uint16_t)); + } + return image; +} + +void test_animation_interpolation() { + using namespace iee::game; + + // --- expanded BAM validation + auto good = make_raw_bam(5, {{0, 3, 1, 4, 2, 2}}); + ExpandedBamView view{}; + expect_true(parse_expanded_bam(good, view) && view.header->nFrames == 5 && + view.sequences[0].nFrames == 6 && view.frameList[1] == 3, + "A well-formed raw BAM V1 yields its table pointers"); + auto bad = good; + bad.resize(bad.size() - 1); + expect_true(!parse_expanded_bam(bad, view), "A frame whose pixels overrun the image is rejected"); + bad = make_raw_bam(5, {{0, 3, 1, 9, 2, 2}}); + expect_true(!parse_expanded_bam(bad, view), "A lookup entry beyond the frame table is rejected"); + bad = good; + frameTableEntry_st rle{}; + std::memcpy(&rle, bad.data() + sizeof(bamHeader_st), sizeof(rle)); + rle.___u4 &= 0x7FFFFFFFu; + std::memcpy(bad.data() + sizeof(bamHeader_st), &rle, sizeof(rle)); + expect_true(!parse_expanded_bam(bad, view), "An RLE frame cannot be bounded and is rejected"); + bad = good; + bad[0] = std::byte{'X'}; + expect_true(!parse_expanded_bam(bad, view), "A wrong signature is rejected"); + + // --- frame choice + AnimationFrameTracker tracker; + int key = 0; + const double step = AnimationFrameTracker::kDefaultInterval; + expect_eq(tracker.sample(&key, 0, 0, 3, 10.0), std::uint16_t{0}, "First sighting: original frame"); + expect_eq(tracker.sample(&key, 0, 1, 3, 10.0 + step), std::uint16_t{1}, + "Right after a step the in-between leading to the new frame is shown"); + expect_eq(tracker.sample(&key, 0, 1, 3, 10.0 + step + step * 0.6), std::uint16_t{2}, + "Past half the interval the new frame itself is shown"); + expect_eq(tracker.sample(&key, 0, 2, 3, 10.0 + step + step), std::uint16_t{3}, "Next step: next in-between"); + expect_eq(tracker.sample(&key, 0, 0, 3, 10.0 + step + step * 2), std::uint16_t{5}, + "A looping cycle wraps through the closing in-between"); + expect_eq(tracker.sample(&key, 0, 2, 3, 10.0 + step + step * 3), std::uint16_t{4}, + "A non-consecutive jump shows the original frame"); + expect_eq(tracker.sample(&key, 1, 1, 3, 10.0 + step + step * 4), std::uint16_t{2}, + "A sequence change starts over"); + + // --- draw-time swap + const auto directory = std::filesystem::temp_directory_path() / "iee_interp_test"; + std::filesystem::create_directories(directory); + { + std::ofstream file(directory / "TESTBAM.bam", std::ios::binary); + file.write(reinterpret_cast(good.data()), static_cast(good.size())); + } + auto originalImage = make_raw_bam(3, {{0, 1, 2}}); + ExpandedBamView originalView{}; + expect_true(parse_expanded_bam(originalImage, originalView), "Fixture BAM parses"); + CResCell original{}; + const char resref[] = "testbam"; + original.baseclass_0.resref = resref; + original.baseclass_0.pData = originalImage.data(); + original.baseclass_0.bLoaded = true; + original.m_pBamHeader = originalView.header; + original.m_pFrames = originalView.frames; + original.m_pSequences = originalView.sequences; + original.m_pFrameList = originalView.frameList; + CVidCell cell{}; + cell.pRes = &original; + cell.m_nCurrentFrame = 1; + int stale = 0; + cell.m_pFrame = &stale; + + AnimationInterpolator interp; + interp.set_directory(directory); + interp.touch(&cell, 1.0); + expect_true(cell.pRes == &original && cell.m_nCurrentFrame == 1, + "Outside a draw scope nothing is swapped"); + interp.begin_scope(); + interp.touch(&cell, 1.0); + expect_true(cell.pRes != &original && cell.pRes->m_pFrames != original.m_pFrames && + cell.pRes->baseclass_0.bLoaded && cell.m_nCurrentFrame == 2 && + cell.m_pFrame == nullptr, + "Inside a scope the cell points at the expanded BAM and the mapped frame"); + const auto* swapped = cell.pRes; + interp.touch(&cell, 1.0); + expect_true(cell.pRes == swapped && cell.m_nCurrentFrame == 2, "A second touch is a no-op"); + interp.end_scope(); + expect_true(cell.pRes == &original && cell.m_nCurrentFrame == 1 && cell.m_pFrame == nullptr, + "Closing the scope restores the logic resource and frame"); + expect_eq(interp.loaded_count(), std::size_t{1}, "The expanded file is read once"); + + cell.m_nCurrentFrame = 2; + interp.begin_scope(); + interp.touch(&cell, 1.01); + expect_eq(cell.m_nCurrentFrame, std::int16_t{3}, "A logic step draws the in-between first"); + interp.end_scope(); + + cell.m_nCurrentFrame = 7; // outside the logic cycle + interp.begin_scope(); + interp.touch(&cell, 1.02); + expect_true(cell.pRes == &original && cell.m_nCurrentFrame == 7, + "A frame outside the logic cycle is left to the engine"); + interp.end_scope(); + + CResCell other{}; + const char missing[] = "NOFILE"; + other = original; + other.baseclass_0.resref = missing; + CVidCell otherCell{}; + otherCell.pRes = &other; + interp.begin_scope(); + interp.touch(&otherCell, 1.0); + expect_true(otherCell.pRes == &other, "A BAM without an expanded file draws unchanged"); + interp.end_scope(); + std::error_code cleanup; + std::filesystem::remove_all(directory, cleanup); +} + void test_build_area_effect_points() { using namespace iee::game; @@ -1908,6 +2061,7 @@ int main() { test_decode_object_array_globals(); test_collect_area_static_animations(); test_sprite_motion_tracker(); + test_animation_interpolation(); test_build_area_effect_points(); test_config_detection_section(); diff --git a/tools/InfinityEngine-Enhancer.sample.ini b/tools/InfinityEngine-Enhancer.sample.ini index cef5b12..922d2f3 100644 --- a/tools/InfinityEngine-Enhancer.sample.ini +++ b/tools/InfinityEngine-Enhancer.sample.ini @@ -13,6 +13,10 @@ LODBias = -0.25 ; Draw creatures gliding between the engine's logic ticks instead of stepping ; at the logic rate. Only useful with EEex "Uncap FPS"; BGEE 2.7.3 only. SmoothSpriteMovement = false +; Draw creature animations with the in-between frames found in iee-interp/ +; (expanded BAMs, one per original resref). Animations without an expanded +; file are unaffected. Only useful with EEex "Uncap FPS"; BGEE 2.7.3 only. +InterpolateAnimations = false [Detection] ; Collect the current area's authored ambient animations from engine memory From 948f69f3fc4277044bf2fb2543514641cea20280 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 3 Oct 2026 23:07:54 +0200 Subject: [PATCH 30/57] Animation interpolation: all-or-nothing per creature, guarded restore, field log A creature's body, weapon, shield and helmet are separate frame-synced BAMs; swapping only some would draw the body half a step behind its weapon. A creature is now interpolated only when every cell it drew in its previous scope has an expanded BAM, and its first scope only observes. The scope restore leaves a cell alone if the engine re-pointed it during the draw. Each expanded BAM that loads, fails validation, or is missing is logged once. --- docs/architecture.md | 9 +++ ...26-10-03-graphics-uplift-roadmap-design.md | 62 ++++++++++++++++--- src/iee/game/animation_interp.cpp | 53 +++++++++++++++- src/iee/game/animation_interp.h | 22 ++++++- src/iee/hooks.cpp | 2 +- tests/iee_tests.cpp | 28 ++++++--- 6 files changed, 153 insertions(+), 23 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 2d71264..1f7ad4d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -80,6 +80,15 @@ The supported runtime is intentionally narrow: one EEex-loaded Windows DLL, one (default off); the three targets resolve as a set or not at all, and only the 2.7.3 manifest carries their patterns. +`src/iee/game/animation_interp.*` + +- Host-safe draw-time substitution of expanded (frame-interpolated) BAMs: + validation of the expanded image, the per-cell frame chooser, and the + scoped `CVidCell` swap/restore. See item F of the 2026-10-03 roadmap for + the format and the engine facts it relies on. +- Driven from `hooks.cpp`: `CGameSprite::Render` opens the scope, the three + `CVidCell` frame accessors call `touch`. + `src/iee/game/object_statics.*` - Host-safe decode of the `CGameObjectArray` globals out of the engine's diff --git a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md index db6e347..07bc4e7 100644 --- a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md +++ b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md @@ -203,15 +203,59 @@ A sibling texture per PVRZ page, generated offline alongside the upscale. ### F. Animation frame in-betweening -At the sprite render, decode frame N and N+1 with the same live palette and -blend in our own shader by tick progress. - -- Costs one extra CPU blit per sprite and a small animation latency. -- Crossfade ghosts on fast limb motion; motion-compensated blending is - plausible at ~100 px but unproven on this art. -- Starts as an offline spike on extracted BAM frames. No DLL work until the - spike shows acceptable quality. -- PVRZ-backed sprites need a separate path. +Owner decision 2026-10-03: in-between frames are generated ahead of drawing +with RIFE and stored as expanded BAMs; the engine keeps drawing them itself, +so palettes, recolouring, wall clipping and layering are untouched. + +**Expanded BAM** (`iee-interp/.bam` in the game folder): an +uncompressed BAM V1 with the original palette and cycle count. For an +original cycle `[f0 .. f(L-1)]` the expanded cycle is +`[f0, m(0,1), f1, m(1,2), ..., f(L-1), m(L-1,0)]`: original frame k at 2k, +the in-between leading to k+1 at 2k+1. A slot with no usable in-between +repeats frame k. + +**Draw-time swap** (implemented, `game::AnimationInterpolator`; in-game +validation pending). Logic must keep the original BAM, because it counts +frames from it (`CVidCell::IsEndOfSequence`, one frame per creature update). +So only while `CGameSprite::Render` runs, each `CVidCell` the engine resolves +a frame for is pointed at a private `CResCell` over the expanded image and at +the frame for the current instant; both are restored when the draw returns. + +- Hooks: `CVidCell::GetFrame` (`0x4118A0`), `GetCurrentCenterPoint` + (`0x411660`), `GetCurrentFrameSize` (`0x411780`) — every path that first + reads a cell's frame during a draw goes through one of them. +- Why a private `CResCell` is safe: `CRes::Demand` (`0x3F6D50`) returns + `pData` immediately when `bLoaded` is set, and + `CResCell::GetFrameData` (`0x3F79E0`) only adds the frame offset to + `m_pBamHeader`. The private cell is never in the engine's resource table, + so nothing evicts or frees it. +- Timing: for the first half of the observed animation step after logic + moves k-1 -> k the in-between is shown, then frame k. Half a step of lag, + no guessing ahead. +- A creature is interpolated only if every cell it drew last time has an + expanded BAM (body, weapon, shield and helmet are separate, frame-synced + BAMs; a partial swap would draw the body behind its weapon). +- Gated by `[Rendering] InterpolateAnimations` (default off, 2.7.3 only). + The log names each expanded BAM loaded and each resref that has none. + +**Generation** (prototype only so far, Linux Python in a scratch folder): +RIFE v4.25-heavy through `rife-ncnn-vulkan` in batch mode, colour and +silhouette side by side at 4x, result mapped back to palette indices +restricted to the entries the two source frames use (an unrestricted match +put 17% of pixels on entries from other recolour ranges). Pairs whose +silhouettes overlap too little, and in-betweens whose area drifts from the +pair's mean, are dropped. Measured: about 40 frame pairs per second on an +RTX 4090 including PNG I/O. + +Tested alternatives: RIFE v4.6/v4.25/v4.26 variants (v4.25-heavy best), +IFRNet (worse), plain crossfade (ghosting). GIMM-VFI, FILM and GMFSS need a +PyTorch stack and were not tested. No model handled a limb that crosses its +own length in one step. + +Still to build: the generator as our own Windows program (runs natively and +under Proton, calls the RIFE executable), invoked for the animations an area +needs with a status line on the loading screen; equipment overlay families; +non-creature animations. ### G. Small polish diff --git a/src/iee/game/animation_interp.cpp b/src/iee/game/animation_interp.cpp index f16dfe8..b4a3a84 100644 --- a/src/iee/game/animation_interp.cpp +++ b/src/iee/game/animation_interp.cpp @@ -7,6 +7,7 @@ #include #include +#include "iee/core/logger.h" #include "iee/core/pattern_scanner.h" namespace iee::game { @@ -129,7 +130,16 @@ std::shared_ptr AnimationInterpolator::load( parse_expanded_bam(expanded->image, expanded->view)) { result = std::move(expanded); ++loaded_; + LOG_INFO("Animation interpolation: loaded expanded BAM {} ({} frames)", resref, + result->view.header->nFrames); + } else { + LOG_WARN("Animation interpolation: {} is not a valid expanded BAM; ignored", + path.string()); } + } else if (!error) { + LOG_WARN("Animation interpolation: {} has an unusable size; ignored", path.string()); + } else { + LOG_INFO("Animation interpolation: no expanded BAM for {}", resref); } files_.emplace(resref, result); return result; @@ -170,6 +180,9 @@ AnimationInterpolator::Resource* AnimationInterpolator::resource_for(CResCell* o bamHeader_st originalHeader{}; if (!core::safe_read(snapshot.m_pBamHeader, originalHeader) || originalHeader.nSequences != view.header->nSequences) { + LOG_WARN("Animation interpolation: expanded BAM {} does not match the game's cycle count; " + "ignored", + resref); resource->expanded.reset(); } else { resource->cell = snapshot; @@ -191,17 +204,34 @@ AnimationInterpolator::Resource* AnimationInterpolator::resource_for(CResCell* o return resources_.emplace(original, std::move(resource)).first->second.get(); } +void AnimationInterpolator::begin_scope(const void* owner) noexcept { + if (depth_++ > 0) return; + owner_ = owner; + scopeTouched_ = 0; + scopeMissed_ = false; + observeOnly_ = true; + try { + if (const auto known = owners_.find(owner); known != owners_.end()) { + observeOnly_ = !known->second; + } + } catch (...) { + } +} + void AnimationInterpolator::touch(CVidCell* cell, double now) noexcept { if (depth_ <= 0 || !cell) return; try { for (const auto& swap : swaps_) { if (swap.cell == cell) return; } - if (swaps_.size() >= kMaxSwapsPerScope || !cell->pRes) return; + if (!cell->pRes) return; + ++scopeTouched_; auto* original = cell->pRes; auto* resource = resource_for(original); - if (!resource || !resource->expanded) return; - + if (!resource || !resource->expanded) { + scopeMissed_ = true; + return; + } const auto sequence = cell->m_nCurrentSequence; const auto frame = cell->m_nCurrentFrame; const auto& view = resource->expanded->view; @@ -210,8 +240,10 @@ void AnimationInterpolator::touch(CVidCell* cell, double now) noexcept { !core::safe_read(original->m_pSequences + sequence, logic) || logic.nFrames <= 0 || frame < 0 || frame >= logic.nFrames || view.sequences[sequence].nFrames != logic.nFrames * 2) { + scopeMissed_ = true; return; } + if (observeOnly_ || swaps_.size() >= kMaxSwapsPerScope) return; const auto shown = tracker_.sample(cell, sequence, frame, logic.nFrames, now); swaps_.push_back({cell, original, frame}); cell->pRes = &resource->cell; @@ -219,6 +251,7 @@ void AnimationInterpolator::touch(CVidCell* cell, double now) noexcept { cell->m_pFrame = nullptr; } catch (...) { // Interpolation is cosmetic; any doubt draws the original frame. + scopeMissed_ = true; } } @@ -226,10 +259,24 @@ void AnimationInterpolator::end_scope() noexcept { if (depth_ <= 0) return; if (--depth_ > 0) return; for (const auto& swap : swaps_) { + // Restore only our own swap: if the engine re-pointed the cell during the + // draw, its resource is the newer one and must win. + bool ours = false; + for (const auto& [original, resource] : resources_) { + if (swap.cell->pRes == &resource->cell) { + ours = true; + break; + } + } + if (!ours) continue; swap.cell->pRes = swap.original; swap.cell->m_nCurrentFrame = swap.frame; swap.cell->m_pFrame = nullptr; } swaps_.clear(); + try { + owners_[owner_] = scopeTouched_ > 0 && !scopeMissed_; + } catch (...) { + } } } // namespace iee::game diff --git a/src/iee/game/animation_interp.h b/src/iee/game/animation_interp.h index 4629ea0..df4e43e 100644 --- a/src/iee/game/animation_interp.h +++ b/src/iee/game/animation_interp.h @@ -68,19 +68,30 @@ class AnimationFrameTracker { // frame back. Expanded files are read from `directory/.bam` on first // use; a BAM without one draws exactly as before. // +// A creature is drawn from several frame-synced cells (body, weapon, shield, +// helmet), each its own BAM. Swapping only some of them would draw the body +// half a step behind its weapon, so a creature is interpolated only when +// every cell it drew in its previous scope had an expanded BAM: each owner's +// first scope (and any scope after a miss) only observes. +// // Single-threaded: every call must come from the engine's render thread. class AnimationInterpolator { public: void set_directory(std::filesystem::path directory) { directory_ = std::move(directory); } - void begin_scope() noexcept { ++depth_; } - // Restores every cell swapped since the matching begin_scope. + // `owner` identifies the creature being drawn. + void begin_scope(const void* owner) noexcept; + // Restores every cell swapped since the matching begin_scope and records + // whether the owner's cells were all covered. void end_scope() noexcept; // Inside a scope: swap `cell` if it is not swapped yet and an expanded BAM // exists for its resource. Outside a scope, or on any doubt, does nothing. void touch(CVidCell* cell, double now) noexcept; // Area change: forget per-cell animation timing (loaded files are kept). - void reset_timing() noexcept { tracker_.clear(); } + void reset_timing() noexcept { + tracker_.clear(); + owners_.clear(); + } [[nodiscard]] std::size_t loaded_count() const noexcept { return loaded_; } @@ -104,6 +115,11 @@ class AnimationInterpolator { std::filesystem::path directory_; int depth_{}; + const void* owner_{}; + bool observeOnly_{true}; + bool scopeMissed_{}; + std::size_t scopeTouched_{}; + std::unordered_map owners_; // owner -> last scope fully covered std::vector swaps_; std::unordered_map> resources_; std::unordered_map> files_; diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index c786b7e..31f1ad6 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -395,7 +395,7 @@ static void detour_sprite_render(void* sprite, void* a, void* b) { call_with_smoothed_position(g_spriteRenderHook, g_spriteMotion, sprite, a, b); return; } - g_animationInterp.begin_scope(); + g_animationInterp.begin_scope(sprite); call_with_smoothed_position(g_spriteRenderHook, g_spriteMotion, sprite, a, b); g_animationInterp.end_scope(); } diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index acc4b64..de050f2 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1845,7 +1845,13 @@ void test_animation_interpolation() { interp.touch(&cell, 1.0); expect_true(cell.pRes == &original && cell.m_nCurrentFrame == 1, "Outside a draw scope nothing is swapped"); - interp.begin_scope(); + int owner = 0; + interp.begin_scope(&owner); + interp.touch(&cell, 1.0); + expect_true(cell.pRes == &original && cell.m_nCurrentFrame == 1, + "An owner's first scope only observes which cells are covered"); + interp.end_scope(); + interp.begin_scope(&owner); interp.touch(&cell, 1.0); expect_true(cell.pRes != &original && cell.pRes->m_pFrames != original.m_pFrames && cell.pRes->baseclass_0.bLoaded && cell.m_nCurrentFrame == 2 && @@ -1860,13 +1866,13 @@ void test_animation_interpolation() { expect_eq(interp.loaded_count(), std::size_t{1}, "The expanded file is read once"); cell.m_nCurrentFrame = 2; - interp.begin_scope(); + interp.begin_scope(&owner); interp.touch(&cell, 1.01); expect_eq(cell.m_nCurrentFrame, std::int16_t{3}, "A logic step draws the in-between first"); interp.end_scope(); cell.m_nCurrentFrame = 7; // outside the logic cycle - interp.begin_scope(); + interp.begin_scope(&owner); interp.touch(&cell, 1.02); expect_true(cell.pRes == &original && cell.m_nCurrentFrame == 7, "A frame outside the logic cycle is left to the engine"); @@ -1878,10 +1884,18 @@ void test_animation_interpolation() { other.baseclass_0.resref = missing; CVidCell otherCell{}; otherCell.pRes = &other; - interp.begin_scope(); - interp.touch(&otherCell, 1.0); - expect_true(otherCell.pRes == &other, "A BAM without an expanded file draws unchanged"); - interp.end_scope(); + // A creature with one uncovered cell is not interpolated at all, so its + // body never runs half a step behind its weapon. + cell.m_nCurrentFrame = 1; + int armed = 0; + for (int pass = 0; pass < 3; ++pass) { + interp.begin_scope(&armed); + interp.touch(&cell, 2.0 + pass); + interp.touch(&otherCell, 2.0 + pass); + expect_true(cell.pRes == &original && otherCell.pRes == &other, + "A creature with an uncovered cell draws every cell unchanged"); + interp.end_scope(); + } std::error_code cleanup; std::filesystem::remove_all(directory, cleanup); } From 5879249d6889851a008281a8ea9fc562a1e06bc0 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sat, 3 Oct 2026 23:44:39 +0200 Subject: [PATCH 31/57] Animation interpolation: accept the BAM empty-slot marker in lookup tables Mirrored-direction BAMs (the *E files) list 0xFFFF for cycles they do not carry. The validator rejected the whole file for it, so east-facing creatures were never interpolated. --- src/iee/game/animation_interp.cpp | 4 +++- tests/iee_tests.cpp | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/iee/game/animation_interp.cpp b/src/iee/game/animation_interp.cpp index b4a3a84..b8550a3 100644 --- a/src/iee/game/animation_interp.cpp +++ b/src/iee/game/animation_interp.cpp @@ -15,6 +15,7 @@ namespace { constexpr std::uint32_t kBamSignature = 0x204D4142; // "BAM " constexpr std::uint32_t kBamVersion1 = 0x20203156; // "V1 " constexpr std::uint32_t kRawFrameFlag = 0x80000000u; +constexpr std::uint16_t kBamNoFrame = 0xFFFF; constexpr std::size_t kPaletteBytes = 256 * 4; constexpr std::size_t kMaxExpandedBamBytes = 64u * 1024u * 1024u; constexpr std::size_t kMaxSwapsPerScope = 32; @@ -69,7 +70,8 @@ bool parse_expanded_bam(std::span image, ExpandedBamView& out) noexce image.data() + header.nFrameListOffset + (sequence.nStartingFrame + slot) * sizeof(listed), sizeof(listed)); - if (listed >= header.nFrames) return false; + // 0xFFFF is the format's "no frame in this slot"; the engine skips it. + if (listed >= header.nFrames && listed != kBamNoFrame) return false; } } diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index de050f2..6697680 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1788,6 +1788,9 @@ void test_animation_interpolation() { expect_true(!parse_expanded_bam(bad, view), "A frame whose pixels overrun the image is rejected"); bad = make_raw_bam(5, {{0, 3, 1, 9, 2, 2}}); expect_true(!parse_expanded_bam(bad, view), "A lookup entry beyond the frame table is rejected"); + auto sparse = make_raw_bam(5, {{0, 3, 1, 4, 2, 2}, {0xFFFF, 0xFFFF}}); + expect_true(parse_expanded_bam(sparse, view), + "The format's empty-slot marker (0xFFFF) is a valid lookup entry"); bad = good; frameTableEntry_st rle{}; std::memcpy(&rle, bad.data() + sizeof(bamHeader_st), sizeof(rle)); From a01139cd025054e879030a0fcaa84c1dcb67dd32 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Sun, 4 Oct 2026 02:45:52 +0200 Subject: [PATCH 32/57] docs: record the FSR-family decision and the revised phase order --- ...26-10-03-graphics-uplift-roadmap-design.md | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md index 07bc4e7..9e4fcc5 100644 --- a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md +++ b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md @@ -148,13 +148,20 @@ each sprite's draw position between ticks inside a `CGameSprite::Render` hook One override shader that covers every character and effect quad. -- B1 upscaling kernel (pixel-art family; `FPCRSPRT` holds the developers' own - abandoned xBR attempt as a reference). +- B1 sharpening kernel. Owner decision 2026-10-04: an FSR-family filter. + Only the spatial part applies (FSR1: edge-adaptive upscale plus + sharpening). FSR2/3 and other temporal upscalers do not fit, because a + pre-rendered sprite frame yields no new samples over time. The aim is clean + edges when the game magnifies sprites, not added detail. - B2 premultiplied-alpha halo fix (all three parts together, per the 2026-06-10 design §4.8). - B3 soft shadows: tag `SHADOW_ENTRY` pixels at blit time, blur in shader. - B4 smooth wall occlusion: replace the dithered stipple with alpha. - Risk: atlas neighbour bleed; **gate** on per-quad UV rectangle availability. +- Rejected 2026-10-04: offline AI upscaling of sprites. Real-ESRGAN (general, + anime and anime-video models) and SeedVR 7B (owner's test) smooth the + sprites without adding real detail; a 25-pixel-wide figure does not hold + enough to reconstruct. ### C. Effect replacement framework @@ -265,17 +272,17 @@ item icons. ## 3. Order -| # | Item | Why here | +Revised 2026-10-04 after A and F's engine side were confirmed in game. + +| Phase | Items | Why here | |---|---|---| -| 1 | C1 | Existing work on the branch; make it correct before building on it | -| 2 | A | One hook, most certain perceived gain | -| 3 | B1, B2 | Sprites are the most visible mismatch against 4x maps | -| 4 | C2, C3 | Effects plus the light they cast | -| 5 | D1, D2 | Shared offscreen plumbing | -| 6 | B3, B4 | Ride on the `fpSprite` replacement | -| 7 | F spike | Quality question answered offline first | -| 8 | E1 → E2 → E3 | Largest visual jump, largest asset cost | -| 9 | C4, D3, G | Polish | +| 1 | F: in-betweens-only pack with source fingerprints, in-memory merge, standalone generator, vanilla pack as its own mod; narrow the all-cells rule to frame-synced cells | The owner's favourite feature currently depends on hand-generated whole files | +| 2 | B1, B2 | Sprites are the most visible mismatch against 4x maps | +| 3 | C1 confirmation, C2 (blood first), C3 | Effects plus the light they cast | +| 4 | D1, D2 | Shared offscreen plumbing | +| Later | B3, B4, E1 -> E2 -> E3, C4, D3, G | Polish and the large asset-driven items | + +Done: A (movement smoothing), F's draw-time swap. ## 4. Reversed decisions From 30d518f854ad3e5548b5c26cdeb200f42929a6bf Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Wed, 7 Oct 2026 20:29:14 +0200 Subject: [PATCH 33/57] docs: soft fog of war and bloom design and plan --- .../plans/2026-10-07-soft-fog-and-bloom.md | 1866 +++++++++++++++++ .../2026-10-07-soft-fog-and-bloom-design.md | 246 +++ 2 files changed, 2112 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-07-soft-fog-and-bloom.md create mode 100644 docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md diff --git a/docs/superpowers/plans/2026-10-07-soft-fog-and-bloom.md b/docs/superpowers/plans/2026-10-07-soft-fog-and-bloom.md new file mode 100644 index 0000000..3901ca8 --- /dev/null +++ b/docs/superpowers/plans/2026-10-07-soft-fog-and-bloom.md @@ -0,0 +1,1866 @@ +# Soft Fog of War and Bloom Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add an INI-gated soft fog-of-war edge and a threshold bloom to the BGEE 2.7.3 world view, leaving the UI and every other build untouched. + +**Architecture:** One detour on `CInfinity::RenderFog` calls the engine's own queue flush (`DrawFlush_GL`) so the finished world is in the framebuffer, runs bloom on it, lets the engine draw its fog into our white-cleared target, flushes again, then multiplies the framebuffer by the blurred fog. Pure sizing logic is host-tested; GL work lives in one feature module behind a state guard. + +**Tech Stack:** C++20, CMake, MinHook (existing `core::Hook`), OpenGL 4.6 compatibility context through the existing `game::gl::OpenGLFunctions` table, GLSL 3.30, the repo's single-file test executable `iee_tests`. + +**Spec:** `docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md` + +## Global Constraints + +- BGEE 2.7.3 only. Other builds leave both features off (manifest entries empty). +- Every new address goes through `build_manifest` as pattern + reference RVA. Never an ad-hoc constant. +- All settings default off/neutral: `SoftFogOfWar=false`, `SoftFogRadius=24`, `Bloom=false`, `BloomThreshold=0.80`, `BloomStrength=0.35`. +- Any failure (unresolved pattern, missing GL entry point, incomplete framebuffer, shader error) logs once, latches the feature off, and calls the original `RenderFog` untouched. +- Never detour `CVisibilityMap::BltFogOWar3d`. +- With both settings off the detour must not flush: the frame is the engine's own. +- Restore every GL state our passes touch; the engine caches hardware state in `gl.hwState`. +- No new dependencies. No AI attribution in commits or PRs. +- Windows-only code (`windows.h`, GL table, MinHook) stays out of `iee_common` and the host tests. +- CI uses GCC 13 with `-Werror`; build with `-DIEE_WARNINGS_AS_ERRORS=ON` locally. + +## Review Focus + +1. **Window resize or resolution change mid-session.** Targets must be rebuilt at the new size; no stretched or cropped fog. Pinned by the extent-change path in Task 6 (in-game check) and `test_world_post_plan` extent cases in Task 2. +2. **Zoom at both extremes.** Fog radius must scale with zoom and never request more blur levels than the target can hold. Pinned by `blur_plan_for_radius` tests in Task 2 and the two-zoom log check in Task 7. +3. **Nothing to capture.** A fully explored area, or fog switched off in the game options, queues no fog triangles; the result must be the plain scene, not a black or white screen. Pinned by the Task 6 in-game checklist (white target multiplies by 1). +4. **Mid-frame engine flush from elsewhere.** A screenshot (`DrawReadPixels`) must still capture a correct frame with the effects on. Pinned by the Task 6 in-game checklist. +5. **Garbage INI values.** `SoftFogRadius = nan`, negative strength, threshold above 1 must clamp to safe values. Pinned by `test_config_world_post` in Task 3. + +## File Structure + +| File | Status | Responsibility | +|---|---|---| +| `src/iee/game/world_post_plan.h/.cpp` | new | Pure sizing: extents, blur level count, radius-to-plan, zoom scale. In `iee_common`, host-tested | +| `src/iee/core/config.h/.cpp` | modify | Five new `[Rendering]` keys, clamped in `normalize` | +| `src/iee/game/build_manifest.h/.cpp` | modify | `renderFog` and `drawFlush` patterns + reference RVAs | +| `src/iee/game/game_addrs.h/.cpp` | modify | Resolve the two addresses when a world-post setting is on | +| `src/iee/game/opengl_types.h/.cpp` | modify | Framebuffer, vertex-array, buffer and fixed-function entry points; `postProcessAvailable` | +| `src/iee/core/gl_pass_guard.h` | new | RAII save/restore of the non-texture state our passes touch | +| `src/iee/features/world_post.h/.cpp` | new | GL resources and the passes; the only file that draws | +| `src/iee/hooks.cpp` | modify | `detour_render_fog`, install/uninstall, area-load notification | +| `CMakeLists.txt` | modify | New sources in `iee_common` and the DLL | +| `tests/iee_tests.cpp` | modify | Plan, config and manifest tests | +| `docs/…` , `../handoff.md` | modify | Evidence, status, runtime facts | + +Build commands used throughout (run from `InfinityEngine-Enhancer/`): + +```bash +# Host tests (CI flags) +cmake -S . -B build-host -DBUILD_TESTING=ON -DIEE_WARNINGS_AS_ERRORS=ON -DIEE_ENABLE_SANITIZERS=ON +cmake --build build-host --target iee_tests && ctest --test-dir build-host --output-on-failure +# Windows compile check +cmake -S . -B build-mingw -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake -DIEE_BUILD_WINDOWS_DLL=ON -DIEE_WARNINGS_AS_ERRORS=ON +cmake --build build-mingw +``` + +Shell quirks on the owner's machine: `ls` is aliased (use `command ls`), zsh `noclobber` is on (use `>|`), unmatched globs are errors. + +Work on a new branch `feature/world-post` created from `feature/are-animation-detection` (the hooks this builds on live there). + +--- + +### Task 1: Depth-state question — resolved before implementation (no work) + +Spec §2.2 raised the risk that an extra `DrawFlush_GL` disturbs depth ordering. It was settled offline on 2026-10-07 and recorded in the spec; nothing here needs doing. Kept as a numbered entry so later task numbers match the spec's references. + +Finding (2.7.3, whole-binary disassembly searched for every reference): + +- `gl.v.z` (`0x142F73F7C`) is written only by `DrawInit_GL` and `DrawFlush_GL`, always to the same value, and read only by `DrawVertex_GL`. It is constant for the whole session. +- `gl.depthLockState` (`0x142F74048`) is written by the same two functions and never read. +- `DrawLockSurface_GL` (`0x42CAF0`) calls `DrawFlush_GL` whenever the engine's 1024x1024 sprite atlas fills, so flushes in the middle of the world pass already happen in vanilla on busy scenes. + +Verdict: **SAFE**. Call `DrawFlush_GL` directly; no engine globals need saving. + +--- + +### Task 2: Sizing logic (`world_post_plan`) + +**Files:** +- Create: `src/iee/game/world_post_plan.h`, `src/iee/game/world_post_plan.cpp` +- Modify: `CMakeLists.txt` (the `iee_common` source list, after `src/iee/game/wed_runtime.cpp`) +- Test: `tests/iee_tests.cpp` + +**Interfaces:** +- Produces (namespace `iee::game`): + - `struct Extent { int width{}; int height{}; }` with `operator==` + - `inline constexpr int kMaxBlurLevels = 6;` + - `struct BlurPlan { int levels{}; float offset{1.0f}; };` + - `Extent half_extent(Extent) noexcept;` + - `int available_blur_levels(Extent base) noexcept;` + - `BlurPlan blur_plan_for_radius(float radiusPixels, Extent base) noexcept;` + - `int bloom_levels(Extent base) noexcept;` + - `float pixels_per_world_pixel(float viewportWidth, float viewWorldWidth) noexcept;` + +A blur chain built on a `base` image has level `i` at `base` halved `i + 1` times. `levels == 0` means "do not blur". + +- [ ] **Step 1: Write the failing test** + +Add the include next to the other `iee/game` includes in `tests/iee_tests.cpp`: + +```cpp +#include "iee/game/world_post_plan.h" +``` + +Add before `int main()`: + +```cpp +void test_world_post_plan() { + using namespace iee::game; + + expect_true(half_extent({1920, 1080}) == Extent{960, 540}, "Half extent halves both sides"); + expect_true(half_extent({1, 1}) == Extent{1, 1}, "Half extent never reaches zero"); + expect_true(half_extent({0, 0}) == Extent{1, 1}, "A degenerate extent still yields 1x1"); + + expect_eq(available_blur_levels({1920, 1080}), 6, "A full-HD image supports the level cap"); + expect_eq(available_blur_levels({64, 64}), 3, "Levels stop before a side drops below 8"); + expect_eq(available_blur_levels({10, 10}), 0, "A tiny image supports no blur levels"); + expect_eq(available_blur_levels({0, 0}), 0, "A degenerate image supports no blur levels"); + + const auto none = blur_plan_for_radius(0.0f, {1920, 1080}); + expect_eq(none.levels, 0, "Radius 0 means no blur"); + expect_eq(blur_plan_for_radius(std::numeric_limits::quiet_NaN(), {1920, 1080}).levels, 0, + "A NaN radius means no blur"); + expect_eq(blur_plan_for_radius(-5.0f, {1920, 1080}).levels, 0, "A negative radius means no blur"); + + const auto small = blur_plan_for_radius(4.0f, {1920, 1080}); + expect_eq(small.levels, 1, "A 4 px radius needs one level"); + expect_eq(small.offset, 1.0f, "A radius equal to the level reach uses offset 1"); + + const auto typical = blur_plan_for_radius(24.0f, {1920, 1080}); + expect_eq(typical.levels, 4, "A 24 px radius needs four levels (reach 32)"); + expect_eq(typical.offset, 0.75f, "Offset scales the reach down to the radius"); + + const auto huge = blur_plan_for_radius(1000.0f, {1920, 1080}); + expect_eq(huge.levels, 6, "Levels are capped by what the image supports"); + expect_eq(huge.offset, 1.5f, "Offset is clamped so taps never skip texels badly"); + + expect_eq(blur_plan_for_radius(24.0f, {10, 10}).levels, 0, + "No blur is planned on an image too small to hold a level"); + + expect_eq(bloom_levels({960, 540}), 5, "Bloom uses at most five levels"); + expect_eq(bloom_levels({40, 40}), 2, "Bloom levels shrink with the image"); + + expect_eq(pixels_per_world_pixel(3840.0f, 1920.0f), 2.0f, "Scale is viewport over world width"); + expect_eq(pixels_per_world_pixel(3840.0f, 0.0f), 1.0f, "An unknown world width falls back to 1"); + expect_eq(pixels_per_world_pixel(0.0f, 1920.0f), 1.0f, "An unknown viewport falls back to 1"); + expect_eq(pixels_per_world_pixel(100000.0f, 1.0f), 8.0f, "Scale is clamped to a sane maximum"); + expect_eq(pixels_per_world_pixel(std::numeric_limits::infinity(), 100.0f), 1.0f, + "A non-finite input falls back to 1"); +} +``` + +Call it in `main()` after `test_animation_interpolation();`: + +```cpp + test_world_post_plan(); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `cmake --build build-host --target iee_tests` +Expected: compile error, `iee/game/world_post_plan.h: No such file or directory`. + +- [ ] **Step 3: Write the header** + +`src/iee/game/world_post_plan.h`: + +```cpp +#pragma once + +namespace iee::game { +// Sizing for the world post-processing passes (soft fog of war, bloom). Pure +// arithmetic so it can be tested without a GL context. + +struct Extent { + int width{}; + int height{}; + friend bool operator==(const Extent&, const Extent&) = default; +}; + +inline constexpr int kMaxBlurLevels = 6; + +// A dual-filter blur: `levels` successive half-size images below the source, +// sampled with `offset` texels of spread. levels == 0 means "do not blur". +struct BlurPlan { + int levels{}; + float offset{1.0f}; +}; + +Extent half_extent(Extent extent) noexcept; +// How many half-size levels fit below `base` before a side drops under 8 px. +int available_blur_levels(Extent base) noexcept; +// The plan whose blur reaches about `radiusPixels` on an image of `base` size. +BlurPlan blur_plan_for_radius(float radiusPixels, Extent base) noexcept; +int bloom_levels(Extent base) noexcept; +// Screen pixels per world pixel at the current zoom; 1 when either is unknown. +float pixels_per_world_pixel(float viewportWidth, float viewWorldWidth) noexcept; +} // namespace iee::game +``` + +- [ ] **Step 4: Write the implementation** + +`src/iee/game/world_post_plan.cpp`: + +```cpp +#include "iee/game/world_post_plan.h" + +#include +#include + +namespace iee::game { +namespace { +constexpr int kMinBlurExtent = 8; +constexpr int kMaxBloomLevels = 5; +} // namespace + +Extent half_extent(Extent extent) noexcept { + return {std::max(1, extent.width / 2), std::max(1, extent.height / 2)}; +} + +int available_blur_levels(Extent base) noexcept { + int levels = 0; + Extent extent = base; + while (levels < kMaxBlurLevels) { + extent = half_extent(extent); + if (std::min(extent.width, extent.height) < kMinBlurExtent) break; + ++levels; + } + return levels; +} + +BlurPlan blur_plan_for_radius(float radiusPixels, Extent base) noexcept { + // Written so NaN fails the test. + if (!(radiusPixels >= 1.0f)) return {}; + const int available = available_blur_levels(base); + if (available == 0) return {}; + int levels = 1; + while (levels < available && static_cast(1 << (levels + 1)) < radiusPixels) ++levels; + const auto reach = static_cast(1 << (levels + 1)); + return {levels, std::clamp(radiusPixels / reach, 0.5f, 1.5f)}; +} + +int bloom_levels(Extent base) noexcept { + return std::min(kMaxBloomLevels, available_blur_levels(base)); +} + +float pixels_per_world_pixel(float viewportWidth, float viewWorldWidth) noexcept { + if (!std::isfinite(viewportWidth) || !std::isfinite(viewWorldWidth)) return 1.0f; + if (!(viewportWidth > 0.0f) || !(viewWorldWidth > 0.0f)) return 1.0f; + return std::clamp(viewportWidth / viewWorldWidth, 0.25f, 8.0f); +} +} // namespace iee::game +``` + +Add to `CMakeLists.txt` in the `iee_common` list, after `src/iee/game/wed_runtime.cpp`: + +```cmake + src/iee/game/world_post_plan.cpp +``` + +- [ ] **Step 5: Run the tests to verify they pass** + +Run: `cmake -S . -B build-host -DBUILD_TESTING=ON -DIEE_WARNINGS_AS_ERRORS=ON -DIEE_ENABLE_SANITIZERS=ON && cmake --build build-host --target iee_tests && ctest --test-dir build-host --output-on-failure` +Expected: `All InfinityEngine-Enhancer native tests passed`. + +- [ ] **Step 6: Commit** + +```bash +git add src/iee/game/world_post_plan.h src/iee/game/world_post_plan.cpp CMakeLists.txt tests/iee_tests.cpp +git commit -m "World post: sizing logic for blur chains and zoom scale" +``` + +--- + +### Task 3: Configuration keys + +**Files:** +- Modify: `src/iee/core/config.h` (struct `EngineConfig`), `src/iee/core/config.cpp` (`normalize`, `apply_kv` `[Rendering]` block, the save function's `Rendering` section) +- Test: `tests/iee_tests.cpp` + +**Interfaces:** +- Produces: `EngineConfig::softFogOfWar` (bool, false), `softFogRadius` (float, 24), `bloom` (bool, false), `bloomThreshold` (float, 0.80), `bloomStrength` (float, 0.35). After `ConfigManager::load` the floats are finite and within `[0,256]`, `[0,1]`, `[0,2]`. + +- [ ] **Step 1: Write the failing test** + +Add before `int main()` in `tests/iee_tests.cpp`: + +```cpp +void test_config_world_post() { + iee::core::EngineConfig defaults{}; + expect_true(!defaults.softFogOfWar && !defaults.bloom, "World post effects default off"); + expect_eq(defaults.softFogRadius, 24.0f, "Soft fog radius default"); + expect_eq(defaults.bloomThreshold, 0.80f, "Bloom threshold default"); + expect_eq(defaults.bloomStrength, 0.35f, "Bloom strength default"); + + const auto tempPath = + std::filesystem::current_path() / "InfinityEngine-Enhancer-world-post-test.ini"; + { + std::ofstream out(tempPath, std::ios::trunc); + out << "[Rendering]\n"; + out << "SoftFogOfWar = true\n"; + out << "SoftFogRadius = 40\n"; + out << "Bloom = true\n"; + out << "BloomThreshold = 0.6\n"; + out << "BloomStrength = 0.5\n"; + } + iee::core::EngineConfig cfg{}; + expect_true(iee::core::ConfigManager::load(tempPath, cfg), "World post keys should load"); + expect_true(cfg.softFogOfWar && cfg.bloom, "World post bools should parse"); + expect_eq(cfg.softFogRadius, 40.0f, "Soft fog radius should parse"); + expect_eq(cfg.bloomThreshold, 0.6f, "Bloom threshold should parse"); + expect_eq(cfg.bloomStrength, 0.5f, "Bloom strength should parse"); + + { + std::ofstream out(tempPath, std::ios::trunc); + out << "[Rendering]\n"; + out << "SoftFogRadius = 100000\n"; + out << "BloomThreshold = 7\n"; + out << "BloomStrength = -3\n"; + } + cfg = {}; + expect_true(iee::core::ConfigManager::load(tempPath, cfg), "Out-of-range values should load"); + expect_eq(cfg.softFogRadius, 256.0f, "Soft fog radius is clamped to its maximum"); + expect_eq(cfg.bloomThreshold, 1.0f, "Bloom threshold is clamped to 1"); + expect_eq(cfg.bloomStrength, 0.0f, "Bloom strength is clamped to 0"); + + { + std::ofstream out(tempPath, std::ios::trunc); + out << "[Rendering]\n"; + out << "SoftFogRadius = nan\n"; + out << "BloomThreshold = inf\n"; + out << "BloomStrength = nan\n"; + } + cfg = {}; + expect_true(iee::core::ConfigManager::load(tempPath, cfg), "Non-finite values should load"); + expect_eq(cfg.softFogRadius, 24.0f, "A non-finite radius falls back to the default"); + expect_eq(cfg.bloomThreshold, 0.80f, "A non-finite threshold falls back to the default"); + expect_eq(cfg.bloomStrength, 0.35f, "A non-finite strength falls back to the default"); + + std::error_code error; + std::filesystem::remove(tempPath, error); +} +``` + +Call it in `main()` after `test_config_detection_section();`: + +```cpp + test_config_world_post(); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `cmake --build build-host --target iee_tests` +Expected: compile error, `'struct iee::core::EngineConfig' has no member named 'softFogOfWar'`. + +- [ ] **Step 3: Implement** + +`src/iee/core/config.h`, after `bool interpolateAnimations = false;`: + +```cpp + bool softFogOfWar = false; + float softFogRadius = 24.0f; // world pixels; 0 = capture without blur + bool bloom = false; + float bloomThreshold = 0.80f; + float bloomStrength = 0.35f; +``` + +`src/iee/core/config.cpp`, in `normalize`, after the `lodBias` clamp: + +```cpp + if (!std::isfinite(cfg.softFogRadius)) cfg.softFogRadius = 24.0f; + if (!std::isfinite(cfg.bloomThreshold)) cfg.bloomThreshold = 0.80f; + if (!std::isfinite(cfg.bloomStrength)) cfg.bloomStrength = 0.35f; + cfg.softFogRadius = std::clamp(cfg.softFogRadius, 0.0f, 256.0f); + cfg.bloomThreshold = std::clamp(cfg.bloomThreshold, 0.0f, 1.0f); + cfg.bloomStrength = std::clamp(cfg.bloomStrength, 0.0f, 2.0f); +``` + +In `apply_kv`, in the `[Rendering]` chain, after the `InterpolateAnimations` branch and before `return;`: + +```cpp + else if (iequals(key, "SoftFogOfWar")) + assign_bool(cfg.softFogOfWar); + else if (iequals(key, "SoftFogRadius")) + assign_float(cfg.softFogRadius); + else if (iequals(key, "Bloom")) + assign_bool(cfg.bloom); + else if (iequals(key, "BloomThreshold")) + assign_float(cfg.bloomThreshold); + else if (iequals(key, "BloomStrength")) + assign_float(cfg.bloomStrength); +``` + +In the save function, after `write_bool(f, "InterpolateAnimations", cfg.interpolateAnimations);`: + +```cpp + write_bool(f, "SoftFogOfWar", cfg.softFogOfWar); + f << "SoftFogRadius = " << cfg.softFogRadius << "\n"; + write_bool(f, "Bloom", cfg.bloom); + f << "BloomThreshold = " << cfg.bloomThreshold << "\n"; + f << "BloomStrength = " << cfg.bloomStrength << "\n"; +``` + +Note: `parse_float` already rejects `nan`/`inf` text in some forms (see `test_config_numeric_bounds`, where `LODBias = nan` counts as handled by `normalize`). If the non-finite test cases fail because the parser rejects the text and keeps the default, that is the same observable result; keep the assertions as written. + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: `cmake --build build-host --target iee_tests && ctest --test-dir build-host --output-on-failure` +Expected: `All InfinityEngine-Enhancer native tests passed`. + +- [ ] **Step 5: Commit** + +```bash +git add src/iee/core/config.h src/iee/core/config.cpp tests/iee_tests.cpp +git commit -m "Config: soft fog of war and bloom settings" +``` + +--- + +### Task 4: Manifest entries and address resolution + +Both patterns were verified offline on 2026-10-07 against the installed Steam 2.7.3 `Baldur.exe` (byte-identical to `ghidra-project/input/Baldur.exe`): each matches exactly once in `.text`, at `0x2A1B60` and `0x42B350`. + +**Files:** +- Modify: `src/iee/game/build_manifest.h` (`PatternSet`, `ReferenceRvas`), `src/iee/game/build_manifest.cpp` (2.7.3 entry and its `static_assert`s), `src/iee/game/game_addrs.h`, `src/iee/game/game_addrs.cpp` +- Test: `tests/iee_tests.cpp` + +**Interfaces:** +- Consumes: `EngineConfig::softFogOfWar`, `bloom`, `enableDebugHotkeys` (Task 3). +- Produces: `PatternSet::renderFog`, `PatternSet::drawFlush`; `ReferenceRvas::renderFog`, `ReferenceRvas::drawFlush`; `GameAddresses::RenderFog`, `GameAddresses::DrawFlush` (both 0 unless both resolved). + +- [ ] **Step 1: Write the failing test** + +Add before `int main()`: + +```cpp +void test_manifest_world_post_targets() { + const auto found = iee::game::find_manifest("BGEE 2.7.3.x"); + expect_true(found.has_value(), "The 2.7.3 manifest should be registered"); + if (!found) return; + const auto& manifest = found->get(); + expect_true(!manifest.patterns.renderFog.empty(), "2.7.3 should carry a RenderFog pattern"); + expect_true(!manifest.patterns.drawFlush.empty(), "2.7.3 should carry a DrawFlush pattern"); + expect_eq(manifest.referenceRvas.renderFog, std::uintptr_t{0x2A1B60}, "RenderFog reference RVA"); + expect_eq(manifest.referenceRvas.drawFlush, std::uintptr_t{0x42B350}, "DrawFlush reference RVA"); + expect_true(iee::game::current_manifest().patterns.renderFog.empty(), + "The 2.6.6 manifest has no world post targets"); +} +``` + +Call it in `main()` after `test_manifest_infgame_offsets();`: + +```cpp + test_manifest_world_post_targets(); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `cmake --build build-host --target iee_tests` +Expected: compile error, `'struct iee::game::PatternSet' has no member named 'renderFog'`. + +- [ ] **Step 3: Extend the manifest types** + +`src/iee/game/build_manifest.h`, at the end of `PatternSet`: + +```cpp + // Optional: CInfinity::RenderFog and the GL backend's queue flush + // (DrawFlush_GL). Hooked/called together for the world post passes (soft + // fog of war, bloom); either empty or non-unique leaves both off. + std::string_view renderFog{}; + std::string_view drawFlush{}; +``` + +At the end of `ReferenceRvas`: + +```cpp + std::uintptr_t renderFog{}; + std::uintptr_t drawFlush{}; +``` + +- [ ] **Step 4: Add the 2.7.3 values** + +`src/iee/game/build_manifest.cpp`, in the `"BGEE 2.7.3.x"` entry. The patterns block is positional; append after the `vidCellFrameAccessor` string (the one ending `"48 8B F2 48 85 C9 75 15 48 89 1A 33 C0",`): + +```cpp + // CInfinity::RenderFog and DrawFlush_GL (PDB-named, each + // offline-verified unique on the Steam 2.7.3.0 binary). + "48 89 5C 24 10 4C 89 44 24 18 55 56 57 41 54 41 55 41 56 41 57 48 83 EC 50 " + "48 8B D9 E8 ? ? ? ? B9 FF FF FF FF E8 ? ? ? ? B9 E1 0D 00 00", + "4C 8B DC 55 41 56 41 57 49 8D 6B D8 48 81 EC 10 01 00 00 48 8B 05 ? ? ? ? " + "48 33 C4 48 89 45 90 45 33 F6 44 39 35 ? ? ? ? 45 8B FE 0F 84", +``` + +Extend the reference RVA list of the same entry from + +```cpp + {0x27EBD0, 0x4257C0, 0x276700, 0x1F27D0, 0x36BA50, 0x36F170, 0x36E820, 0x4118A0, 0x411660, + 0x411780}, +``` + +to + +```cpp + {0x27EBD0, 0x4257C0, 0x276700, 0x1F27D0, 0x36BA50, 0x36F170, 0x36E820, 0x4118A0, 0x411660, + 0x411780, 0x2A1B60, 0x42B350}, +``` + +Add next to the other 2.7.3 `static_assert`s: + +```cpp +static_assert(validate_pattern_format(kKnownBuilds[1].patterns.renderFog) && + validate_pattern_format(kKnownBuilds[1].patterns.drawFlush), + "2.7.3 world post pattern format is invalid"); +``` + +- [ ] **Step 5: Resolve the addresses** + +`src/iee/game/game_addrs.h`, in `GameAddresses` before `bool initialized`: + +```cpp + // Optional, all-or-nothing: the world post passes (soft fog of war, + // bloom) need the fog render to hook and the queue flush to call. + std::uintptr_t RenderFog = 0; + std::uintptr_t DrawFlush = 0; +``` + +`src/iee/game/game_addrs.cpp`: the `resolveOptional` lambda is currently defined inside the `if (cfg.smoothSpriteMovement || cfg.interpolateAnimations)` block. Move its definition to just above that `if` (unchanged body), then add after the closing brace of that block and before `const bool success = ...`: + +```cpp + // Optional targets: world post passes. Resolved only when requested + // (or when debug hotkeys can switch them on), and only as a pair. + if (cfg.softFogOfWar || cfg.bloom || cfg.enableDebugHotkeys) { + out.RenderFog = resolveOptional("CInfinity::RenderFog", manifest.patterns.renderFog, + manifest.referenceRvas.renderFog); + out.DrawFlush = resolveOptional("DrawFlush_GL", manifest.patterns.drawFlush, + manifest.referenceRvas.drawFlush); + if (out.RenderFog && out.DrawFlush) { + LOG_INFO("World post targets resolved at RVA 0x{:X} / 0x{:X}", + out.RenderFog - moduleBase, out.DrawFlush - moduleBase); + } else { + const bool known = + !manifest.patterns.renderFog.empty() && !manifest.patterns.drawFlush.empty(); + out.RenderFog = out.DrawFlush = 0; + if (cfg.softFogOfWar || cfg.bloom) { + LOG_WARN("Soft fog of war and bloom disabled: {}", + known ? "RenderFog/DrawFlush did not resolve uniquely" + : "this build has no RenderFog/DrawFlush targets"); + } + } + } +``` + +- [ ] **Step 6: Run the tests and the Windows compile check** + +Run: `cmake --build build-host --target iee_tests && ctest --test-dir build-host --output-on-failure` +Expected: `All InfinityEngine-Enhancer native tests passed`. + +Run: `cmake -S . -B build-mingw -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake -DIEE_BUILD_WINDOWS_DLL=ON -DIEE_WARNINGS_AS_ERRORS=ON && cmake --build build-mingw` +Expected: builds with no warnings. + +- [ ] **Step 7: Commit** + +```bash +git add src/iee/game/build_manifest.h src/iee/game/build_manifest.cpp src/iee/game/game_addrs.h src/iee/game/game_addrs.cpp tests/iee_tests.cpp +git commit -m "Manifest: CInfinity::RenderFog and DrawFlush_GL for 2.7.3" +``` + +--- + +### Task 5: GL entry points and the pass state guard + +Windows-only code; there is no host test. The deliverable is a clean mingw build and a log line reporting availability. + +**Files:** +- Modify: `src/iee/game/opengl_types.h`, `src/iee/game/opengl_types.cpp` +- Create: `src/iee/core/gl_pass_guard.h` + +**Interfaces:** +- Produces in `iee::game::gl`: the constants and `OpenGLFunctions` members listed below, and `bool OpenGLFunctions::postProcessAvailable`. +- Produces `iee::core::GlPassGuard`: default-constructible RAII type. Construct only when `postProcessAvailable` is true. Saves and restores current program, vertex array binding, array buffer binding, viewport, blend enable and functions, scissor/depth-test/cull-face enables, clear colour. It does **not** manage framebuffer bindings or texture bindings (use the existing `core::GlStateGuard({0})` for unit 0). + +- [ ] **Step 1: Add constants** + +`src/iee/game/opengl_types.h`, after `constexpr unsigned FRAMEBUFFER = 0x8D40;`: + +```cpp +constexpr unsigned READ_FRAMEBUFFER = 0x8CA8; +constexpr unsigned DRAW_FRAMEBUFFER = 0x8CA9; +constexpr unsigned DRAW_FRAMEBUFFER_BINDING = 0x8CA6; +constexpr unsigned READ_FRAMEBUFFER_BINDING = 0x8CAA; +constexpr unsigned COLOR_ATTACHMENT0 = 0x8CE0; +constexpr unsigned FRAMEBUFFER_COMPLETE = 0x8CD5; +constexpr unsigned RGBA16F = 0x881A; +constexpr unsigned HALF_FLOAT = 0x140B; +constexpr unsigned FLOAT = 0x1406; +constexpr unsigned VIEWPORT = 0x0BA2; +constexpr unsigned SCISSOR_TEST = 0x0C11; +constexpr unsigned BLEND = 0x0BE2; +constexpr unsigned DEPTH_TEST = 0x0B71; +constexpr unsigned CULL_FACE = 0x0B44; +constexpr unsigned BLEND_DST_RGB = 0x80C8; +constexpr unsigned BLEND_SRC_RGB = 0x80C9; +constexpr unsigned BLEND_DST_ALPHA = 0x80CA; +constexpr unsigned BLEND_SRC_ALPHA = 0x80CB; +constexpr unsigned CURRENT_PROGRAM = 0x8B8D; +constexpr unsigned VERTEX_ARRAY_BINDING = 0x85B5; +constexpr unsigned ARRAY_BUFFER = 0x8892; +constexpr unsigned ARRAY_BUFFER_BINDING = 0x8894; +constexpr unsigned STATIC_DRAW = 0x88E4; +constexpr unsigned COLOR_CLEAR_VALUE = 0x0C22; +constexpr unsigned COLOR_BUFFER_BIT = 0x4000; +constexpr unsigned TRIANGLES = 0x0004; +constexpr unsigned ZERO = 0; +constexpr unsigned ONE = 1; +constexpr unsigned DST_COLOR = 0x0306; +constexpr unsigned FRAGMENT_SHADER = 0x8B30; +constexpr unsigned VERTEX_SHADER = 0x8B31; +constexpr unsigned COMPILE_STATUS = 0x8B81; +constexpr unsigned LINK_STATUS = 0x8B82; +``` + +- [ ] **Step 2: Add function pointer types and members** + +In `opengl_types.h`, after the existing `using PFN_glBindFramebuffer = ...` line: + +```cpp +using PFN_glViewport = void(APIENTRY*)(int x, int y, int width, int height); +using PFN_glEnable = void(APIENTRY*)(unsigned cap); +using PFN_glDisable = void(APIENTRY*)(unsigned cap); +using PFN_glIsEnabled = unsigned char(APIENTRY*)(unsigned cap); +using PFN_glBlendFunc = void(APIENTRY*)(unsigned sfactor, unsigned dfactor); +using PFN_glClearColor = void(APIENTRY*)(float r, float g, float b, float a); +using PFN_glClear = void(APIENTRY*)(unsigned mask); +using PFN_glDrawArrays = void(APIENTRY*)(unsigned mode, int first, int count); +using PFN_glGetFloatv = void(APIENTRY*)(unsigned pname, float* data); +using PFN_glGenFramebuffers = void(APIENTRY*)(int n, unsigned* framebuffers); +using PFN_glDeleteFramebuffers = void(APIENTRY*)(int n, const unsigned* framebuffers); +using PFN_glFramebufferTexture2D = void(APIENTRY*)(unsigned target, unsigned attachment, + unsigned textarget, unsigned texture, int level); +using PFN_glCheckFramebufferStatus = unsigned(APIENTRY*)(unsigned target); +using PFN_glBlitFramebuffer = void(APIENTRY*)(int srcX0, int srcY0, int srcX1, int srcY1, + int dstX0, int dstY0, int dstX1, int dstY1, + unsigned mask, unsigned filter); +using PFN_glGenVertexArrays = void(APIENTRY*)(int n, unsigned* arrays); +using PFN_glDeleteVertexArrays = void(APIENTRY*)(int n, const unsigned* arrays); +using PFN_glBindVertexArray = void(APIENTRY*)(unsigned array); +using PFN_glGenBuffers = void(APIENTRY*)(int n, unsigned* buffers); +using PFN_glDeleteBuffers = void(APIENTRY*)(int n, const unsigned* buffers); +using PFN_glBindBuffer = void(APIENTRY*)(unsigned target, unsigned buffer); +using PFN_glBufferData = void(APIENTRY*)(unsigned target, std::ptrdiff_t size, const void* data, + unsigned usage); +using PFN_glEnableVertexAttribArray = void(APIENTRY*)(unsigned index); +using PFN_glVertexAttribPointer = void(APIENTRY*)(unsigned index, int size, unsigned type, + unsigned char normalized, int stride, + const void* pointer); +using PFN_glBlendFuncSeparate = void(APIENTRY*)(unsigned srcRgb, unsigned dstRgb, + unsigned srcAlpha, unsigned dstAlpha); +``` + +Add `#include ` at the top of the header. + +In `struct OpenGLFunctions`, after `PFN_glPixelStorei glPixelStorei{};`: + +```cpp + PFN_glViewport glViewport{}; + PFN_glEnable glEnable{}; + PFN_glDisable glDisable{}; + PFN_glIsEnabled glIsEnabled{}; + PFN_glBlendFunc glBlendFunc{}; + PFN_glClearColor glClearColor{}; + PFN_glClear glClear{}; + PFN_glDrawArrays glDrawArrays{}; + PFN_glGetFloatv glGetFloatv{}; +``` + +After `PFN_glBindFramebuffer glBindFramebuffer{};`: + +```cpp + PFN_glGenFramebuffers glGenFramebuffers{}; + PFN_glDeleteFramebuffers glDeleteFramebuffers{}; + PFN_glFramebufferTexture2D glFramebufferTexture2D{}; + PFN_glCheckFramebufferStatus glCheckFramebufferStatus{}; + PFN_glBlitFramebuffer glBlitFramebuffer{}; + PFN_glGenVertexArrays glGenVertexArrays{}; + PFN_glDeleteVertexArrays glDeleteVertexArrays{}; + PFN_glBindVertexArray glBindVertexArray{}; + PFN_glGenBuffers glGenBuffers{}; + PFN_glDeleteBuffers glDeleteBuffers{}; + PFN_glBindBuffer glBindBuffer{}; + PFN_glBufferData glBufferData{}; + PFN_glEnableVertexAttribArray glEnableVertexAttribArray{}; + PFN_glVertexAttribPointer glVertexAttribPointer{}; + PFN_glBlendFuncSeparate glBlendFuncSeparate{}; +``` + +After `bool compressedTextureUploadAvailable{false};`: + +```cpp + // Everything the world post passes need: framebuffer objects, a vertex + // array, shaders with introspection, and the fixed-function state calls. + bool postProcessAvailable{false}; +``` + +- [ ] **Step 3: Load them** + +`src/iee/game/opengl_types.cpp`, in `OpenGLFunctions::initialize`, after the `glPixelStorei` load: + +```cpp + glViewport = reinterpret_cast(get_gl1_proc_address(opengl32, "glViewport")); + glEnable = reinterpret_cast(get_gl1_proc_address(opengl32, "glEnable")); + glDisable = reinterpret_cast(get_gl1_proc_address(opengl32, "glDisable")); + glIsEnabled = reinterpret_cast(get_gl1_proc_address(opengl32, "glIsEnabled")); + glBlendFunc = reinterpret_cast(get_gl1_proc_address(opengl32, "glBlendFunc")); + glClearColor = + reinterpret_cast(get_gl1_proc_address(opengl32, "glClearColor")); + glClear = reinterpret_cast(get_gl1_proc_address(opengl32, "glClear")); + glDrawArrays = + reinterpret_cast(get_gl1_proc_address(opengl32, "glDrawArrays")); + glGetFloatv = reinterpret_cast(get_gl1_proc_address(opengl32, "glGetFloatv")); +``` + +After the `glBindFramebuffer` load: + +```cpp + glGenFramebuffers = reinterpret_cast( + get_ext_proc_address(opengl32, "glGenFramebuffers")); + glDeleteFramebuffers = reinterpret_cast( + get_ext_proc_address(opengl32, "glDeleteFramebuffers")); + glFramebufferTexture2D = reinterpret_cast( + get_ext_proc_address(opengl32, "glFramebufferTexture2D")); + glCheckFramebufferStatus = reinterpret_cast( + get_ext_proc_address(opengl32, "glCheckFramebufferStatus")); + glBlitFramebuffer = reinterpret_cast( + get_ext_proc_address(opengl32, "glBlitFramebuffer")); + glGenVertexArrays = reinterpret_cast( + get_ext_proc_address(opengl32, "glGenVertexArrays")); + glDeleteVertexArrays = reinterpret_cast( + get_ext_proc_address(opengl32, "glDeleteVertexArrays")); + glBindVertexArray = reinterpret_cast( + get_ext_proc_address(opengl32, "glBindVertexArray")); + glGenBuffers = reinterpret_cast(get_ext_proc_address(opengl32, "glGenBuffers")); + glDeleteBuffers = + reinterpret_cast(get_ext_proc_address(opengl32, "glDeleteBuffers")); + glBindBuffer = reinterpret_cast(get_ext_proc_address(opengl32, "glBindBuffer")); + glBufferData = reinterpret_cast(get_ext_proc_address(opengl32, "glBufferData")); + glEnableVertexAttribArray = reinterpret_cast( + get_ext_proc_address(opengl32, "glEnableVertexAttribArray")); + glVertexAttribPointer = reinterpret_cast( + get_ext_proc_address(opengl32, "glVertexAttribPointer")); + glBlendFuncSeparate = reinterpret_cast( + get_ext_proc_address(opengl32, "glBlendFuncSeparate")); +``` + +After the `compressedTextureUploadAvailable = ...` line: + +```cpp + postProcessAvailable = + textureUploadAvailable && readyForSourcePatching && glViewport && glEnable && glDisable && + glIsEnabled && glBlendFunc && glClearColor && glClear && glDrawArrays && glGetFloatv && + glBindFramebuffer && glGenFramebuffers && glDeleteFramebuffers && glFramebufferTexture2D && + glCheckFramebufferStatus && glBlitFramebuffer && glGenVertexArrays && + glDeleteVertexArrays && glBindVertexArray && glGenBuffers && glDeleteBuffers && + glBindBuffer && glBufferData && glEnableVertexAttribArray && glVertexAttribPointer && + glBlendFuncSeparate; +``` + +Extend the existing `LOG_INFO("OpenGL initialized (...` call: add `, post process={}` to the format string and `postProcessAvailable ? "ready" : "partial"` as the last argument. + +- [ ] **Step 4: Write the guard** + +`src/iee/core/gl_pass_guard.h`: + +```cpp +#pragma once + +#include "iee/game/opengl_types.h" + +namespace iee::core { +// Saves the GL state our own draw passes change and puts it back on scope +// exit. The engine issues state changes relative to what it believes the +// hardware state is, so anything left altered corrupts its next draws. +// Framebuffer and texture bindings are not covered: the caller owns the +// framebuffer hand-over, and GlStateGuard covers texture units. +// Requires OpenGLFunctions::postProcessAvailable. +class GlPassGuard { + public: + GlPassGuard() noexcept { + const auto& gl = game::gl::get_gl_functions(); + gl.glGetIntegerv(game::gl::CURRENT_PROGRAM, &program_); + gl.glGetIntegerv(game::gl::VERTEX_ARRAY_BINDING, &vertexArray_); + gl.glGetIntegerv(game::gl::ARRAY_BUFFER_BINDING, &arrayBuffer_); + gl.glGetIntegerv(game::gl::VIEWPORT, viewport_); + gl.glGetIntegerv(game::gl::BLEND_SRC_RGB, &blendSrcRgb_); + gl.glGetIntegerv(game::gl::BLEND_DST_RGB, &blendDstRgb_); + gl.glGetIntegerv(game::gl::BLEND_SRC_ALPHA, &blendSrcAlpha_); + gl.glGetIntegerv(game::gl::BLEND_DST_ALPHA, &blendDstAlpha_); + gl.glGetFloatv(game::gl::COLOR_CLEAR_VALUE, clearColor_); + blend_ = gl.glIsEnabled(game::gl::BLEND) != 0; + scissor_ = gl.glIsEnabled(game::gl::SCISSOR_TEST) != 0; + depthTest_ = gl.glIsEnabled(game::gl::DEPTH_TEST) != 0; + cullFace_ = gl.glIsEnabled(game::gl::CULL_FACE) != 0; + } + + ~GlPassGuard() noexcept { + const auto& gl = game::gl::get_gl_functions(); + const auto set = [&](unsigned capability, bool enabled) { + if (enabled) { + gl.glEnable(capability); + } else { + gl.glDisable(capability); + } + }; + set(game::gl::BLEND, blend_); + set(game::gl::SCISSOR_TEST, scissor_); + set(game::gl::DEPTH_TEST, depthTest_); + set(game::gl::CULL_FACE, cullFace_); + gl.glBlendFuncSeparate( + static_cast(blendSrcRgb_), static_cast(blendDstRgb_), + static_cast(blendSrcAlpha_), static_cast(blendDstAlpha_)); + gl.glClearColor(clearColor_[0], clearColor_[1], clearColor_[2], clearColor_[3]); + gl.glViewport(viewport_[0], viewport_[1], viewport_[2], viewport_[3]); + gl.glBindVertexArray(static_cast(vertexArray_)); + gl.glBindBuffer(game::gl::ARRAY_BUFFER, static_cast(arrayBuffer_)); + gl.glUseProgram(static_cast(program_)); + } + + GlPassGuard(const GlPassGuard&) = delete; + GlPassGuard& operator=(const GlPassGuard&) = delete; + + // The viewport as the engine left it: {x, y, width, height}. + [[nodiscard]] const int* viewport() const noexcept { return viewport_; } + + private: + int program_{}; + int vertexArray_{}; + int arrayBuffer_{}; + int viewport_[4]{}; + int blendSrcRgb_{}; + int blendDstRgb_{}; + int blendSrcAlpha_{}; + int blendDstAlpha_{}; + float clearColor_[4]{}; + bool blend_{}; + bool scissor_{}; + bool depthTest_{}; + bool cullFace_{}; +}; +} // namespace iee::core +``` + +- [ ] **Step 5: Compile check** + +The guard header is not included anywhere yet; add a temporary `#include "iee/core/gl_pass_guard.h"` at the top of `src/iee/game/opengl_types.cpp`, build, then remove it. + +Run: `cmake --build build-mingw` +Expected: builds with no warnings. Remove the temporary include and rebuild. + +- [ ] **Step 6: Commit** + +```bash +git add src/iee/game/opengl_types.h src/iee/game/opengl_types.cpp src/iee/core/gl_pass_guard.h +git commit -m "GL: framebuffer and vertex-array entry points, pass state guard" +``` + +--- + +### Task 6: Fog capture without blur, wired to the hook (first in-game bundle) + +This is spec §8 step 1: the engine's fog goes through our target and comes back unblurred. The picture must be indistinguishable from vanilla. Blur and bloom arrive in Tasks 7 and 8 and reuse everything built here. + +**Files:** +- Create: `src/iee/features/world_post.h`, `src/iee/features/world_post.cpp` +- Modify: `src/iee/hooks.cpp`, `CMakeLists.txt` (DLL source list, after `src/iee/features/tile_render.cpp`) + +**Interfaces:** +- Consumes: `game::Extent`, `game::pixels_per_world_pixel` (Task 2); `EngineConfig` fields (Task 3); `GameAddresses::RenderFog`, `DrawFlush` (Task 4); `gl::OpenGLFunctions::postProcessAvailable`, `core::GlPassGuard` (Task 5); existing `core::GlStateGuard`, `area::read_view_transform`, `area::ViewTransform`. +- Produces (namespace `iee::features`, all `noexcept`, render thread only unless noted): + - `void world_post_configure(const core::EngineConfig& cfg) noexcept;` + - `bool world_post_active() noexcept;` — true when an effect is on and nothing has failed + - `bool world_post_before_fog(float viewWorldWidth) noexcept;` — call right after flushing; returns true when the fog target is now bound and the caller must flush after the original `RenderFog` and then call `world_post_after_fog()` + - `void world_post_after_fog() noexcept;` + - `void world_post_on_area_load() noexcept;` — any thread; re-arms the once-per-area log + - `void world_post_forget() noexcept;` — drops handles without GL calls (shutdown) + +- [ ] **Step 1: Write the header** + +`src/iee/features/world_post.h`: + +```cpp +#pragma once + +namespace iee::core { +struct EngineConfig; +} + +namespace iee::features { +// World post passes: soft fog of war and bloom, run from the +// CInfinity::RenderFog detour. The engine queues the whole frame and only +// draws in DrawFlush_GL, so the caller flushes that queue around these calls: +// +// flush(); // world is now in the framebuffer +// bool capturing = world_post_before_fog(); // bloom; fog target bound if true +// original RenderFog // queues the fog triangles +// if (capturing) { flush(); world_post_after_fog(); } +// +// Render thread only, except world_post_on_area_load. + +void world_post_configure(const core::EngineConfig& cfg) noexcept; +// True when at least one effect is switched on and no pass has failed. +bool world_post_active() noexcept; +// viewWorldWidth: world pixels visible across the view (0 when unknown). +bool world_post_before_fog(float viewWorldWidth) noexcept; +void world_post_after_fog() noexcept; +// Any thread. The next frame logs what engaged. +void world_post_on_area_load() noexcept; +// Drops every GL handle without touching GL. For shutdown paths. +void world_post_forget() noexcept; +} // namespace iee::features +``` + +- [ ] **Step 2: Write the implementation** + +`src/iee/features/world_post.cpp`: + +```cpp +#include "iee/features/world_post.h" + +#include + +#include + +#include "iee/core/config.h" +#include "iee/core/gl_pass_guard.h" +#include "iee/core/gl_state_guard.h" +#include "iee/core/logger.h" +#include "iee/game/opengl_types.h" +#include "iee/game/world_post_plan.h" + +namespace iee::features { +namespace { +namespace gl = game::gl; + +struct Target { + unsigned texture{}; + unsigned framebuffer{}; + game::Extent extent{}; +}; + +struct CompositeProgram { + unsigned id{}; + int scale{-1}; + int dither{-1}; +}; + +struct State { + bool softFog{}; + bool bloom{}; + float fogRadius{}; + float bloomThreshold{}; + float bloomStrength{}; + + bool failed{}; + HGLRC context{}; + game::Extent viewport{}; + int engineDrawFramebuffer{}; + int engineReadFramebuffer{}; + float pixelsPerWorldPixel{1.0f}; + + unsigned vertexArray{}; + unsigned vertexBuffer{}; + CompositeProgram composite{}; + Target fogCapture{}; + bool fogDrawnOnce{}; +}; + +State g_state; +std::atomic g_logNextFrame{true}; + +constexpr const char* kVertexSource = R"glsl(#version 330 +layout(location = 0) in vec2 aPosition; +out vec2 vUv; +void main() { + vUv = aPosition * 0.5 + 0.5; + gl_Position = vec4(aPosition, 0.0, 1.0); +} +)glsl"; + +// Writes the texture scaled by uScale; the caller's blend mode decides whether +// that multiplies (fog) or adds (bloom). uDither adds +-0.5/255 of ordered +// noise to hide banding in long dark gradients. +constexpr const char* kCompositeSource = R"glsl(#version 330 +uniform sampler2D uTexture; +uniform float uScale; +uniform float uDither; +in vec2 vUv; +out vec4 fragColor; +void main() { + vec3 color = texture(uTexture, vUv).rgb * uScale; + float noise = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))); + fragColor = vec4(color + uDither * (noise - 0.5) / 255.0, 1.0); +} +)glsl"; + +void fail(const char* reason) noexcept { + g_state.failed = true; + try { + LOG_WARN("Soft fog of war and bloom disabled for this session: {}", reason); + } catch (...) { + } +} + +unsigned compile_shader(unsigned type, const char* source) noexcept { + const auto& fn = gl::get_gl_functions(); + const unsigned shader = fn.glCreateShader(type); + if (!shader) return 0; + fn.glShaderSource(shader, 1, &source, nullptr); + fn.glCompileShader(shader); + int status = 0; + fn.glGetShaderiv(shader, gl::COMPILE_STATUS, &status); + if (!status) { + char log[512]{}; + fn.glGetShaderInfoLog(shader, static_cast(sizeof(log)) - 1, nullptr, log); + try { + LOG_WARN("World post shader compile failed: {}", log); + } catch (...) { + } + fn.glDeleteShader(shader); + return 0; + } + return shader; +} + +unsigned build_program(const char* fragmentSource) noexcept { + const auto& fn = gl::get_gl_functions(); + const unsigned vertex = compile_shader(gl::VERTEX_SHADER, kVertexSource); + const unsigned fragment = compile_shader(gl::FRAGMENT_SHADER, fragmentSource); + unsigned program = 0; + if (vertex && fragment) { + program = fn.glCreateProgram(); + fn.glAttachShader(program, vertex); + fn.glAttachShader(program, fragment); + fn.glLinkProgram(program); + int status = 0; + fn.glGetProgramiv(program, gl::LINK_STATUS, &status); + if (!status) { + char log[512]{}; + fn.glGetProgramInfoLog(program, static_cast(sizeof(log)) - 1, nullptr, log); + try { + LOG_WARN("World post program link failed: {}", log); + } catch (...) { + } + fn.glDeleteProgram(program); + program = 0; + } + } + if (vertex) fn.glDeleteShader(vertex); + if (fragment) fn.glDeleteShader(fragment); + return program; +} + +void destroy_target(Target& target) noexcept { + const auto& fn = gl::get_gl_functions(); + if (target.framebuffer) fn.glDeleteFramebuffers(1, &target.framebuffer); + if (target.texture) fn.glDeleteTextures(1, &target.texture); + target = {}; +} + +// Leaves the new framebuffer bound; the caller rebinds what it needs. +bool create_target(Target& target, game::Extent extent, unsigned internalFormat, + unsigned type) noexcept { + const auto& fn = gl::get_gl_functions(); + destroy_target(target); + fn.glGenTextures(1, &target.texture); + fn.glBindTexture(gl::TEXTURE_2D, target.texture); + fn.glTexImage2D(gl::TEXTURE_2D, 0, static_cast(internalFormat), extent.width, + extent.height, 0, gl::RGBA, type, nullptr); + fn.glTexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, static_cast(gl::LINEAR)); + fn.glTexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, static_cast(gl::LINEAR)); + fn.glTexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, static_cast(gl::CLAMP_TO_EDGE)); + fn.glTexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, static_cast(gl::CLAMP_TO_EDGE)); + fn.glGenFramebuffers(1, &target.framebuffer); + fn.glBindFramebuffer(gl::FRAMEBUFFER, target.framebuffer); + fn.glFramebufferTexture2D(gl::FRAMEBUFFER, gl::COLOR_ATTACHMENT0, gl::TEXTURE_2D, + target.texture, 0); + if (fn.glCheckFramebufferStatus(gl::FRAMEBUFFER) != gl::FRAMEBUFFER_COMPLETE) { + destroy_target(target); + return false; + } + target.extent = extent; + return true; +} + +void bind_engine_framebuffer() noexcept { + const auto& fn = gl::get_gl_functions(); + fn.glBindFramebuffer(gl::DRAW_FRAMEBUFFER, + static_cast(g_state.engineDrawFramebuffer)); + fn.glBindFramebuffer(gl::READ_FRAMEBUFFER, + static_cast(g_state.engineReadFramebuffer)); +} + +// State every pass of ours draws with. The guard in scope restores it. +void set_pass_state() noexcept { + const auto& fn = gl::get_gl_functions(); + fn.glDisable(gl::SCISSOR_TEST); + fn.glDisable(gl::DEPTH_TEST); + fn.glDisable(gl::CULL_FACE); + fn.glDisable(gl::BLEND); + fn.glBindVertexArray(g_state.vertexArray); + fn.glActiveTexture(gl::TEXTURE0); +} + +void draw_composite(unsigned texture, float scale, float dither) noexcept { + const auto& fn = gl::get_gl_functions(); + fn.glUseProgram(g_state.composite.id); + fn.glUniform1f(g_state.composite.scale, scale); + fn.glUniform1f(g_state.composite.dither, dither); + fn.glBindTexture(gl::TEXTURE_2D, texture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); +} + +bool create_shared_resources() noexcept { + const auto& fn = gl::get_gl_functions(); + static constexpr float kTriangle[] = {-1.0f, -1.0f, 3.0f, -1.0f, -1.0f, 3.0f}; + fn.glGenVertexArrays(1, &g_state.vertexArray); + fn.glGenBuffers(1, &g_state.vertexBuffer); + fn.glBindVertexArray(g_state.vertexArray); + fn.glBindBuffer(gl::ARRAY_BUFFER, g_state.vertexBuffer); + fn.glBufferData(gl::ARRAY_BUFFER, sizeof(kTriangle), kTriangle, gl::STATIC_DRAW); + fn.glEnableVertexAttribArray(0); + fn.glVertexAttribPointer(0, 2, gl::FLOAT, 0, 0, nullptr); + + g_state.composite.id = build_program(kCompositeSource); + if (!g_state.composite.id) return false; + g_state.composite.scale = fn.glGetUniformLocation(g_state.composite.id, "uScale"); + g_state.composite.dither = fn.glGetUniformLocation(g_state.composite.id, "uDither"); + fn.glUseProgram(g_state.composite.id); + fn.glUniform1i(fn.glGetUniformLocation(g_state.composite.id, "uTexture"), 0); + return true; +} + +bool create_sized_resources(game::Extent viewport) noexcept { + if (!create_target(g_state.fogCapture, viewport, gl::RGBA8, gl::UNSIGNED_BYTE)) return false; + g_state.viewport = viewport; + return true; +} + +// Called inside a GlPassGuard + GlStateGuard scope: creation changes bindings. +bool ensure_resources(game::Extent viewport) noexcept { + const auto context = gl::current_context(); + if (context != g_state.context) { + // The old context is gone and took its objects with it. + const State kept = g_state; + g_state = {}; + g_state.softFog = kept.softFog; + g_state.bloom = kept.bloom; + g_state.fogRadius = kept.fogRadius; + g_state.bloomThreshold = kept.bloomThreshold; + g_state.bloomStrength = kept.bloomStrength; + g_state.engineDrawFramebuffer = kept.engineDrawFramebuffer; + g_state.engineReadFramebuffer = kept.engineReadFramebuffer; + g_state.context = context; + gl::discard_errors(); + if (!create_shared_resources() || !gl::check_error("world post shared resources")) { + return false; + } + } + if (!(viewport == g_state.viewport)) { + gl::discard_errors(); + if (!create_sized_resources(viewport) || !gl::check_error("world post targets")) return false; + g_logNextFrame.store(true, std::memory_order_relaxed); + } + return true; +} +} // namespace + +void world_post_configure(const core::EngineConfig& cfg) noexcept { + g_state.softFog = cfg.softFogOfWar; + g_state.bloom = cfg.bloom; + g_state.fogRadius = cfg.softFogRadius; + g_state.bloomThreshold = cfg.bloomThreshold; + g_state.bloomStrength = cfg.bloomStrength; +} + +bool world_post_active() noexcept { + return !g_state.failed && (g_state.softFog || g_state.bloom); +} + +bool world_post_before_fog(float viewWorldWidth) noexcept { + const auto& fn = gl::get_gl_functions(); + if (!fn.postProcessAvailable) { + fail("the GL context lacks framebuffer or vertex-array support"); + return false; + } + fn.glGetIntegerv(gl::DRAW_FRAMEBUFFER_BINDING, &g_state.engineDrawFramebuffer); + fn.glGetIntegerv(gl::READ_FRAMEBUFFER_BINDING, &g_state.engineReadFramebuffer); + + bool capturing = false; + { + core::GlStateGuard textures({0}); + core::GlPassGuard pass; + const int* viewport = pass.viewport(); + if (viewport[0] != 0 || viewport[1] != 0 || viewport[2] <= 0 || viewport[3] <= 0) { + fail("the engine viewport is not a full-window rectangle at the origin"); + } else if (!ensure_resources({viewport[2], viewport[3]})) { + fail("GL resources could not be created"); + } else { + g_state.pixelsPerWorldPixel = + game::pixels_per_world_pixel(static_cast(viewport[2]), viewWorldWidth); + set_pass_state(); + if (g_state.softFog) { + // White = "nothing hidden". The engine's alpha-blended black fog then + // leaves, per pixel, the fraction of the scene that stays visible. + fn.glBindFramebuffer(gl::FRAMEBUFFER, g_state.fogCapture.framebuffer); + fn.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + fn.glClear(gl::COLOR_BUFFER_BIT); + capturing = true; + } + if (g_logNextFrame.exchange(false, std::memory_order_relaxed)) { + try { + LOG_INFO( + "World post: softFog={} (radius {} world px), bloom={}, viewport {}x{}, " + "{:.3f} px per world px, engine framebuffer draw={} read={}", + g_state.softFog, g_state.fogRadius, g_state.bloom, viewport[2], viewport[3], + g_state.pixelsPerWorldPixel, g_state.engineDrawFramebuffer, + g_state.engineReadFramebuffer); + } catch (...) { + } + } + } + bind_engine_framebuffer(); + } + // The guards have restored the engine's state; hand it our target for the + // fog draw only. Framebuffer bindings are not part of the engine's cache. + if (capturing) fn.glBindFramebuffer(gl::FRAMEBUFFER, g_state.fogCapture.framebuffer); + return capturing; +} + +void world_post_after_fog() noexcept { + const auto& fn = gl::get_gl_functions(); + core::GlStateGuard textures({0}); + core::GlPassGuard pass; + bind_engine_framebuffer(); + set_pass_state(); + fn.glViewport(0, 0, g_state.viewport.width, g_state.viewport.height); + // framebuffer = framebuffer * visible fraction + fn.glEnable(gl::BLEND); + fn.glBlendFunc(gl::DST_COLOR, gl::ZERO); + draw_composite(g_state.fogCapture.texture, 1.0f, 0.0f); + if (!g_state.fogDrawnOnce) { + g_state.fogDrawnOnce = true; + if (!gl::check_error("world post fog composite")) { + fail("the fog composite raised a GL error"); + } else { + try { + LOG_INFO("World post: first fog composite drawn"); + } catch (...) { + } + } + } +} + +void world_post_on_area_load() noexcept { g_logNextFrame.store(true, std::memory_order_relaxed); } + +void world_post_forget() noexcept { + const State kept = g_state; + g_state = {}; + g_state.softFog = kept.softFog; + g_state.bloom = kept.bloom; + g_state.fogRadius = kept.fogRadius; + g_state.bloomThreshold = kept.bloomThreshold; + g_state.bloomStrength = kept.bloomStrength; +} +} // namespace iee::features +``` + +Notes for the implementer: +- `fogDrawnOnce` is reset when the context changes (the whole `State` is rebuilt), which is intended. +- `fail()` inside `world_post_before_fog` still falls through to `bind_engine_framebuffer()`, so a failure never leaves our target bound. +- If `fail()` fires inside `world_post_after_fog`, the frame has already been composited; later frames skip the effect. +- If `gl::RGBA` / `gl::RGBA8` / `gl::UNSIGNED_BYTE` constants are declared with different spellings in `opengl_types.h`, use the existing ones. + +Add to `CMakeLists.txt` in the DLL source list after `src/iee/features/tile_render.cpp`: + +```cmake + src/iee/features/world_post.cpp +``` + +- [ ] **Step 3: Wire the hook** + +`src/iee/hooks.cpp`: + +Add the include next to the other feature include: + +```cpp +#include "iee/features/world_post.h" +``` + +After the `g_vidCellFrameSizeHook` declaration: + +```cpp +// CInfinity::RenderFog(CVidMode*, CVisibilityMap*) and the GL backend's +// argument-less queue flush, used together by the world post passes. +using RenderFogFn = void (*)(void*, void*, void*); +using DrawFlushFn = void (*)(); +static core::Hook g_renderFogHook; +static DrawFlushFn g_drawFlush = nullptr; +``` + +Before `detour_static_render`: + +```cpp +// World post passes (soft fog of war, bloom). The engine queues every draw +// and submits the queue in DrawFlush_GL at the end of the frame, so the world +// only exists in the framebuffer once we flush, and the fog only lands in our +// target if we flush again while it is bound. With both effects off this +// detour is a plain pass-through: no flush, the frame is the engine's own. +static void detour_render_fog(void* infinity, void* vidMode, void* visibility) { + const auto original = g_renderFogHook.original(); + if (!g_ctx || !g_drawFlush || !features::world_post_active()) { + original(infinity, vidMode, visibility); + return; + } + float viewWorldWidth = 0.0f; + if (const auto* activeArea = g_ctx->activeArea.load()) { + area::ViewTransform view{}; + if (area::read_view_transform(activeArea, view)) viewWorldWidth = view.viewWorldW; + } + g_drawFlush(); + const bool capturing = features::world_post_before_fog(viewWorldWidth); + original(infinity, vidMode, visibility); + if (capturing) { + g_drawFlush(); + features::world_post_after_fog(); + } +} +``` + +In `install_all`, after the `if (ctx.cfg.smoothSpriteMovement) { ... }` block and before `g_loadAreaHook.enable();`: + +```cpp + if (ctx.addrs.RenderFog && ctx.addrs.DrawFlush) { + try { + features::world_post_configure(ctx.cfg); + g_drawFlush = reinterpret_cast(ctx.addrs.DrawFlush); + g_renderFogHook.create(reinterpret_cast(ctx.addrs.RenderFog), + reinterpret_cast(&detour_render_fog)); + g_renderFogHook.enable(); + LOG_INFO("CInfinity::RenderFog hook installed (soft fog of war={}, bloom={})", + ctx.cfg.softFogOfWar, ctx.cfg.bloom); + } catch (...) { + g_drawFlush = nullptr; + (void)g_renderFogHook.remove(); + LOG_WARN("CInfinity::RenderFog hook failed; fog of war and bloom stay vanilla"); + } + } +``` + +In `uninstall_all`, before `g_animationInterpActive = false;`: + +```cpp + (void)g_renderFogHook.remove(); + g_drawFlush = nullptr; + features::world_post_forget(); +``` + +In `prepare_for_shutdown`, as the first statement: + +```cpp + (void)g_renderFogHook.disable(); +``` + +In `detour_load_area`, next to where `g_spriteMotionReset` is raised: + +```cpp + features::world_post_on_area_load(); +``` + +- [ ] **Step 4: Build** + +Run: `cmake --build build-host --target iee_tests && ctest --test-dir build-host --output-on-failure` +Expected: `All InfinityEngine-Enhancer native tests passed`. + +Run: `cmake --build build-mingw` +Expected: builds with no warnings. + +- [ ] **Step 5: Commit and produce the bundle** + +```bash +git add src/iee/features/world_post.h src/iee/features/world_post.cpp src/iee/hooks.cpp CMakeLists.txt +git commit -m "World post: capture the engine fog through our own target" +git push -u origin feature/world-post +``` + +Open a draft PR against `feature/are-animation-detection` so CI builds the bundle. (Pushes go through the 1Password SSH agent; an unanswered approval prompt shows up as `Permission denied (publickey)`.) + +- [ ] **Step 6: Owner in-game check (blocking)** + +INI: `[Rendering] SoftFogOfWar = true`, `SoftFogRadius = 0`, `Bloom = false`. Compare against `SoftFogOfWar = false` in the same spot. + +Checklist: +1. Log shows `CInfinity::RenderFog hook installed`, one `World post: softFog=true …` line per area, and `first fog composite drawn`. No `disabled for this session` line. +2. Fog of war looks the same as vanilla: same shape, same darkness for unexplored and explored-but-unseen. +3. Characters behind walls are still hidden/dithered correctly; nothing draws over the UI or vanishes behind the world. +4. Rain or snow still draws (AR2600 area in bad weather, or any outdoor area). +5. A fully explored interior shows no darkening at all. +6. A screenshot (the game's own key) looks like the screen. +7. Resize the window or change resolution: fog still lines up. +8. Save and load, change area: no stale fog, log re-arms. + +If item 3 fails, stop: the offline depth finding in Task 1 is contradicted, so report the symptom before going further. If any other item fails, fix before Task 7. + +--- + +### Task 7: Soft fog (blur chain) + +**Files:** +- Modify: `src/iee/features/world_post.cpp` + +**Interfaces:** +- Consumes: `game::BlurPlan`, `blur_plan_for_radius`, `half_extent`, `available_blur_levels`, `kMaxBlurLevels` (Task 2); everything from Task 6. +- Produces (file-local, reused by Task 8): + - `struct BlurChain { std::array levels{}; int count{}; };` + - `bool create_chain(BlurChain& chain, game::Extent base) noexcept;` — level `i` is `base` halved `i + 1` times, `RGBA16F` + - `void destroy_chain(BlurChain& chain) noexcept;` + - `void run_blur(const Target& source, BlurChain& chain, game::BlurPlan plan, bool accumulate) noexcept;` — result in `chain.levels[0]`; leaves an arbitrary framebuffer bound; requires pass state set + +- [ ] **Step 1: Add the blur programs and chain** + +In `world_post.cpp`, add `#include ` and, after `kCompositeSource`: + +```cpp +// Dual-filter blur (down then up through half-size levels). uStep is the +// sample spread in source UV units. +constexpr const char* kBlurDownSource = R"glsl(#version 330 +uniform sampler2D uTexture; +uniform vec2 uStep; +in vec2 vUv; +out vec4 fragColor; +void main() { + vec4 sum = texture(uTexture, vUv) * 4.0; + sum += texture(uTexture, vUv - uStep); + sum += texture(uTexture, vUv + uStep); + sum += texture(uTexture, vUv + vec2(uStep.x, -uStep.y)); + sum += texture(uTexture, vUv - vec2(uStep.x, -uStep.y)); + fragColor = sum / 8.0; +} +)glsl"; + +constexpr const char* kBlurUpSource = R"glsl(#version 330 +uniform sampler2D uTexture; +uniform vec2 uStep; +in vec2 vUv; +out vec4 fragColor; +void main() { + vec4 sum = texture(uTexture, vUv + vec2(-uStep.x * 2.0, 0.0)); + sum += texture(uTexture, vUv + vec2(-uStep.x, uStep.y)) * 2.0; + sum += texture(uTexture, vUv + vec2(0.0, uStep.y * 2.0)); + sum += texture(uTexture, vUv + vec2(uStep.x, uStep.y)) * 2.0; + sum += texture(uTexture, vUv + vec2(uStep.x * 2.0, 0.0)); + sum += texture(uTexture, vUv + vec2(uStep.x, -uStep.y)) * 2.0; + sum += texture(uTexture, vUv + vec2(0.0, -uStep.y * 2.0)); + sum += texture(uTexture, vUv + vec2(-uStep.x, -uStep.y)) * 2.0; + fragColor = sum / 12.0; +} +)glsl"; +``` + +After `struct CompositeProgram`: + +```cpp +struct BlurProgram { + unsigned id{}; + int step{-1}; +}; + +struct BlurChain { + std::array levels{}; + int count{}; +}; +``` + +Add to `State`, after `Target fogCapture{};`: + +```cpp + BlurProgram blurDown{}; + BlurProgram blurUp{}; + BlurChain fogChain{}; +``` + +After `create_target`: + +```cpp +void destroy_chain(BlurChain& chain) noexcept { + for (auto& level : chain.levels) destroy_target(level); + chain.count = 0; +} + +bool create_chain(BlurChain& chain, game::Extent base) noexcept { + destroy_chain(chain); + const int count = game::available_blur_levels(base); + game::Extent extent = base; + for (int index = 0; index < count; ++index) { + extent = game::half_extent(extent); + if (!create_target(chain.levels[static_cast(index)], extent, gl::RGBA16F, + gl::HALF_FLOAT)) { + destroy_chain(chain); + return false; + } + } + chain.count = count; + return true; +} + +void blur_pass(const BlurProgram& program, const Target& source, const Target& destination, + float stepTexels) noexcept { + const auto& fn = gl::get_gl_functions(); + fn.glBindFramebuffer(gl::FRAMEBUFFER, destination.framebuffer); + fn.glViewport(0, 0, destination.extent.width, destination.extent.height); + fn.glUseProgram(program.id); + fn.glUniform2f(program.step, stepTexels / static_cast(source.extent.width), + stepTexels / static_cast(source.extent.height)); + fn.glBindTexture(gl::TEXTURE_2D, source.texture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); +} + +// Blurs `source` into chain.levels[0]. With `accumulate`, each level keeps its +// own downsampled content and the wider blur is added on top (bloom); without +// it the result is a plain blur (fog). Requires set_pass_state(). +void run_blur(const Target& source, BlurChain& chain, game::BlurPlan plan, + bool accumulate) noexcept { + const auto& fn = gl::get_gl_functions(); + const int levels = plan.levels < chain.count ? plan.levels : chain.count; + if (levels <= 0) return; + fn.glDisable(gl::BLEND); + const Target* from = &source; + for (int index = 0; index < levels; ++index) { + const auto& to = chain.levels[static_cast(index)]; + blur_pass(g_state.blurDown, *from, to, plan.offset); + from = &to; + } + if (accumulate) { + fn.glEnable(gl::BLEND); + fn.glBlendFunc(gl::ONE, gl::ONE); + } + for (int index = levels - 1; index > 0; --index) { + blur_pass(g_state.blurUp, chain.levels[static_cast(index)], + chain.levels[static_cast(index - 1)], plan.offset * 0.5f); + } + fn.glDisable(gl::BLEND); +} +``` + +In `create_shared_resources`, before `return true;`: + +```cpp + const auto build_blur = [&](BlurProgram& program, const char* source) { + program.id = build_program(source); + if (!program.id) return false; + program.step = fn.glGetUniformLocation(program.id, "uStep"); + fn.glUseProgram(program.id); + fn.glUniform1i(fn.glGetUniformLocation(program.id, "uTexture"), 0); + return true; + }; + if (!build_blur(g_state.blurDown, kBlurDownSource)) return false; + if (!build_blur(g_state.blurUp, kBlurUpSource)) return false; +``` + +In `create_sized_resources`, after the fog capture is created: + +```cpp + if (!create_chain(g_state.fogChain, viewport)) return false; +``` + +- [ ] **Step 2: Use the blur in the fog composite** + +Replace the body of `world_post_after_fog` from `bind_engine_framebuffer();` through the `draw_composite(...)` call with: + +```cpp + set_pass_state(); + const auto plan = game::blur_plan_for_radius(g_state.fogRadius * g_state.pixelsPerWorldPixel, + g_state.viewport); + const bool blurred = plan.levels > 0 && g_state.fogChain.count > 0; + if (blurred) run_blur(g_state.fogCapture, g_state.fogChain, plan, false); + bind_engine_framebuffer(); + fn.glViewport(0, 0, g_state.viewport.width, g_state.viewport.height); + // framebuffer = framebuffer * visible fraction + fn.glEnable(gl::BLEND); + fn.glBlendFunc(gl::DST_COLOR, gl::ZERO); + draw_composite(blurred ? g_state.fogChain.levels[0].texture : g_state.fogCapture.texture, 1.0f, + blurred ? 1.0f : 0.0f); +``` + +Extend the once-per-area `LOG_INFO` in `world_post_before_fog` so the plan is visible: compute `const auto plan = game::blur_plan_for_radius(g_state.fogRadius * g_state.pixelsPerWorldPixel, {viewport[2], viewport[3]});` just before the log and append `, fog blur levels={} offset={:.2f}` with `plan.levels, plan.offset`. + +- [ ] **Step 3: Build** + +Run: `cmake --build build-host --target iee_tests && ctest --test-dir build-host --output-on-failure && cmake --build build-mingw` +Expected: tests pass; mingw builds with no warnings. + +- [ ] **Step 4: Commit and push** + +```bash +git add src/iee/features/world_post.cpp +git commit -m "World post: soft fog of war edge" +git push +``` + +- [ ] **Step 5: Owner in-game check (blocking)** + +INI: `SoftFogOfWar = true`, `SoftFogRadius = 24`. + +1. The fog edge is a smooth gradient with no diamond or stair-step shapes. +2. Zoom fully in and fully out, changing area or reloading between them so the log line prints again: the logged `px per world px` differs between the two, and the edge softness looks proportionate at both. (If the value does not change with zoom, the view transform is stale at this point of the frame; read the zoom from `CInfinity::m_fZoom` at `+0x484` on the hook's `infinity` argument instead, via a manifest offset.) +3. No banding in the dark gradient; no light leaking in from unexplored areas beyond a few pixels. +4. `SoftFogRadius = 0` still matches vanilla. +5. Try 12 and 48 and report the preferred value; update the default in `config.h`, the Task 3 test and the spec if it is not 24. + +--- + +### Task 8: Bloom + +**Files:** +- Modify: `src/iee/features/world_post.cpp` + +**Interfaces:** +- Consumes: `BlurChain`, `create_chain`, `destroy_chain`, `run_blur`, `draw_composite`, `set_pass_state`, `bind_engine_framebuffer` (Tasks 6–7); `game::bloom_levels`, `game::half_extent` (Task 2). +- Produces: bloom drawn inside `world_post_before_fog`; no interface change for callers. + +- [ ] **Step 1: Add the bright-pass program and bloom targets** + +After `kBlurUpSource`: + +```cpp +// Keeps what is brighter than uThreshold, with a soft knee so the cut-off +// does not flicker. +constexpr const char* kBrightSource = R"glsl(#version 330 +uniform sampler2D uTexture; +uniform float uThreshold; +in vec2 vUv; +out vec4 fragColor; +void main() { + vec3 color = texture(uTexture, vUv).rgb; + float level = max(color.r, max(color.g, color.b)); + const float knee = 0.1; + float soft = clamp(level - uThreshold + knee, 0.0, 2.0 * knee); + soft = soft * soft / (4.0 * knee); + float weight = max(soft, level - uThreshold) / max(level, 1e-4); + fragColor = vec4(color * weight, 1.0); +} +)glsl"; +``` + +After `struct BlurProgram`: + +```cpp +struct BrightProgram { + unsigned id{}; + int threshold{-1}; +}; +``` + +Add to `State`, after `BlurChain fogChain{};`: + +```cpp + BrightProgram bright{}; + Target bloomScene{}; // half-size copy of the world + Target bloomBright{}; // its bright parts + BlurChain bloomChain{}; + bool bloomDrawnOnce{}; +``` + +In `create_shared_resources`, before `return true;`: + +```cpp + g_state.bright.id = build_program(kBrightSource); + if (!g_state.bright.id) return false; + g_state.bright.threshold = fn.glGetUniformLocation(g_state.bright.id, "uThreshold"); + fn.glUseProgram(g_state.bright.id); + fn.glUniform1i(fn.glGetUniformLocation(g_state.bright.id, "uTexture"), 0); +``` + +In `create_sized_resources`, after the fog chain: + +```cpp + const auto half = game::half_extent(viewport); + if (!create_target(g_state.bloomScene, half, gl::RGBA16F, gl::HALF_FLOAT)) return false; + if (!create_target(g_state.bloomBright, half, gl::RGBA16F, gl::HALF_FLOAT)) return false; + if (!create_chain(g_state.bloomChain, half)) return false; +``` + +- [ ] **Step 2: Add the bloom pass** + +After `run_blur`: + +```cpp +// Adds a glow of the bright parts of the world image to the engine's +// framebuffer. Runs before the fog so unexplored areas stay dark, and before +// the UI is drawn so the UI cannot glow. Requires set_pass_state(). +void draw_bloom() noexcept { + const auto& fn = gl::get_gl_functions(); + const auto half = g_state.bloomScene.extent; + const int levels = game::bloom_levels(half); + if (levels <= 0 || !(g_state.bloomStrength > 0.0f)) return; + + fn.glBindFramebuffer(gl::READ_FRAMEBUFFER, + static_cast(g_state.engineDrawFramebuffer)); + fn.glBindFramebuffer(gl::DRAW_FRAMEBUFFER, g_state.bloomScene.framebuffer); + fn.glBlitFramebuffer(0, 0, g_state.viewport.width, g_state.viewport.height, 0, 0, half.width, + half.height, gl::COLOR_BUFFER_BIT, gl::LINEAR); + + fn.glBindFramebuffer(gl::FRAMEBUFFER, g_state.bloomBright.framebuffer); + fn.glViewport(0, 0, half.width, half.height); + fn.glUseProgram(g_state.bright.id); + fn.glUniform1f(g_state.bright.threshold, g_state.bloomThreshold); + fn.glBindTexture(gl::TEXTURE_2D, g_state.bloomScene.texture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); + + run_blur(g_state.bloomBright, g_state.bloomChain, {levels, 1.0f}, true); + + bind_engine_framebuffer(); + fn.glViewport(0, 0, g_state.viewport.width, g_state.viewport.height); + fn.glEnable(gl::BLEND); + fn.glBlendFunc(gl::ONE, gl::ONE); + // Each level adds its share on the way up; normalise so strength means the + // same thing at every resolution. + draw_composite(g_state.bloomChain.levels[0].texture, + g_state.bloomStrength / static_cast(levels), 0.0f); + fn.glDisable(gl::BLEND); + + if (!g_state.bloomDrawnOnce) { + g_state.bloomDrawnOnce = true; + if (!gl::check_error("world post bloom")) { + fail("the bloom pass raised a GL error"); + } else { + try { + LOG_INFO("World post: first bloom drawn ({} levels)", levels); + } catch (...) { + } + } + } +} +``` + +In `world_post_before_fog`, right after `set_pass_state();` and before the `if (g_state.softFog)` block: + +```cpp + if (g_state.bloom) { + draw_bloom(); + set_pass_state(); + } +``` + +(`set_pass_state()` is repeated because `draw_bloom` leaves blending and bindings in its own state; the fog clear that follows needs the baseline.) + +A multisampled default framebuffer makes the scaled `glBlitFramebuffer` raise `GL_INVALID_OPERATION`; the first-frame error check then latches the feature off with a log line, which is the intended fail-closed behaviour. + +- [ ] **Step 3: Build** + +Run: `cmake --build build-host --target iee_tests && ctest --test-dir build-host --output-on-failure && cmake --build build-mingw` +Expected: tests pass; mingw builds with no warnings. + +- [ ] **Step 4: Commit and push** + +```bash +git add src/iee/features/world_post.cpp +git commit -m "World post: threshold bloom on the world image" +git push +``` + +- [ ] **Step 5: Owner in-game check (blocking)** + +INI: `Bloom = true`, defaults otherwise; test with `SoftFogOfWar` both on and off. + +1. Log shows `first bloom drawn`; no `disabled for this session`. +2. Fires, torches, spell effects and lit windows glow. +3. UI panels, text, portraits and the cursor do not glow. +4. Unexplored (black) areas show no glow leaking over them. +5. A bright daytime outdoor map and a snow map are not hazy at the defaults. +6. With `Bloom = true` and `SoftFogOfWar = false` the fog looks vanilla (this is the bloom-only path: one flush, engine fog untouched). +7. Tune `BloomThreshold` (try 0.7 and 0.9) and `BloomStrength` (try 0.2 and 0.6); report preferred values and update the defaults in `config.h`, the Task 3 test and the spec if they differ. + +--- + +### Task 9: Runtime toggle and documentation + +**Files:** +- Modify: `src/iee/features/world_post.h`, `src/iee/features/world_post.cpp`, `src/iee/hooks.cpp` +- Modify: `docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md` (items D1/D2, §3 order), `docs/superpowers/specs/2026-06-10-graphics-enhancement-roadmap-design.md` (note on phases 4–5), `CLAUDE.md` (Runtime Facts — Renderer / GL), `../handoff.md` + +**Interfaces:** +- Produces: `void features::world_post_poll_hotkeys() noexcept;` — render thread; F8 toggles soft fog, F9 toggles bloom. + +- [ ] **Step 1: Add the hotkey poll** + +`world_post.h`, after `world_post_active`: + +```cpp +// Debug A/B: F8 toggles soft fog of war, F9 toggles bloom. Call once per +// frame, only when debug hotkeys are enabled. +void world_post_poll_hotkeys() noexcept; +``` + +`world_post.cpp`, after `world_post_active`: + +```cpp +void world_post_poll_hotkeys() noexcept { + static bool fogKeyWasDown = false; + static bool bloomKeyWasDown = false; + const bool fogKeyDown = (GetAsyncKeyState(VK_F8) & 0x8000) != 0; + const bool bloomKeyDown = (GetAsyncKeyState(VK_F9) & 0x8000) != 0; + const bool fogPressed = fogKeyDown && !fogKeyWasDown; + const bool bloomPressed = bloomKeyDown && !bloomKeyWasDown; + fogKeyWasDown = fogKeyDown; + bloomKeyWasDown = bloomKeyDown; + if (!fogPressed && !bloomPressed) return; + if (fogPressed) g_state.softFog = !g_state.softFog; + if (bloomPressed) g_state.bloom = !g_state.bloom; + try { + LOG_INFO("Hotkey: soft fog of war={}, bloom={}", g_state.softFog, g_state.bloom); + } catch (...) { + } +} +``` + +`hooks.cpp`, as the first statements of `detour_render_fog` after `const auto original = ...;`: + +```cpp + if (g_ctx && g_ctx->cfg.enableDebugHotkeys) features::world_post_poll_hotkeys(); +``` + +- [ ] **Step 2: Build** + +Run: `cmake --build build-host --target iee_tests && ctest --test-dir build-host --output-on-failure && cmake --build build-mingw` +Expected: tests pass; mingw builds with no warnings. + +- [ ] **Step 3: Update the documents** + +- `CLAUDE.md`, under "Runtime Facts — Renderer / GL", add: + +```markdown +- The engine queues every draw (`DrawEnd_GL` only appends to `gl.cmds`) and submits the whole frame in `DrawFlush_GL` (2.7.3 RVA `0x42B350`), normally once at flip. Binding a framebuffer around an engine render function captures nothing, and an immediate GL draw of ours lands under the still-queued world. Flush first (see `features/world_post.*`), and restore every GL state touched: the flush issues changes relative to the engine's cached `gl.hwState`. +- `gl.pp.enabled` is forced false in 2.7.3 (`DrawInit_GL`): the engine's own offscreen target and `fpCatRom` are dead. +``` + +- `2026-10-03-graphics-uplift-roadmap-design.md`: in item D, mark D1 and D2 implemented behind `[Rendering] SoftFogOfWar` / `Bloom` with a pointer to the 2026-10-07 spec; in §3, move D1/D2 to "Done" and note that F's pack work is paused by owner decision 2026-10-07. +- `2026-06-10-graphics-enhancement-roadmap-design.md`: at the top of "Phase 4 — Fog" add one line: "Superseded by `2026-10-07-soft-fog-and-bloom-design.md`: the engine queues draws, so an FBO bracket around `RenderFog` captures nothing." +- `../handoff.md`: update the date, the State table (soft fog and bloom rows with their in-game status; animation in-betweens pack marked paused), replace "Phase 1" as next step with effect-cast light, and add the queue/flush fact to Working notes. + +- [ ] **Step 4: Commit and push** + +```bash +git add src/iee/features/world_post.h src/iee/features/world_post.cpp src/iee/hooks.cpp CLAUDE.md docs/superpowers/specs +git commit -m "World post: debug A/B hotkeys; record the engine draw queue in the docs" +git push +``` + +(`../handoff.md` lives outside the repository; edit it in place.) + +- [ ] **Step 5: Final verification** + +Run: `cmake --build build-host --target iee_tests && ctest --test-dir build-host --output-on-failure && cmake --build build-mingw` +Expected: tests pass; mingw builds with no warnings. Confirm CI is green on the PR, and that with both settings `false` and hotkeys off the log shows no `RenderFog hook installed` line (addresses are not even resolved). diff --git a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md new file mode 100644 index 0000000..cc3b3c8 --- /dev/null +++ b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md @@ -0,0 +1,246 @@ +# Soft Fog of War and Bloom — Design + +Date: 2026-10-07. Roadmap items D1 and D2 of +`2026-10-03-graphics-uplift-roadmap-design.md`. Supersedes the fog and bloom +phases (4 and 5) of `2026-06-10-graphics-enhancement-roadmap-design.md`, whose +bracket design rests on a premise this document disproves (§2.1). + +## 1. Goal + +Two world-view effects with a visible result on every map: + +- **Soft fog of war.** The engine's fog edge is built from 32 px linear + gradient pieces and reads as diamonds and stair-steps. Replace it with a + smooth edge of the same extent and darkness. +- **Bloom.** Bright parts of the world (fires, spell effects, lit windows, + lava) get a soft glow. + +Owner decisions (2026-10-07): + +- Bloom is threshold-based now, tunable. A light-source contribution is added + later, with effect-cast light (roadmap C3), not in this work. +- Animation in-betweening (old phase 1) is paused; this work comes first. + +Constraints carried over: INI-gated and off by default, UI untouched, 2.7.3 +only, every address through `build_manifest`, any failure leaves the game +drawing exactly as vanilla. + +Done means: the owner confirms both effects in game under Proton, CI is green, +and with both settings off the frame is the engine's own. + +## 2. Evidence (BGEE 2.7.3, decompiled 2026-10-07) + +### 2.1 The engine queues the whole frame + +`DrawEnd_GL` (`0x42B220`) does not draw. It appends a command (state word, +first vertex, count) to `gl.cmds`. The GPU sees nothing until `DrawFlush_GL` +(`0x42B350`, static, no arguments) walks the queue, applies state changes +against its cached `gl.hwState`, and issues `glDrawArrays`. + +`DrawFlush_GL` is called from `DrawFlip_GL` (once per frame, before +`SDL_GL_SwapWindow`), from `DrawReadPixels_GL` and `DrawLockSurface_GL`, and +from `DrawBeginScaled_GL` / `DrawEndScaled_GL` only when `gl.pp.enabled`. + +Consequences: + +- Binding a framebuffer around `CInfinity::RenderFog` captures nothing. The + 2026-06-10 fog design ("bind an FBO, call the original, blur") cannot work + as written. +- Any GL draw of ours issued during the world pass lands **under** the + still-queued world, unless the queue is flushed first. +- A mid-frame flush is an operation the engine performs itself (screenshots, + surface locks), so calling it at a chosen point has precedent. + +### 2.2 Flush side effects + +After the draws, `DrawFlush_GL` sets `gl.hwState` to the last command's +state, clears `gl.force`, resets the vertex cursor, and rewrites `gl.v.z` and +`gl.depthLockState`. Checked against a full disassembly (2026-10-07): + +- `gl.v.z` (`0x142F73F7C`) is written only by `DrawInit_GL` and + `DrawFlush_GL`, always to the same value, and read only by `DrawVertex_GL`. + It never changes during a session. +- `gl.depthLockState` (`0x142F74048`) is written by the same two functions + and never read. +- `DrawLockSurface_GL` (`0x42CAF0`) calls `DrawFlush_GL` whenever the + engine's 1024x1024 sprite atlas fills, so flushes in the middle of the world + pass already occur in vanilla on busy scenes. + +An extra flush between the world and the fog therefore changes no state that +later draws depend on, provided our own GL state is restored (§5). + +### 2.3 Engine post-processing is off in this build + +`DrawInit_GL` reads `Graphics/Postprocessing` and then sets +`gl.pp.enabled = false` unconditionally. The engine's own offscreen target +(`gl.pp.fb`, blitted with `fpCatRom`) is therefore never used in 2.7.3, which +matches gate V5 (no engine framebuffer binds) and V3 (`fpCatRom` dead). Our +code still saves and restores whatever framebuffer is bound rather than +assuming 0. + +### 2.4 Fog draw + +`CInfinity::RenderFog` (`0x2A1B60`) is the last world step before +`CInfinity::PostRender` (weather) in `CGameArea::Render` (`0x189360`). It +pushes draw state, disables texturing, sets `SRC_ALPHA / ONE_MINUS_SRC_ALPHA`, +and emits triangles for every visible 64 px tile through +`CVisibilityMap::BltFogOWar3d` (never to be detoured) inside one +`DrawBegin`/`DrawEnd`. The fog colour is `0x030103` after fade and +brightness, i.e. near black; only alpha varies. `RenderFog` is outside the +`DrawBeginSort`/`DrawEndSort` range, which is confined to `CInfinity::Render`. + +## 3. Approach + +One hook on `CInfinity::RenderFog`, plus the address of `DrawFlush_GL`: + +``` +detour RenderFog: + flush # world so far is now in the framebuffer + if bloom: copy world -> glow chain -> add glow to framebuffer + if fog: bind fog target, clear white + original RenderFog + flush # fog triangles land in the fog target + rebind engine framebuffer + blur fog target -> multiply framebuffer by it + else: original RenderFog + restore GL state +``` + +Weather and the UI are queued afterwards and drawn at flip, on top, as today. +Bloom runs before fog so unexplored areas stay dark. + +### 3.1 Why white-clear and multiply + +Drawing alpha-blended black over a white target leaves, per pixel, the +product of `(1 - alpha)` over every fog triangle covering it: exactly the +fraction of the scene the engine's fog would let through, correct for +overlapping triangles too. Multiplying the framebuffer by that value +reproduces the engine's result (the fog colour's own contribution, at most +3/255, is dropped). With blur radius 0 the output must match vanilla to +within that error; this is the acceptance check for the capture path. + +### 3.2 Alternatives considered + +| Alternative | Why not now | +|---|---| +| Skip the original `RenderFog`; build a small visibility texture from the tile codes and draw one smooth-upsampled pass | No second flush and full control of the falloff, but it re-implements the engine's fog rules (explored vs unexplored shades, corner codes, fade, the fog option) from `CVisibilityMap` internals and needs several more manifest entries. Worth revisiting if the blurred capture looks wrong at the edges | +| Redirect the whole world pass into our own framebuffer and blit at the end | Gives a clean world-only image but moves every engine draw off the default framebuffer; far larger blast radius for the same result | +| Do the bloom at flip time | The UI is already in the image by then and would glow | +| Capture into a transparent target | Blending onto an empty target squares alpha and compounds on overlap; not recoverable in general | + +## 4. Components + +| Unit | Responsibility | Depends on | +|---|---|---| +| `game/world_post_plan.*` | Pure sizing and parameter logic: target sizes, glow chain levels, fog blur radius in pixels from world radius and zoom, clamping of INI values. Host-tested | nothing | +| `core/gl_pass_guard.h` | RAII save/restore of every GL state our passes touch (§5) | GL function table | +| `features/world_post.*` | Owns GL resources (targets, programs, quad). `begin_world_post()` / `end_fog_capture()` style entry points called by the hook. Lazy creation, resize on viewport change, drop on context change, one-shot failure latch | plan, guard, GL table | +| `hooks.cpp` | `detour_render_fog`, install/uninstall, gating | manifest, config | +| `game/build_manifest.*` | Patterns and reference RVAs for `CInfinity::RenderFog` and `DrawFlush_GL`, each verified unique offline | — | +| `game/opengl_types.*` | Added entries: framebuffer objects, `glBlitFramebuffer`, vertex array and buffer objects, and the fixed-function calls the guard needs | — | +| `core/config.*` | New INI keys (§6) | — | + +Shaders are short GLSL sources embedded in `world_post.cpp` (our own +programs, not engine overrides): copy/downsample, bright-pass, blur +up/down, composite. + +### 4.1 Bloom pass + +1. `glBlitFramebuffer` the framebuffer to a half-resolution RGBA16F target + (linear filter). +2. Bright-pass with a soft knee around `BloomThreshold`. +3. Dual-filter blur: downsample through a fixed number of levels (count from + the plan, capped by target size), then upsample back, accumulating. +4. Add the result to the framebuffer (`ONE, ONE`) scaled by `BloomStrength`. + +At the hook point the framebuffer holds only world content for this frame, so +no world rectangle is needed and the UI cannot glow. + +### 4.2 Fog pass + +1. Full-size RGBA8 fog target, cleared white; original `RenderFog`; flush. +2. Downsample to quarter size, blur with the same dual-filter code; radius + from `SoftFogRadius` (world pixels) times the current zoom, taken from the + view transform the mod already publishes (`area_state.h`). +3. Multiply the framebuffer by the blurred result (`DST_COLOR, ZERO`), with a + small ordered dither to hide 8-bit banding in the long dark gradient. + +Radius 0 skips step 2 and composites the capture directly. + +## 5. GL state contract + +The engine believes the hardware state equals `gl.hwState` after a flush and +only issues changes relative to it. Everything our passes change must be put +back exactly. `GlPassGuard` saves and restores: draw and read framebuffer, +viewport, scissor box and enable, blend enable and function, depth test and +depth mask, colour mask, cull face, current program, active texture and +client active texture, the 2D binding and enable on units 0 and 1, vertex +array object and array buffer binding, clear colour. + +The engine's texture-environment setup on unit 1 is not touched (we only use +our own programs and rebind the saved textures). + +All of this runs on the render thread inside an engine render call, the same +thread and context as the existing draw-time hooks. + +## 6. Configuration + +`[Rendering]`, all off or neutral by default: + +| Key | Default | Meaning | +|---|---|---| +| `SoftFogOfWar` | `false` | Enable the fog capture and soft composite | +| `SoftFogRadius` | `24` | Blur radius in world pixels; `0` = capture without blur (must match vanilla) | +| `Bloom` | `false` | Enable bloom | +| `BloomThreshold` | `0.80` | Brightness above which pixels glow (0..1) | +| `BloomStrength` | `0.35` | Glow intensity | + +With `EnableDebugHotkeys` on, one key per effect toggles it at runtime for +A/B comparison, using the existing hotkey mechanism. + +## 7. Failure handling and logging + +- Missing or non-unique pattern for either address: both features off, one + log line naming which. +- Missing GL entry point, incomplete framebuffer, shader compile or link + error: log the reason once, latch the feature off for the session, call the + original `RenderFog` untouched. +- Context change: drop all GL objects and recreate on next use. +- Field log, once per area load: which effects engaged, target sizes, zoom, + the framebuffer found bound at the hook, and one line the first time each + pass draws. Verbose logging adds per-frame counts. + +## 8. Build order and validation + +Each step is a CI bundle the owner can check in game. + +1. **Flush and capture, no blur.** Hook, manifest entries, guard, fog capture + with `SoftFogRadius = 0`. Check: looks identical to `SoftFogOfWar = false`, + including sprite/wall occlusion, weather, UI and screenshots. This + validates the capture path and the state restore of §5. +2. **Soft fog.** Blur and dither. Check: edge smoothness at several zoom + levels, no light leak at explored boundaries, radius tuning by eye. +3. **Bloom.** Check: fires and spells glow, UI does not, bright daytime maps + are not hazy at defaults; tune threshold and strength by eye. + +Host tests cover `world_post_plan` (sizes, level counts, radius scaling, +clamping) and INI parsing. GL behaviour is validated in game only. + +## 9. Known limits and risks + +- **GL state restore** (§5): a missed piece of state corrupts the engine's next draws; step 1 of §8 is the check. +- Selection circles, health bars and other bright world-space markers are + part of the world image and will glow slightly. The later light-source + path is the fix. +- Weather (rain, snow, lightning flash) is drawn after the hook and does not + glow. +- The soft edge reveals a few pixels more at the visible boundary and darkens + a few pixels inside it; the radius controls both. +- Threshold bloom on painted maps can haze bright scenes; defaults are + conservative and the owner tunes by eye. +- 2.7.3 only. Other builds leave both features off. + +## 10. Out of scope + +Light-source bloom, effect-cast light, normal-derived relief, FXAA, water +reflections, changes to the engine's fog rules or colours. From 180d62bf09c2ed2114536f11df84c3cafda6b4b6 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Wed, 7 Oct 2026 21:07:04 +0200 Subject: [PATCH 34/57] World post: sizing logic for blur chains and zoom scale --- CMakeLists.txt | 1 + src/iee/game/world_post_plan.cpp | 47 ++++++++++++++++++++++++++++++++ src/iee/game/world_post_plan.h | 30 ++++++++++++++++++++ tests/iee_tests.cpp | 46 +++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 src/iee/game/world_post_plan.cpp create mode 100644 src/iee/game/world_post_plan.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 02b0cae..83c60eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,6 +107,7 @@ add_library(iee_common STATIC src/iee/game/tis_palette.cpp src/iee/game/tis_runtime.cpp src/iee/game/wed_runtime.cpp + src/iee/game/world_post_plan.cpp ) target_include_directories(iee_common diff --git a/src/iee/game/world_post_plan.cpp b/src/iee/game/world_post_plan.cpp new file mode 100644 index 0000000..9fe62ec --- /dev/null +++ b/src/iee/game/world_post_plan.cpp @@ -0,0 +1,47 @@ +#include "iee/game/world_post_plan.h" + +#include +#include + +namespace iee::game { +namespace { +constexpr int kMinBlurExtent = 8; +constexpr int kMaxBloomLevels = 5; +} // namespace + +Extent half_extent(Extent extent) noexcept { + return {std::max(1, extent.width / 2), std::max(1, extent.height / 2)}; +} + +int available_blur_levels(Extent base) noexcept { + int levels = 0; + Extent extent = base; + while (levels < kMaxBlurLevels) { + extent = half_extent(extent); + if (std::min(extent.width, extent.height) < kMinBlurExtent) break; + ++levels; + } + return levels; +} + +BlurPlan blur_plan_for_radius(float radiusPixels, Extent base) noexcept { + // Written so NaN fails the test. + if (!(radiusPixels >= 1.0f)) return {}; + const int available = available_blur_levels(base); + if (available == 0) return {}; + int levels = 1; + while (levels < available && static_cast(1 << (levels + 1)) < radiusPixels) ++levels; + const auto reach = static_cast(1 << (levels + 1)); + return {levels, std::clamp(radiusPixels / reach, 0.5f, 1.5f)}; +} + +int bloom_levels(Extent base) noexcept { + return std::min(kMaxBloomLevels, available_blur_levels(base)); +} + +float pixels_per_world_pixel(float viewportWidth, float viewWorldWidth) noexcept { + if (!std::isfinite(viewportWidth) || !std::isfinite(viewWorldWidth)) return 1.0f; + if (!(viewportWidth > 0.0f) || !(viewWorldWidth > 0.0f)) return 1.0f; + return std::clamp(viewportWidth / viewWorldWidth, 0.25f, 8.0f); +} +} // namespace iee::game diff --git a/src/iee/game/world_post_plan.h b/src/iee/game/world_post_plan.h new file mode 100644 index 0000000..82aba06 --- /dev/null +++ b/src/iee/game/world_post_plan.h @@ -0,0 +1,30 @@ +#pragma once + +namespace iee::game { +// Sizing for the world post-processing passes (soft fog of war, bloom). Pure +// arithmetic so it can be tested without a GL context. + +struct Extent { + int width{}; + int height{}; + friend bool operator==(const Extent&, const Extent&) = default; +}; + +inline constexpr int kMaxBlurLevels = 6; + +// A dual-filter blur: `levels` successive half-size images below the source, +// sampled with `offset` texels of spread. levels == 0 means "do not blur". +struct BlurPlan { + int levels{}; + float offset{1.0f}; +}; + +Extent half_extent(Extent extent) noexcept; +// How many half-size levels fit below `base` before a side drops under 8 px. +int available_blur_levels(Extent base) noexcept; +// The plan whose blur reaches about `radiusPixels` on an image of `base` size. +BlurPlan blur_plan_for_radius(float radiusPixels, Extent base) noexcept; +int bloom_levels(Extent base) noexcept; +// Screen pixels per world pixel at the current zoom; 1 when either is unknown. +float pixels_per_world_pixel(float viewportWidth, float viewWorldWidth) noexcept; +} // namespace iee::game diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 6697680..050b378 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -33,6 +33,7 @@ #include "iee/game/tile_upscale.h" #include "iee/game/tis_palette.h" #include "iee/game/wed_runtime.h" +#include "iee/game/world_post_plan.h" namespace { int g_failures = 0; @@ -1266,6 +1267,50 @@ void test_interface_contract_token_boundary() { expect_eq(failed.missingIdentifiers.size(), std::size_t{1}, "only sTex missing"); expect_eq(failed.missingIdentifiers[0], std::string("sTex"), "missing identifier is sTex"); } +void test_world_post_plan() { + using namespace iee::game; + + expect_true(half_extent({1920, 1080}) == Extent{960, 540}, "Half extent halves both sides"); + expect_true(half_extent({1, 1}) == Extent{1, 1}, "Half extent never reaches zero"); + expect_true(half_extent({0, 0}) == Extent{1, 1}, "A degenerate extent still yields 1x1"); + + expect_eq(available_blur_levels({1920, 1080}), 6, "A full-HD image supports the level cap"); + expect_eq(available_blur_levels({64, 64}), 3, "Levels stop before a side drops below 8"); + expect_eq(available_blur_levels({10, 10}), 0, "A tiny image supports no blur levels"); + expect_eq(available_blur_levels({0, 0}), 0, "A degenerate image supports no blur levels"); + + const auto none = blur_plan_for_radius(0.0f, {1920, 1080}); + expect_eq(none.levels, 0, "Radius 0 means no blur"); + expect_eq(blur_plan_for_radius(std::numeric_limits::quiet_NaN(), {1920, 1080}).levels, 0, + "A NaN radius means no blur"); + expect_eq(blur_plan_for_radius(-5.0f, {1920, 1080}).levels, 0, "A negative radius means no blur"); + + const auto small = blur_plan_for_radius(4.0f, {1920, 1080}); + expect_eq(small.levels, 1, "A 4 px radius needs one level"); + expect_eq(small.offset, 1.0f, "A radius equal to the level reach uses offset 1"); + + const auto typical = blur_plan_for_radius(24.0f, {1920, 1080}); + expect_eq(typical.levels, 4, "A 24 px radius needs four levels (reach 32)"); + expect_eq(typical.offset, 0.75f, "Offset scales the reach down to the radius"); + + const auto huge = blur_plan_for_radius(1000.0f, {1920, 1080}); + expect_eq(huge.levels, 6, "Levels are capped by what the image supports"); + expect_eq(huge.offset, 1.5f, "Offset is clamped so taps never skip texels badly"); + + expect_eq(blur_plan_for_radius(24.0f, {10, 10}).levels, 0, + "No blur is planned on an image too small to hold a level"); + + expect_eq(bloom_levels({960, 540}), 5, "Bloom uses at most five levels"); + expect_eq(bloom_levels({40, 40}), 2, "Bloom levels shrink with the image"); + + expect_eq(pixels_per_world_pixel(3840.0f, 1920.0f), 2.0f, "Scale is viewport over world width"); + expect_eq(pixels_per_world_pixel(3840.0f, 0.0f), 1.0f, "An unknown world width falls back to 1"); + expect_eq(pixels_per_world_pixel(0.0f, 1920.0f), 1.0f, "An unknown viewport falls back to 1"); + expect_eq(pixels_per_world_pixel(100000.0f, 1.0f), 8.0f, "Scale is clamped to a sane maximum"); + expect_eq(pixels_per_world_pixel(std::numeric_limits::infinity(), 100.0f), 1.0f, + "A non-finite input falls back to 1"); +} + } // namespace void test_area_liquid_texture_packing() { @@ -2079,6 +2124,7 @@ int main() { test_collect_area_static_animations(); test_sprite_motion_tracker(); test_animation_interpolation(); + test_world_post_plan(); test_build_area_effect_points(); test_config_detection_section(); From 2b79279001c0cf0193ec45a791e36ef53dd1bc97 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Wed, 7 Oct 2026 21:07:47 +0200 Subject: [PATCH 35/57] Config: soft fog of war and bloom settings --- src/iee/core/config.cpp | 21 +++++++++++++++++++++ src/iee/core/config.h | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/src/iee/core/config.cpp b/src/iee/core/config.cpp index 6e047bb..f03004a 100644 --- a/src/iee/core/config.cpp +++ b/src/iee/core/config.cpp @@ -58,6 +58,12 @@ static void normalize(EngineConfig& cfg) noexcept { if (!std::isfinite(cfg.lodBias)) cfg.lodBias = -0.25f; cfg.maxAnisotropy = std::clamp(cfg.maxAnisotropy, 1.0f, 64.0f); cfg.lodBias = std::clamp(cfg.lodBias, -4.0f, 4.0f); + if (!std::isfinite(cfg.softFogRadius)) cfg.softFogRadius = 24.0f; + if (!std::isfinite(cfg.bloomThreshold)) cfg.bloomThreshold = 0.80f; + if (!std::isfinite(cfg.bloomStrength)) cfg.bloomStrength = 0.35f; + cfg.softFogRadius = std::clamp(cfg.softFogRadius, 0.0f, 256.0f); + cfg.bloomThreshold = std::clamp(cfg.bloomThreshold, 0.0f, 1.0f); + cfg.bloomStrength = std::clamp(cfg.bloomStrength, 0.0f, 2.0f); } static void apply_kv(EngineConfig& cfg, const std::string& section, const std::string& key, @@ -98,6 +104,16 @@ static void apply_kv(EngineConfig& cfg, const std::string& section, const std::s assign_bool(cfg.smoothSpriteMovement); else if (iequals(key, "InterpolateAnimations")) assign_bool(cfg.interpolateAnimations); + else if (iequals(key, "SoftFogOfWar")) + assign_bool(cfg.softFogOfWar); + else if (iequals(key, "SoftFogRadius")) + assign_float(cfg.softFogRadius); + else if (iequals(key, "Bloom")) + assign_bool(cfg.bloom); + else if (iequals(key, "BloomThreshold")) + assign_float(cfg.bloomThreshold); + else if (iequals(key, "BloomStrength")) + assign_float(cfg.bloomStrength); return; } @@ -195,6 +211,11 @@ bool ConfigManager::save(const std::filesystem::path& path, const EngineConfig& f << "LODBias = " << cfg.lodBias << "\n"; write_bool(f, "SmoothSpriteMovement", cfg.smoothSpriteMovement); write_bool(f, "InterpolateAnimations", cfg.interpolateAnimations); + write_bool(f, "SoftFogOfWar", cfg.softFogOfWar); + f << "SoftFogRadius = " << cfg.softFogRadius << "\n"; + write_bool(f, "Bloom", cfg.bloom); + f << "BloomThreshold = " << cfg.bloomThreshold << "\n"; + f << "BloomStrength = " << cfg.bloomStrength << "\n"; write_section(f, "Detection"); write_bool(f, "AreaAnimationScan", cfg.enableAreaAnimationScan); diff --git a/src/iee/core/config.h b/src/iee/core/config.h index 3a91b3a..b1a4543 100644 --- a/src/iee/core/config.h +++ b/src/iee/core/config.h @@ -9,6 +9,11 @@ struct EngineConfig { float lodBias = -0.25f; bool smoothSpriteMovement = false; bool interpolateAnimations = false; + bool softFogOfWar = false; + float softFogRadius = 24.0f; // world pixels; 0 = capture without blur + bool bloom = false; + float bloomThreshold = 0.80f; + float bloomStrength = 0.35f; bool dumpEngineShaders = false; bool enableDebugHotkeys = false; From 694d3648c640111eecad0b30e17d23a3e19f4ede Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Wed, 7 Oct 2026 21:07:47 +0200 Subject: [PATCH 36/57] Manifest: CInfinity::RenderFog and DrawFlush_GL for 2.7.3 --- src/iee/game/build_manifest.cpp | 11 +++++- src/iee/game/build_manifest.h | 7 ++++ src/iee/game/game_addrs.cpp | 43 +++++++++++++++----- src/iee/game/game_addrs.h | 4 ++ tests/iee_tests.cpp | 70 +++++++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 11 deletions(-) diff --git a/src/iee/game/build_manifest.cpp b/src/iee/game/build_manifest.cpp index 74c368a..c9cd6a8 100644 --- a/src/iee/game/build_manifest.cpp +++ b/src/iee/game/build_manifest.cpp @@ -196,9 +196,15 @@ constexpr BuildManifest kKnownBuilds[] = { "40 53 48 83 EC 20 48 8B D9 48 8B 89 08 01 00 00 48 85 C9 0F", "48 89 5C 24 10 48 89 74 24 18 57 48 83 EC 20 48 8B F9 33 DB 48 8B 89 08 01 00 00 " "48 8B F2 48 85 C9 75 15 48 89 1A 33 C0", + // CInfinity::RenderFog and DrawFlush_GL (PDB-named, each + // offline-verified unique on the Steam 2.7.3.0 binary). + "48 89 5C 24 10 4C 89 44 24 18 55 56 57 41 54 41 55 41 56 41 57 48 83 EC 50 " + "48 8B D9 E8 ? ? ? ? B9 FF FF FF FF E8 ? ? ? ? B9 E1 0D 00 00", + "4C 8B DC 55 41 56 41 57 49 8D 6B D8 48 81 EC 10 01 00 00 48 8B 05 ? ? ? ? " + "48 33 C4 48 89 45 90 45 33 F6 44 39 35 ? ? ? ? 45 8B FE 0F 84", }, {0x27EBD0, 0x4257C0, 0x276700, 0x1F27D0, 0x36BA50, 0x36F170, 0x36E820, 0x4118A0, 0x411660, - 0x411780}, + 0x411780, 0x2A1B60, 0x42B350}, {0x100, 0x1DC, 0x14, 0x6590, 0x6598, 0x65F8}, {{ {"CRes_Demand", 0x36, BranchInstructionKind::CallRel32, 0xE8, 1, 5, true}, @@ -263,6 +269,9 @@ static_assert([] { } return true; }(), "2.7.3 smoothed object render pattern format is invalid"); +static_assert(validate_pattern_format(kKnownBuilds[1].patterns.renderFog) && + validate_pattern_format(kKnownBuilds[1].patterns.drawFlush), + "2.7.3 world post pattern format is invalid"); static_assert(kKnownBuilds[1].validate(), "2.7.3 build manifest is invalid"); } // namespace diff --git a/src/iee/game/build_manifest.h b/src/iee/game/build_manifest.h index f3c3a65..b5f70ce 100644 --- a/src/iee/game/build_manifest.h +++ b/src/iee/game/build_manifest.h @@ -53,6 +53,11 @@ struct PatternSet { // being searched for. std::string_view vidCellGetFrame{}; std::string_view vidCellFrameAccessor{}; + // Optional: CInfinity::RenderFog and the GL backend's queue flush + // (DrawFlush_GL). Hooked/called together for the world post passes (soft + // fog of war, bloom); either empty or non-unique leaves both off. + std::string_view renderFog{}; + std::string_view drawFlush{}; }; struct ReferenceRvas { @@ -67,6 +72,8 @@ struct ReferenceRvas { std::uintptr_t vidCellGetFrame{}; std::uintptr_t vidCellGetCurrentCenterPoint{}; std::uintptr_t vidCellGetCurrentFrameSize{}; + std::uintptr_t renderFog{}; + std::uintptr_t drawFlush{}; }; struct RuntimeOffsets { diff --git a/src/iee/game/game_addrs.cpp b/src/iee/game/game_addrs.cpp index 214222e..24c583c 100644 --- a/src/iee/game/game_addrs.cpp +++ b/src/iee/game/game_addrs.cpp @@ -79,19 +79,20 @@ namespace iee::game { } } + const auto resolveOptional = [&](const char *name, std::string_view pattern, + std::uintptr_t referenceRva) -> std::uintptr_t { + if (pattern.empty()) return 0; + std::size_t matches = 0; + auto target = reinterpret_cast( + core::find_unique_in_module(nullptr, pattern, &matches)); + recover(name, target, matches, referenceRva, pattern); + if (!target) LOG_WARN("{} pattern matched {} times", name, matches); + return target; + }; + // Optional targets: sprite movement smoothing. Resolved only when // requested, and only as a complete set. if (cfg.smoothSpriteMovement || cfg.interpolateAnimations) { - const auto resolveOptional = [&](const char *name, std::string_view pattern, - std::uintptr_t referenceRva) -> std::uintptr_t { - if (pattern.empty()) return 0; - std::size_t matches = 0; - auto target = reinterpret_cast( - core::find_unique_in_module(nullptr, pattern, &matches)); - recover(name, target, matches, referenceRva, pattern); - if (!target) LOG_WARN("{} pattern matched {} times", name, matches); - return target; - }; const auto &patterns = manifest.patterns; const auto &rvas = manifest.referenceRvas; out.SpriteRender = @@ -143,6 +144,28 @@ namespace iee::game { } } + // Optional targets: world post passes. Resolved only when requested + // (or when debug hotkeys can switch them on), and only as a pair. + if (cfg.softFogOfWar || cfg.bloom || cfg.enableDebugHotkeys) { + out.RenderFog = resolveOptional("CInfinity::RenderFog", manifest.patterns.renderFog, + manifest.referenceRvas.renderFog); + out.DrawFlush = resolveOptional("DrawFlush_GL", manifest.patterns.drawFlush, + manifest.referenceRvas.drawFlush); + if (out.RenderFog && out.DrawFlush) { + LOG_INFO("World post targets resolved at RVA 0x{:X} / 0x{:X}", + out.RenderFog - moduleBase, out.DrawFlush - moduleBase); + } else { + const bool known = + !manifest.patterns.renderFog.empty() && !manifest.patterns.drawFlush.empty(); + out.RenderFog = out.DrawFlush = 0; + if (cfg.softFogOfWar || cfg.bloom) { + LOG_WARN("Soft fog of war and bloom disabled: {}", + known ? "RenderFog/DrawFlush did not resolve uniquely" + : "this build has no RenderFog/DrawFlush targets"); + } + } + } + const bool success = out.LoadArea && out.RenderTexture; if (success) { diff --git a/src/iee/game/game_addrs.h b/src/iee/game/game_addrs.h index 1c603b2..65eb8a2 100644 --- a/src/iee/game/game_addrs.h +++ b/src/iee/game/game_addrs.h @@ -27,6 +27,10 @@ namespace iee::game { // Optional, each independent; 0 where unresolved. Indexed like // BuildManifest::smoothedObjectRenders. std::array SmoothedObjectRenders{}; + // Optional, all-or-nothing: the world post passes (soft fog of war, + // bloom) need the fog render to hook and the queue flush to call. + std::uintptr_t RenderFog = 0; + std::uintptr_t DrawFlush = 0; bool initialized = false; }; diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 050b378..fa81b6d 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1267,6 +1267,74 @@ void test_interface_contract_token_boundary() { expect_eq(failed.missingIdentifiers.size(), std::size_t{1}, "only sTex missing"); expect_eq(failed.missingIdentifiers[0], std::string("sTex"), "missing identifier is sTex"); } +void test_config_world_post() { + iee::core::EngineConfig defaults{}; + expect_true(!defaults.softFogOfWar && !defaults.bloom, "World post effects default off"); + expect_eq(defaults.softFogRadius, 24.0f, "Soft fog radius default"); + expect_eq(defaults.bloomThreshold, 0.80f, "Bloom threshold default"); + expect_eq(defaults.bloomStrength, 0.35f, "Bloom strength default"); + + const auto tempPath = + std::filesystem::current_path() / "InfinityEngine-Enhancer-world-post-test.ini"; + { + std::ofstream out(tempPath, std::ios::trunc); + out << "[Rendering]\n"; + out << "SoftFogOfWar = true\n"; + out << "SoftFogRadius = 40\n"; + out << "Bloom = true\n"; + out << "BloomThreshold = 0.6\n"; + out << "BloomStrength = 0.5\n"; + } + iee::core::EngineConfig cfg{}; + expect_true(iee::core::ConfigManager::load(tempPath, cfg), "World post keys should load"); + expect_true(cfg.softFogOfWar && cfg.bloom, "World post bools should parse"); + expect_eq(cfg.softFogRadius, 40.0f, "Soft fog radius should parse"); + expect_eq(cfg.bloomThreshold, 0.6f, "Bloom threshold should parse"); + expect_eq(cfg.bloomStrength, 0.5f, "Bloom strength should parse"); + + { + std::ofstream out(tempPath, std::ios::trunc); + out << "[Rendering]\n"; + out << "SoftFogRadius = 100000\n"; + out << "BloomThreshold = 7\n"; + out << "BloomStrength = -3\n"; + } + cfg = {}; + expect_true(iee::core::ConfigManager::load(tempPath, cfg), "Out-of-range values should load"); + expect_eq(cfg.softFogRadius, 256.0f, "Soft fog radius is clamped to its maximum"); + expect_eq(cfg.bloomThreshold, 1.0f, "Bloom threshold is clamped to 1"); + expect_eq(cfg.bloomStrength, 0.0f, "Bloom strength is clamped to 0"); + + { + std::ofstream out(tempPath, std::ios::trunc); + out << "[Rendering]\n"; + out << "SoftFogRadius = nan\n"; + out << "BloomThreshold = inf\n"; + out << "BloomStrength = nan\n"; + } + cfg = {}; + expect_true(iee::core::ConfigManager::load(tempPath, cfg), "Non-finite values should load"); + expect_eq(cfg.softFogRadius, 24.0f, "A non-finite radius falls back to the default"); + expect_eq(cfg.bloomThreshold, 0.80f, "A non-finite threshold falls back to the default"); + expect_eq(cfg.bloomStrength, 0.35f, "A non-finite strength falls back to the default"); + + std::error_code error; + std::filesystem::remove(tempPath, error); +} + +void test_manifest_world_post_targets() { + const auto found = iee::game::find_manifest("BGEE 2.7.3.x"); + expect_true(found.has_value(), "The 2.7.3 manifest should be registered"); + if (!found) return; + const auto& manifest = found->get(); + expect_true(!manifest.patterns.renderFog.empty(), "2.7.3 should carry a RenderFog pattern"); + expect_true(!manifest.patterns.drawFlush.empty(), "2.7.3 should carry a DrawFlush pattern"); + expect_eq(manifest.referenceRvas.renderFog, std::uintptr_t{0x2A1B60}, "RenderFog reference RVA"); + expect_eq(manifest.referenceRvas.drawFlush, std::uintptr_t{0x42B350}, "DrawFlush reference RVA"); + expect_true(iee::game::current_manifest().patterns.renderFog.empty(), + "The 2.6.6 manifest has no world post targets"); +} + void test_world_post_plan() { using namespace iee::game; @@ -2112,6 +2180,7 @@ int main() { test_tile_table_detection_ignores_garbage_steps(); test_tile_table_detection_uses_coordinate_deltas(); test_manifest_infgame_offsets(); + test_manifest_world_post_targets(); test_shader_name_extraction(); test_interface_contract(); test_interface_contract_token_boundary(); @@ -2127,6 +2196,7 @@ int main() { test_world_post_plan(); test_build_area_effect_points(); test_config_detection_section(); + test_config_world_post(); if (g_failures != 0) { std::cerr << g_failures << " test(s) failed\n"; From 29dc3eb31c33a92a1f9b55110d6def4d47e57cb1 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Wed, 7 Oct 2026 21:07:47 +0200 Subject: [PATCH 37/57] GL: framebuffer and vertex-array entry points, pass state guard --- src/iee/core/gl_pass_guard.h | 75 ++++++++++++++++++++++++++++ src/iee/game/opengl_types.cpp | 51 ++++++++++++++++++- src/iee/game/opengl_types.h | 92 +++++++++++++++++++++++++++++++++++ 3 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 src/iee/core/gl_pass_guard.h diff --git a/src/iee/core/gl_pass_guard.h b/src/iee/core/gl_pass_guard.h new file mode 100644 index 0000000..cc40d89 --- /dev/null +++ b/src/iee/core/gl_pass_guard.h @@ -0,0 +1,75 @@ +#pragma once + +#include "iee/game/opengl_types.h" + +namespace iee::core { +// Saves the GL state our own draw passes change and puts it back on scope +// exit. The engine issues state changes relative to what it believes the +// hardware state is, so anything left altered corrupts its next draws. +// Framebuffer and texture bindings are not covered: the caller owns the +// framebuffer hand-over, and GlStateGuard covers texture units. +// Requires OpenGLFunctions::postProcessAvailable. +class GlPassGuard { + public: + GlPassGuard() noexcept { + const auto& gl = game::gl::get_gl_functions(); + gl.glGetIntegerv(game::gl::CURRENT_PROGRAM, &program_); + gl.glGetIntegerv(game::gl::VERTEX_ARRAY_BINDING, &vertexArray_); + gl.glGetIntegerv(game::gl::ARRAY_BUFFER_BINDING, &arrayBuffer_); + gl.glGetIntegerv(game::gl::VIEWPORT, viewport_); + gl.glGetIntegerv(game::gl::BLEND_SRC_RGB, &blendSrcRgb_); + gl.glGetIntegerv(game::gl::BLEND_DST_RGB, &blendDstRgb_); + gl.glGetIntegerv(game::gl::BLEND_SRC_ALPHA, &blendSrcAlpha_); + gl.glGetIntegerv(game::gl::BLEND_DST_ALPHA, &blendDstAlpha_); + gl.glGetFloatv(game::gl::COLOR_CLEAR_VALUE, clearColor_); + blend_ = gl.glIsEnabled(game::gl::BLEND) != 0; + scissor_ = gl.glIsEnabled(game::gl::SCISSOR_TEST) != 0; + depthTest_ = gl.glIsEnabled(game::gl::DEPTH_TEST) != 0; + cullFace_ = gl.glIsEnabled(game::gl::CULL_FACE) != 0; + } + + ~GlPassGuard() noexcept { + const auto& gl = game::gl::get_gl_functions(); + const auto set = [&](unsigned capability, bool enabled) { + if (enabled) { + gl.glEnable(capability); + } else { + gl.glDisable(capability); + } + }; + set(game::gl::BLEND, blend_); + set(game::gl::SCISSOR_TEST, scissor_); + set(game::gl::DEPTH_TEST, depthTest_); + set(game::gl::CULL_FACE, cullFace_); + gl.glBlendFuncSeparate( + static_cast(blendSrcRgb_), static_cast(blendDstRgb_), + static_cast(blendSrcAlpha_), static_cast(blendDstAlpha_)); + gl.glClearColor(clearColor_[0], clearColor_[1], clearColor_[2], clearColor_[3]); + gl.glViewport(viewport_[0], viewport_[1], viewport_[2], viewport_[3]); + gl.glBindVertexArray(static_cast(vertexArray_)); + gl.glBindBuffer(game::gl::ARRAY_BUFFER, static_cast(arrayBuffer_)); + gl.glUseProgram(static_cast(program_)); + } + + GlPassGuard(const GlPassGuard&) = delete; + GlPassGuard& operator=(const GlPassGuard&) = delete; + + // The viewport as the engine left it: {x, y, width, height}. + [[nodiscard]] const int* viewport() const noexcept { return viewport_; } + + private: + int program_{}; + int vertexArray_{}; + int arrayBuffer_{}; + int viewport_[4]{}; + int blendSrcRgb_{}; + int blendDstRgb_{}; + int blendSrcAlpha_{}; + int blendDstAlpha_{}; + float clearColor_[4]{}; + bool blend_{}; + bool scissor_{}; + bool depthTest_{}; + bool cullFace_{}; +}; +} // namespace iee::core diff --git a/src/iee/game/opengl_types.cpp b/src/iee/game/opengl_types.cpp index 1d4e91f..7842827 100644 --- a/src/iee/game/opengl_types.cpp +++ b/src/iee/game/opengl_types.cpp @@ -111,6 +111,17 @@ bool OpenGLFunctions::initialize() noexcept { reinterpret_cast(get_gl1_proc_address(opengl32, "glDeleteTextures")); glPixelStorei = reinterpret_cast(get_gl1_proc_address(opengl32, "glPixelStorei")); + glViewport = reinterpret_cast(get_gl1_proc_address(opengl32, "glViewport")); + glEnable = reinterpret_cast(get_gl1_proc_address(opengl32, "glEnable")); + glDisable = reinterpret_cast(get_gl1_proc_address(opengl32, "glDisable")); + glIsEnabled = reinterpret_cast(get_gl1_proc_address(opengl32, "glIsEnabled")); + glBlendFunc = reinterpret_cast(get_gl1_proc_address(opengl32, "glBlendFunc")); + glClearColor = + reinterpret_cast(get_gl1_proc_address(opengl32, "glClearColor")); + glClear = reinterpret_cast(get_gl1_proc_address(opengl32, "glClear")); + glDrawArrays = + reinterpret_cast(get_gl1_proc_address(opengl32, "glDrawArrays")); + glGetFloatv = reinterpret_cast(get_gl1_proc_address(opengl32, "glGetFloatv")); glGetString = reinterpret_cast(get_gl1_proc_address(opengl32, "glGetString")); @@ -160,6 +171,33 @@ bool OpenGLFunctions::initialize() noexcept { reinterpret_cast(get_ext_proc_address(opengl32, "glGenerateMipmap")); glBindFramebuffer = reinterpret_cast(get_ext_proc_address(opengl32, "glBindFramebuffer")); + glGenFramebuffers = reinterpret_cast( + get_ext_proc_address(opengl32, "glGenFramebuffers")); + glDeleteFramebuffers = reinterpret_cast( + get_ext_proc_address(opengl32, "glDeleteFramebuffers")); + glFramebufferTexture2D = reinterpret_cast( + get_ext_proc_address(opengl32, "glFramebufferTexture2D")); + glCheckFramebufferStatus = reinterpret_cast( + get_ext_proc_address(opengl32, "glCheckFramebufferStatus")); + glBlitFramebuffer = reinterpret_cast( + get_ext_proc_address(opengl32, "glBlitFramebuffer")); + glGenVertexArrays = reinterpret_cast( + get_ext_proc_address(opengl32, "glGenVertexArrays")); + glDeleteVertexArrays = reinterpret_cast( + get_ext_proc_address(opengl32, "glDeleteVertexArrays")); + glBindVertexArray = reinterpret_cast( + get_ext_proc_address(opengl32, "glBindVertexArray")); + glGenBuffers = reinterpret_cast(get_ext_proc_address(opengl32, "glGenBuffers")); + glDeleteBuffers = + reinterpret_cast(get_ext_proc_address(opengl32, "glDeleteBuffers")); + glBindBuffer = reinterpret_cast(get_ext_proc_address(opengl32, "glBindBuffer")); + glBufferData = reinterpret_cast(get_ext_proc_address(opengl32, "glBufferData")); + glEnableVertexAttribArray = reinterpret_cast( + get_ext_proc_address(opengl32, "glEnableVertexAttribArray")); + glVertexAttribPointer = reinterpret_cast( + get_ext_proc_address(opengl32, "glVertexAttribPointer")); + glBlendFuncSeparate = reinterpret_cast( + get_ext_proc_address(opengl32, "glBlendFuncSeparate")); glIsProgram = reinterpret_cast(get_ext_proc_address(opengl32, "glIsProgram")); glShaderSourceARB = reinterpret_cast(get_ext_proc_address(opengl32, "glShaderSourceARB")); @@ -198,15 +236,24 @@ bool OpenGLFunctions::initialize() noexcept { glTexImage2D != nullptr && glDeleteTextures != nullptr && glPixelStorei != nullptr && glActiveTexture != nullptr; compressedTextureUploadAvailable = textureUploadAvailable && glCompressedTexImage2D != nullptr; + postProcessAvailable = + textureUploadAvailable && readyForSourcePatching && glViewport && glEnable && glDisable && + glIsEnabled && glBlendFunc && glClearColor && glClear && glDrawArrays && glGetFloatv && + glBindFramebuffer && glGenFramebuffers && glDeleteFramebuffers && glFramebufferTexture2D && + glCheckFramebufferStatus && glBlitFramebuffer && glGenVertexArrays && + glDeleteVertexArrays && glBindVertexArray && glGenBuffers && glDeleteBuffers && + glBindBuffer && glBufferData && glEnableVertexAttribArray && glVertexAttribPointer && + glBlendFuncSeparate; if (valid) { LOG_INFO( "OpenGL initialized (source patching={}, ARB shaders={}, texture upload={}, compressed " - "texture upload={})", + "texture upload={}, post process={})", readyForSourcePatching ? "ready" : "partial", arbShaderObjectsAvailable ? "ready" : "partial", textureUploadAvailable ? "ready" : "partial", - compressedTextureUploadAvailable ? "ready" : "partial"); + compressedTextureUploadAvailable ? "ready" : "partial", + postProcessAvailable ? "ready" : "partial"); LOG_DEBUG( "OpenGL entry points: glGetString={}, glTexParameteri={}, glTexParameterf={}, " "glGetIntegerv={}, glGetTexParameteriv={}, glGetError={}, glGenTextures={}, " diff --git a/src/iee/game/opengl_types.h b/src/iee/game/opengl_types.h index b1f9a43..fcccdfc 100644 --- a/src/iee/game/opengl_types.h +++ b/src/iee/game/opengl_types.h @@ -1,4 +1,5 @@ #pragma once +#include #include #ifdef _WIN32 @@ -34,6 +35,39 @@ constexpr unsigned RED = 0x1903; constexpr unsigned UNSIGNED_BYTE = 0x1401; constexpr unsigned UNPACK_ALIGNMENT = 0x0CF5; constexpr unsigned FRAMEBUFFER = 0x8D40; +constexpr unsigned READ_FRAMEBUFFER = 0x8CA8; +constexpr unsigned DRAW_FRAMEBUFFER = 0x8CA9; +constexpr unsigned DRAW_FRAMEBUFFER_BINDING = 0x8CA6; +constexpr unsigned READ_FRAMEBUFFER_BINDING = 0x8CAA; +constexpr unsigned COLOR_ATTACHMENT0 = 0x8CE0; +constexpr unsigned FRAMEBUFFER_COMPLETE = 0x8CD5; +constexpr unsigned RGBA16F = 0x881A; +constexpr unsigned HALF_FLOAT = 0x140B; +constexpr unsigned FLOAT = 0x1406; +constexpr unsigned VIEWPORT = 0x0BA2; +constexpr unsigned SCISSOR_TEST = 0x0C11; +constexpr unsigned BLEND = 0x0BE2; +constexpr unsigned DEPTH_TEST = 0x0B71; +constexpr unsigned CULL_FACE = 0x0B44; +constexpr unsigned BLEND_DST_RGB = 0x80C8; +constexpr unsigned BLEND_SRC_RGB = 0x80C9; +constexpr unsigned BLEND_DST_ALPHA = 0x80CA; +constexpr unsigned BLEND_SRC_ALPHA = 0x80CB; +constexpr unsigned CURRENT_PROGRAM = 0x8B8D; +constexpr unsigned VERTEX_ARRAY_BINDING = 0x85B5; +constexpr unsigned ARRAY_BUFFER = 0x8892; +constexpr unsigned ARRAY_BUFFER_BINDING = 0x8894; +constexpr unsigned STATIC_DRAW = 0x88E4; +constexpr unsigned COLOR_CLEAR_VALUE = 0x0C22; +constexpr unsigned COLOR_BUFFER_BIT = 0x4000; +constexpr unsigned TRIANGLES = 0x0004; +constexpr unsigned ZERO = 0; +constexpr unsigned ONE = 1; +constexpr unsigned DST_COLOR = 0x0306; +constexpr unsigned FRAGMENT_SHADER = 0x8B30; +constexpr unsigned VERTEX_SHADER = 0x8B31; +constexpr unsigned COMPILE_STATUS = 0x8B81; +constexpr unsigned LINK_STATUS = 0x8B82; constexpr unsigned COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0; constexpr unsigned COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1; constexpr unsigned COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3; @@ -99,6 +133,37 @@ using PFN_glCompressedTexImage2D = void(APIENTRY*)(unsigned target, int level, int border, int imageSize, const void* data); using PFN_glGenerateMipmap = void(APIENTRY*)(unsigned target); using PFN_glBindFramebuffer = void(APIENTRY*)(unsigned target, unsigned framebuffer); +using PFN_glViewport = void(APIENTRY*)(int x, int y, int width, int height); +using PFN_glEnable = void(APIENTRY*)(unsigned cap); +using PFN_glDisable = void(APIENTRY*)(unsigned cap); +using PFN_glIsEnabled = unsigned char(APIENTRY*)(unsigned cap); +using PFN_glBlendFunc = void(APIENTRY*)(unsigned sfactor, unsigned dfactor); +using PFN_glClearColor = void(APIENTRY*)(float r, float g, float b, float a); +using PFN_glClear = void(APIENTRY*)(unsigned mask); +using PFN_glDrawArrays = void(APIENTRY*)(unsigned mode, int first, int count); +using PFN_glGetFloatv = void(APIENTRY*)(unsigned pname, float* data); +using PFN_glGenFramebuffers = void(APIENTRY*)(int n, unsigned* framebuffers); +using PFN_glDeleteFramebuffers = void(APIENTRY*)(int n, const unsigned* framebuffers); +using PFN_glFramebufferTexture2D = void(APIENTRY*)(unsigned target, unsigned attachment, + unsigned textarget, unsigned texture, int level); +using PFN_glCheckFramebufferStatus = unsigned(APIENTRY*)(unsigned target); +using PFN_glBlitFramebuffer = void(APIENTRY*)(int srcX0, int srcY0, int srcX1, int srcY1, + int dstX0, int dstY0, int dstX1, int dstY1, + unsigned mask, unsigned filter); +using PFN_glGenVertexArrays = void(APIENTRY*)(int n, unsigned* arrays); +using PFN_glDeleteVertexArrays = void(APIENTRY*)(int n, const unsigned* arrays); +using PFN_glBindVertexArray = void(APIENTRY*)(unsigned array); +using PFN_glGenBuffers = void(APIENTRY*)(int n, unsigned* buffers); +using PFN_glDeleteBuffers = void(APIENTRY*)(int n, const unsigned* buffers); +using PFN_glBindBuffer = void(APIENTRY*)(unsigned target, unsigned buffer); +using PFN_glBufferData = void(APIENTRY*)(unsigned target, std::ptrdiff_t size, const void* data, + unsigned usage); +using PFN_glEnableVertexAttribArray = void(APIENTRY*)(unsigned index); +using PFN_glVertexAttribPointer = void(APIENTRY*)(unsigned index, int size, unsigned type, + unsigned char normalized, int stride, + const void* pointer); +using PFN_glBlendFuncSeparate = void(APIENTRY*)(unsigned srcRgb, unsigned dstRgb, + unsigned srcAlpha, unsigned dstAlpha); using PFN_glIsProgram = unsigned char(APIENTRY*)(unsigned program); using PFN_glShaderSourceARB = void(APIENTRY*)(unsigned shader, int count, const char* const* string, const int* length); @@ -124,6 +189,15 @@ struct OpenGLFunctions { PFN_glTexImage2D glTexImage2D{}; PFN_glDeleteTextures glDeleteTextures{}; PFN_glPixelStorei glPixelStorei{}; + PFN_glViewport glViewport{}; + PFN_glEnable glEnable{}; + PFN_glDisable glDisable{}; + PFN_glIsEnabled glIsEnabled{}; + PFN_glBlendFunc glBlendFunc{}; + PFN_glClearColor glClearColor{}; + PFN_glClear glClear{}; + PFN_glDrawArrays glDrawArrays{}; + PFN_glGetFloatv glGetFloatv{}; // Extensions PFN_glCreateShader glCreateShader{}; PFN_glShaderSource glShaderSource{}; @@ -150,6 +224,21 @@ struct OpenGLFunctions { PFN_glCompressedTexImage2D glCompressedTexImage2D{}; PFN_glGenerateMipmap glGenerateMipmap{}; PFN_glBindFramebuffer glBindFramebuffer{}; + PFN_glGenFramebuffers glGenFramebuffers{}; + PFN_glDeleteFramebuffers glDeleteFramebuffers{}; + PFN_glFramebufferTexture2D glFramebufferTexture2D{}; + PFN_glCheckFramebufferStatus glCheckFramebufferStatus{}; + PFN_glBlitFramebuffer glBlitFramebuffer{}; + PFN_glGenVertexArrays glGenVertexArrays{}; + PFN_glDeleteVertexArrays glDeleteVertexArrays{}; + PFN_glBindVertexArray glBindVertexArray{}; + PFN_glGenBuffers glGenBuffers{}; + PFN_glDeleteBuffers glDeleteBuffers{}; + PFN_glBindBuffer glBindBuffer{}; + PFN_glBufferData glBufferData{}; + PFN_glEnableVertexAttribArray glEnableVertexAttribArray{}; + PFN_glVertexAttribPointer glVertexAttribPointer{}; + PFN_glBlendFuncSeparate glBlendFuncSeparate{}; PFN_glIsProgram glIsProgram{}; PFN_glShaderSourceARB glShaderSourceARB{}; PFN_glCompileShaderARB glCompileShaderARB{}; @@ -165,6 +254,9 @@ struct OpenGLFunctions { bool arbShaderObjectsAvailable{false}; bool textureUploadAvailable{false}; bool compressedTextureUploadAvailable{false}; + // Everything the world post passes need: framebuffer objects, a vertex + // array, shaders with introspection, and the fixed-function state calls. + bool postProcessAvailable{false}; bool initialize() noexcept; }; From f200e3801497fc954b276de23fd2a7542051a976 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Wed, 7 Oct 2026 21:09:20 +0200 Subject: [PATCH 38/57] World post: soft fog of war and threshold bloom behind the RenderFog hook --- CMakeLists.txt | 1 + src/iee/core/gl_pass_guard.h | 7 + src/iee/features/world_post.cpp | 572 ++++++++++++++++++++++++++++++++ src/iee/features/world_post.h | 32 ++ src/iee/game/opengl_types.h | 2 + src/iee/hooks.cpp | 54 +++ 6 files changed, 668 insertions(+) create mode 100644 src/iee/features/world_post.cpp create mode 100644 src/iee/features/world_post.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 83c60eb..6fb0de3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,6 +150,7 @@ if(IEE_BUILD_WINDOWS_DLL) src/iee/dll_main.cpp src/iee/exports.cpp src/iee/features/tile_render.cpp + src/iee/features/world_post.cpp src/iee/frame_hook.cpp src/iee/hooks.cpp src/iee/shader_diagnostics.cpp diff --git a/src/iee/core/gl_pass_guard.h b/src/iee/core/gl_pass_guard.h index cc40d89..b228b7b 100644 --- a/src/iee/core/gl_pass_guard.h +++ b/src/iee/core/gl_pass_guard.h @@ -6,6 +6,7 @@ namespace iee::core { // Saves the GL state our own draw passes change and puts it back on scope // exit. The engine issues state changes relative to what it believes the // hardware state is, so anything left altered corrupts its next draws. +// Covers program, vertex array, viewport, blending and the per-fragment tests. // Framebuffer and texture bindings are not covered: the caller owns the // framebuffer hand-over, and GlStateGuard covers texture units. // Requires OpenGLFunctions::postProcessAvailable. @@ -26,6 +27,8 @@ class GlPassGuard { scissor_ = gl.glIsEnabled(game::gl::SCISSOR_TEST) != 0; depthTest_ = gl.glIsEnabled(game::gl::DEPTH_TEST) != 0; cullFace_ = gl.glIsEnabled(game::gl::CULL_FACE) != 0; + stencilTest_ = gl.glIsEnabled(game::gl::STENCIL_TEST) != 0; + alphaTest_ = gl.glIsEnabled(game::gl::ALPHA_TEST) != 0; } ~GlPassGuard() noexcept { @@ -41,6 +44,8 @@ class GlPassGuard { set(game::gl::SCISSOR_TEST, scissor_); set(game::gl::DEPTH_TEST, depthTest_); set(game::gl::CULL_FACE, cullFace_); + set(game::gl::STENCIL_TEST, stencilTest_); + set(game::gl::ALPHA_TEST, alphaTest_); gl.glBlendFuncSeparate( static_cast(blendSrcRgb_), static_cast(blendDstRgb_), static_cast(blendSrcAlpha_), static_cast(blendDstAlpha_)); @@ -71,5 +76,7 @@ class GlPassGuard { bool scissor_{}; bool depthTest_{}; bool cullFace_{}; + bool stencilTest_{}; + bool alphaTest_{}; }; } // namespace iee::core diff --git a/src/iee/features/world_post.cpp b/src/iee/features/world_post.cpp new file mode 100644 index 0000000..b0a471b --- /dev/null +++ b/src/iee/features/world_post.cpp @@ -0,0 +1,572 @@ +#include "iee/features/world_post.h" + +#include + +#include +#include +#include + +#include "iee/core/config.h" +#include "iee/core/gl_pass_guard.h" +#include "iee/core/gl_state_guard.h" +#include "iee/core/logger.h" +#include "iee/game/opengl_types.h" +#include "iee/game/world_post_plan.h" + +namespace iee::features { +namespace { +namespace gl = game::gl; + +struct Target { + unsigned texture{}; + unsigned framebuffer{}; + game::Extent extent{}; +}; + +struct CompositeProgram { + unsigned id{}; + int scale{-1}; + int dither{-1}; +}; + +struct BlurProgram { + unsigned id{}; + int step{-1}; +}; + +struct BrightProgram { + unsigned id{}; + int threshold{-1}; +}; + +// Level i is the base image halved i + 1 times. +struct BlurChain { + std::array levels{}; + int count{}; +}; + +struct Settings { + bool softFog{}; + bool bloom{}; + float fogRadius{}; + float bloomThreshold{}; + float bloomStrength{}; +}; + +// Everything that lives and dies with one GL context. +struct Resources { + HGLRC context{}; + game::Extent viewport{}; + unsigned vertexArray{}; + unsigned vertexBuffer{}; + CompositeProgram composite{}; + BlurProgram blurDown{}; + BlurProgram blurUp{}; + BrightProgram bright{}; + Target fogCapture{}; + BlurChain fogChain{}; + Target bloomScene{}; // half-size copy of the world + Target bloomBright{}; // its bright parts + BlurChain bloomChain{}; + bool fogDrawnOnce{}; + bool bloomDrawnOnce{}; +}; + +Settings g_settings; +Resources g_resources; +bool g_failed = false; +int g_engineDrawFramebuffer = 0; +int g_engineReadFramebuffer = 0; +float g_pixelsPerWorldPixel = 1.0f; +std::atomic g_logNextFrame{true}; + +constexpr const char* kVertexSource = R"glsl(#version 330 +layout(location = 0) in vec2 aPosition; +out vec2 vUv; +void main() { + vUv = aPosition * 0.5 + 0.5; + gl_Position = vec4(aPosition, 0.0, 1.0); +} +)glsl"; + +// Writes the texture scaled by uScale; the caller's blend mode decides whether +// that multiplies (fog) or adds (bloom). uDither adds +-0.5/255 of ordered +// noise to hide banding in long dark gradients. +constexpr const char* kCompositeSource = R"glsl(#version 330 +uniform sampler2D uTexture; +uniform float uScale; +uniform float uDither; +in vec2 vUv; +out vec4 fragColor; +void main() { + vec3 color = texture(uTexture, vUv).rgb * uScale; + float noise = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))); + fragColor = vec4(color + uDither * (noise - 0.5) / 255.0, 1.0); +} +)glsl"; + +// Dual-filter blur (down then up through half-size levels). uStep is the +// sample spread in source UV units. +constexpr const char* kBlurDownSource = R"glsl(#version 330 +uniform sampler2D uTexture; +uniform vec2 uStep; +in vec2 vUv; +out vec4 fragColor; +void main() { + vec4 sum = texture(uTexture, vUv) * 4.0; + sum += texture(uTexture, vUv - uStep); + sum += texture(uTexture, vUv + uStep); + sum += texture(uTexture, vUv + vec2(uStep.x, -uStep.y)); + sum += texture(uTexture, vUv - vec2(uStep.x, -uStep.y)); + fragColor = sum / 8.0; +} +)glsl"; + +constexpr const char* kBlurUpSource = R"glsl(#version 330 +uniform sampler2D uTexture; +uniform vec2 uStep; +in vec2 vUv; +out vec4 fragColor; +void main() { + vec4 sum = texture(uTexture, vUv + vec2(-uStep.x * 2.0, 0.0)); + sum += texture(uTexture, vUv + vec2(-uStep.x, uStep.y)) * 2.0; + sum += texture(uTexture, vUv + vec2(0.0, uStep.y * 2.0)); + sum += texture(uTexture, vUv + vec2(uStep.x, uStep.y)) * 2.0; + sum += texture(uTexture, vUv + vec2(uStep.x * 2.0, 0.0)); + sum += texture(uTexture, vUv + vec2(uStep.x, -uStep.y)) * 2.0; + sum += texture(uTexture, vUv + vec2(0.0, -uStep.y * 2.0)); + sum += texture(uTexture, vUv + vec2(-uStep.x, -uStep.y)) * 2.0; + fragColor = sum / 12.0; +} +)glsl"; + +// Keeps what is brighter than uThreshold, with a soft knee so the cut-off +// does not flicker. +constexpr const char* kBrightSource = R"glsl(#version 330 +uniform sampler2D uTexture; +uniform float uThreshold; +in vec2 vUv; +out vec4 fragColor; +void main() { + vec3 color = texture(uTexture, vUv).rgb; + float level = max(color.r, max(color.g, color.b)); + const float knee = 0.1; + float soft = clamp(level - uThreshold + knee, 0.0, 2.0 * knee); + soft = soft * soft / (4.0 * knee); + float weight = max(soft, level - uThreshold) / max(level, 1e-4); + fragColor = vec4(color * weight, 1.0); +} +)glsl"; + +void fail(const char* reason) noexcept { + g_failed = true; + try { + LOG_WARN("Soft fog of war and bloom disabled for this session: {}", reason); + } catch (...) { + } +} + +unsigned compile_shader(unsigned type, const char* source) noexcept { + const auto& fn = gl::get_gl_functions(); + const unsigned shader = fn.glCreateShader(type); + if (!shader) return 0; + fn.glShaderSource(shader, 1, &source, nullptr); + fn.glCompileShader(shader); + int status = 0; + fn.glGetShaderiv(shader, gl::COMPILE_STATUS, &status); + if (!status) { + char log[512]{}; + fn.glGetShaderInfoLog(shader, static_cast(sizeof(log)) - 1, nullptr, log); + try { + LOG_WARN("World post shader compile failed: {}", log); + } catch (...) { + } + fn.glDeleteShader(shader); + return 0; + } + return shader; +} + +unsigned build_program(const char* fragmentSource) noexcept { + const auto& fn = gl::get_gl_functions(); + const unsigned vertex = compile_shader(gl::VERTEX_SHADER, kVertexSource); + const unsigned fragment = compile_shader(gl::FRAGMENT_SHADER, fragmentSource); + unsigned program = 0; + if (vertex && fragment) { + program = fn.glCreateProgram(); + fn.glAttachShader(program, vertex); + fn.glAttachShader(program, fragment); + fn.glLinkProgram(program); + int status = 0; + fn.glGetProgramiv(program, gl::LINK_STATUS, &status); + if (!status) { + char log[512]{}; + fn.glGetProgramInfoLog(program, static_cast(sizeof(log)) - 1, nullptr, log); + try { + LOG_WARN("World post program link failed: {}", log); + } catch (...) { + } + fn.glDeleteProgram(program); + program = 0; + } + } + if (vertex) fn.glDeleteShader(vertex); + if (fragment) fn.glDeleteShader(fragment); + if (program) { + fn.glUseProgram(program); + fn.glUniform1i(fn.glGetUniformLocation(program, "uTexture"), 0); + } + return program; +} + +void destroy_target(Target& target) noexcept { + const auto& fn = gl::get_gl_functions(); + if (target.framebuffer) fn.glDeleteFramebuffers(1, &target.framebuffer); + if (target.texture) fn.glDeleteTextures(1, &target.texture); + target = {}; +} + +// Leaves the new framebuffer and texture bound; the caller's guards restore. +bool create_target(Target& target, game::Extent extent, unsigned internalFormat, + unsigned type) noexcept { + const auto& fn = gl::get_gl_functions(); + destroy_target(target); + fn.glGenTextures(1, &target.texture); + fn.glBindTexture(gl::TEXTURE_2D, target.texture); + fn.glTexImage2D(gl::TEXTURE_2D, 0, static_cast(internalFormat), extent.width, + extent.height, 0, gl::RGBA, type, nullptr); + fn.glTexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, static_cast(gl::LINEAR)); + fn.glTexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, static_cast(gl::LINEAR)); + fn.glTexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, static_cast(gl::CLAMP_TO_EDGE)); + fn.glTexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, static_cast(gl::CLAMP_TO_EDGE)); + fn.glGenFramebuffers(1, &target.framebuffer); + fn.glBindFramebuffer(gl::FRAMEBUFFER, target.framebuffer); + fn.glFramebufferTexture2D(gl::FRAMEBUFFER, gl::COLOR_ATTACHMENT0, gl::TEXTURE_2D, + target.texture, 0); + if (fn.glCheckFramebufferStatus(gl::FRAMEBUFFER) != gl::FRAMEBUFFER_COMPLETE) { + destroy_target(target); + return false; + } + target.extent = extent; + return true; +} + +void destroy_chain(BlurChain& chain) noexcept { + for (auto& level : chain.levels) destroy_target(level); + chain.count = 0; +} + +bool create_chain(BlurChain& chain, game::Extent base) noexcept { + destroy_chain(chain); + const int count = game::available_blur_levels(base); + game::Extent extent = base; + for (int index = 0; index < count; ++index) { + extent = game::half_extent(extent); + if (!create_target(chain.levels[static_cast(index)], extent, gl::RGBA16F, + gl::HALF_FLOAT)) { + destroy_chain(chain); + return false; + } + } + chain.count = count; + return true; +} + +void bind_engine_framebuffer() noexcept { + const auto& fn = gl::get_gl_functions(); + fn.glBindFramebuffer(gl::DRAW_FRAMEBUFFER, static_cast(g_engineDrawFramebuffer)); + fn.glBindFramebuffer(gl::READ_FRAMEBUFFER, static_cast(g_engineReadFramebuffer)); +} + +// State every pass of ours draws with. The guards in scope restore it. +void set_pass_state() noexcept { + const auto& fn = gl::get_gl_functions(); + fn.glDisable(gl::SCISSOR_TEST); + fn.glDisable(gl::DEPTH_TEST); + fn.glDisable(gl::CULL_FACE); + fn.glDisable(gl::STENCIL_TEST); + fn.glDisable(gl::ALPHA_TEST); + fn.glDisable(gl::BLEND); + fn.glBindVertexArray(g_resources.vertexArray); + fn.glActiveTexture(gl::TEXTURE0); +} + +void draw_composite(unsigned texture, float scale, float dither) noexcept { + const auto& fn = gl::get_gl_functions(); + fn.glUseProgram(g_resources.composite.id); + fn.glUniform1f(g_resources.composite.scale, scale); + fn.glUniform1f(g_resources.composite.dither, dither); + fn.glBindTexture(gl::TEXTURE_2D, texture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); +} + +void blur_pass(const BlurProgram& program, const Target& source, const Target& destination, + float stepTexels) noexcept { + const auto& fn = gl::get_gl_functions(); + fn.glBindFramebuffer(gl::FRAMEBUFFER, destination.framebuffer); + fn.glViewport(0, 0, destination.extent.width, destination.extent.height); + fn.glUseProgram(program.id); + fn.glUniform2f(program.step, stepTexels / static_cast(source.extent.width), + stepTexels / static_cast(source.extent.height)); + fn.glBindTexture(gl::TEXTURE_2D, source.texture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); +} + +// Blurs `source` into chain.levels[0]. With `accumulate`, each level keeps its +// own downsampled content and the wider blur is added on top (bloom); without +// it the result is a plain blur (fog). Returns the number of levels used. +// Requires set_pass_state(); leaves blending off. +int run_blur(const Target& source, BlurChain& chain, game::BlurPlan plan, + bool accumulate) noexcept { + const auto& fn = gl::get_gl_functions(); + const int levels = plan.levels < chain.count ? plan.levels : chain.count; + if (levels <= 0) return 0; + fn.glDisable(gl::BLEND); + const Target* from = &source; + for (int index = 0; index < levels; ++index) { + const auto& to = chain.levels[static_cast(index)]; + blur_pass(g_resources.blurDown, *from, to, plan.offset); + from = &to; + } + if (accumulate) { + fn.glEnable(gl::BLEND); + fn.glBlendFunc(gl::ONE, gl::ONE); + } + for (int index = levels - 1; index > 0; --index) { + blur_pass(g_resources.blurUp, chain.levels[static_cast(index)], + chain.levels[static_cast(index - 1)], plan.offset * 0.5f); + } + fn.glDisable(gl::BLEND); + return levels; +} + +bool create_shared_resources() noexcept { + const auto& fn = gl::get_gl_functions(); + static constexpr float kTriangle[] = {-1.0f, -1.0f, 3.0f, -1.0f, -1.0f, 3.0f}; + fn.glGenVertexArrays(1, &g_resources.vertexArray); + fn.glGenBuffers(1, &g_resources.vertexBuffer); + fn.glBindVertexArray(g_resources.vertexArray); + fn.glBindBuffer(gl::ARRAY_BUFFER, g_resources.vertexBuffer); + fn.glBufferData(gl::ARRAY_BUFFER, static_cast(sizeof(kTriangle)), kTriangle, + gl::STATIC_DRAW); + fn.glEnableVertexAttribArray(0); + fn.glVertexAttribPointer(0, 2, gl::FLOAT, 0, 0, nullptr); + + auto& resources = g_resources; + resources.composite.id = build_program(kCompositeSource); + resources.blurDown.id = build_program(kBlurDownSource); + resources.blurUp.id = build_program(kBlurUpSource); + resources.bright.id = build_program(kBrightSource); + if (!resources.composite.id || !resources.blurDown.id || !resources.blurUp.id || + !resources.bright.id) { + return false; + } + resources.composite.scale = fn.glGetUniformLocation(resources.composite.id, "uScale"); + resources.composite.dither = fn.glGetUniformLocation(resources.composite.id, "uDither"); + resources.blurDown.step = fn.glGetUniformLocation(resources.blurDown.id, "uStep"); + resources.blurUp.step = fn.glGetUniformLocation(resources.blurUp.id, "uStep"); + resources.bright.threshold = fn.glGetUniformLocation(resources.bright.id, "uThreshold"); + return true; +} + +bool create_sized_resources(game::Extent viewport) noexcept { + auto& resources = g_resources; + const auto half = game::half_extent(viewport); + if (!create_target(resources.fogCapture, viewport, gl::RGBA8, gl::UNSIGNED_BYTE)) return false; + if (!create_chain(resources.fogChain, viewport)) return false; + if (!create_target(resources.bloomScene, half, gl::RGBA16F, gl::HALF_FLOAT)) return false; + if (!create_target(resources.bloomBright, half, gl::RGBA16F, gl::HALF_FLOAT)) return false; + if (!create_chain(resources.bloomChain, half)) return false; + resources.viewport = viewport; + return true; +} + +// Called inside the guards' scope: creation changes bindings. +bool ensure_resources(game::Extent viewport) noexcept { + const auto context = gl::current_context(); + // Creation binds textures; keep that on unit 0, which the caller's guard restores. + gl::get_gl_functions().glActiveTexture(gl::TEXTURE0); + if (context != g_resources.context) { + // The old context is gone and took its objects with it. + g_resources = {}; + g_resources.context = context; + gl::discard_errors(); + if (!create_shared_resources() || !gl::check_error("world post shared resources")) { + return false; + } + } + if (!(viewport == g_resources.viewport)) { + gl::discard_errors(); + if (!create_sized_resources(viewport) || !gl::check_error("world post targets")) return false; + g_logNextFrame.store(true, std::memory_order_relaxed); + } + return true; +} + +// Adds a glow of the bright parts of the world image to the engine's +// framebuffer. Runs before the fog so unexplored areas stay dark, and before +// the UI is drawn so the UI cannot glow. Requires set_pass_state(). +void draw_bloom() noexcept { + const auto& fn = gl::get_gl_functions(); + auto& resources = g_resources; + const auto half = resources.bloomScene.extent; + const int planned = game::bloom_levels(half); + if (planned <= 0 || !(g_settings.bloomStrength > 0.0f)) return; + + fn.glBindFramebuffer(gl::READ_FRAMEBUFFER, static_cast(g_engineDrawFramebuffer)); + fn.glBindFramebuffer(gl::DRAW_FRAMEBUFFER, resources.bloomScene.framebuffer); + fn.glBlitFramebuffer(0, 0, resources.viewport.width, resources.viewport.height, 0, 0, + half.width, half.height, gl::COLOR_BUFFER_BIT, gl::LINEAR); + + fn.glBindFramebuffer(gl::FRAMEBUFFER, resources.bloomBright.framebuffer); + fn.glViewport(0, 0, half.width, half.height); + fn.glUseProgram(resources.bright.id); + fn.glUniform1f(resources.bright.threshold, g_settings.bloomThreshold); + fn.glBindTexture(gl::TEXTURE_2D, resources.bloomScene.texture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); + + const int levels = run_blur(resources.bloomBright, resources.bloomChain, {planned, 1.0f}, true); + + bind_engine_framebuffer(); + fn.glViewport(0, 0, resources.viewport.width, resources.viewport.height); + if (levels > 0) { + fn.glEnable(gl::BLEND); + fn.glBlendFunc(gl::ONE, gl::ONE); + // Each level adds its share on the way up; normalise so strength means + // the same thing at every resolution. + draw_composite(resources.bloomChain.levels[0].texture, + g_settings.bloomStrength / static_cast(levels), 0.0f); + fn.glDisable(gl::BLEND); + } + + if (!resources.bloomDrawnOnce) { + resources.bloomDrawnOnce = true; + if (!gl::check_error("world post bloom")) { + fail("the bloom pass raised a GL error"); + } else { + try { + LOG_INFO("World post: first bloom drawn ({} levels)", levels); + } catch (...) { + } + } + } +} +} // namespace + +void world_post_configure(const core::EngineConfig& cfg) noexcept { + g_settings = {cfg.softFogOfWar, cfg.bloom, cfg.softFogRadius, cfg.bloomThreshold, + cfg.bloomStrength}; +} + +bool world_post_active() noexcept { + return !g_failed && (g_settings.softFog || g_settings.bloom); +} + +void world_post_poll_hotkeys() noexcept { + static bool fogKeyWasDown = false; + static bool bloomKeyWasDown = false; + const bool fogKeyDown = (GetAsyncKeyState(VK_F8) & 0x8000) != 0; + const bool bloomKeyDown = (GetAsyncKeyState(VK_F9) & 0x8000) != 0; + const bool fogPressed = fogKeyDown && !fogKeyWasDown; + const bool bloomPressed = bloomKeyDown && !bloomKeyWasDown; + fogKeyWasDown = fogKeyDown; + bloomKeyWasDown = bloomKeyDown; + if (!fogPressed && !bloomPressed) return; + if (fogPressed) g_settings.softFog = !g_settings.softFog; + if (bloomPressed) g_settings.bloom = !g_settings.bloom; + try { + LOG_INFO("Hotkey: soft fog of war={}, bloom={}", g_settings.softFog, g_settings.bloom); + } catch (...) { + } +} + +bool world_post_before_fog(float viewWorldWidth) noexcept { + const auto& fn = gl::get_gl_functions(); + if (!fn.postProcessAvailable) { + fail("the GL context lacks framebuffer or vertex-array support"); + return false; + } + fn.glGetIntegerv(gl::DRAW_FRAMEBUFFER_BINDING, &g_engineDrawFramebuffer); + fn.glGetIntegerv(gl::READ_FRAMEBUFFER_BINDING, &g_engineReadFramebuffer); + + bool capturing = false; + { + core::GlStateGuard textures({0}); + core::GlPassGuard pass; + const int* viewport = pass.viewport(); + if (viewport[0] != 0 || viewport[1] != 0 || viewport[2] <= 0 || viewport[3] <= 0) { + fail("the engine viewport is not a full-window rectangle at the origin"); + } else if (!ensure_resources({viewport[2], viewport[3]})) { + fail("GL resources could not be created"); + } else { + g_pixelsPerWorldPixel = + game::pixels_per_world_pixel(static_cast(viewport[2]), viewWorldWidth); + set_pass_state(); + if (g_settings.bloom) { + draw_bloom(); + set_pass_state(); + } + if (g_settings.softFog && !g_failed) { + // White = "nothing hidden". The engine's alpha-blended black fog then + // leaves, per pixel, the fraction of the scene that stays visible. + fn.glBindFramebuffer(gl::FRAMEBUFFER, g_resources.fogCapture.framebuffer); + fn.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + fn.glClear(gl::COLOR_BUFFER_BIT); + capturing = true; + } + if (g_logNextFrame.exchange(false, std::memory_order_relaxed)) { + const auto plan = game::blur_plan_for_radius( + g_settings.fogRadius * g_pixelsPerWorldPixel, g_resources.viewport); + try { + LOG_INFO( + "World post: softFog={} (radius {} world px, blur levels={} offset={:.2f}), " + "bloom={}, viewport {}x{}, {:.3f} px per world px, engine framebuffer draw={} " + "read={}", + g_settings.softFog, g_settings.fogRadius, plan.levels, plan.offset, + g_settings.bloom, viewport[2], viewport[3], g_pixelsPerWorldPixel, + g_engineDrawFramebuffer, g_engineReadFramebuffer); + } catch (...) { + } + } + } + bind_engine_framebuffer(); + } + // The guards have restored the engine's state; hand it our target for the + // fog draw only. Framebuffer bindings are not part of the engine's cache. + if (capturing) fn.glBindFramebuffer(gl::FRAMEBUFFER, g_resources.fogCapture.framebuffer); + return capturing; +} + +void world_post_after_fog() noexcept { + const auto& fn = gl::get_gl_functions(); + auto& resources = g_resources; + core::GlStateGuard textures({0}); + core::GlPassGuard pass; + set_pass_state(); + const auto plan = + game::blur_plan_for_radius(g_settings.fogRadius * g_pixelsPerWorldPixel, resources.viewport); + const bool blurred = run_blur(resources.fogCapture, resources.fogChain, plan, false) > 0; + bind_engine_framebuffer(); + fn.glViewport(0, 0, resources.viewport.width, resources.viewport.height); + // framebuffer = framebuffer * visible fraction + fn.glEnable(gl::BLEND); + fn.glBlendFunc(gl::DST_COLOR, gl::ZERO); + draw_composite(blurred ? resources.fogChain.levels[0].texture : resources.fogCapture.texture, + 1.0f, blurred ? 1.0f : 0.0f); + if (!resources.fogDrawnOnce) { + resources.fogDrawnOnce = true; + if (!gl::check_error("world post fog composite")) { + fail("the fog composite raised a GL error"); + } else { + try { + LOG_INFO("World post: first fog composite drawn (blurred={})", blurred); + } catch (...) { + } + } + } +} + +void world_post_on_area_load() noexcept { g_logNextFrame.store(true, std::memory_order_relaxed); } + +void world_post_forget() noexcept { g_resources = {}; } +} // namespace iee::features diff --git a/src/iee/features/world_post.h b/src/iee/features/world_post.h new file mode 100644 index 0000000..95385c4 --- /dev/null +++ b/src/iee/features/world_post.h @@ -0,0 +1,32 @@ +#pragma once + +namespace iee::core { +struct EngineConfig; +} + +namespace iee::features { +// World post passes: soft fog of war and bloom, run from the +// CInfinity::RenderFog detour. The engine queues the whole frame and only +// draws in DrawFlush_GL, so the caller flushes that queue around these calls: +// +// flush(); // world is now in the framebuffer +// bool capturing = world_post_before_fog(); // bloom; fog target bound if true +// original RenderFog // queues the fog triangles +// if (capturing) { flush(); world_post_after_fog(); } +// +// Render thread only, except world_post_on_area_load. + +void world_post_configure(const core::EngineConfig& cfg) noexcept; +// True when at least one effect is switched on and no pass has failed. +bool world_post_active() noexcept; +// Debug A/B: F8 toggles soft fog of war, F9 toggles bloom. Call once per +// frame, only when debug hotkeys are enabled. +void world_post_poll_hotkeys() noexcept; +// viewWorldWidth: world pixels visible across the view (0 when unknown). +bool world_post_before_fog(float viewWorldWidth) noexcept; +void world_post_after_fog() noexcept; +// Any thread. The next frame logs what engaged. +void world_post_on_area_load() noexcept; +// Drops every GL handle without touching GL. For shutdown paths. +void world_post_forget() noexcept; +} // namespace iee::features diff --git a/src/iee/game/opengl_types.h b/src/iee/game/opengl_types.h index fcccdfc..6fe9665 100644 --- a/src/iee/game/opengl_types.h +++ b/src/iee/game/opengl_types.h @@ -49,6 +49,8 @@ constexpr unsigned SCISSOR_TEST = 0x0C11; constexpr unsigned BLEND = 0x0BE2; constexpr unsigned DEPTH_TEST = 0x0B71; constexpr unsigned CULL_FACE = 0x0B44; +constexpr unsigned STENCIL_TEST = 0x0B90; +constexpr unsigned ALPHA_TEST = 0x0BC0; constexpr unsigned BLEND_DST_RGB = 0x80C8; constexpr unsigned BLEND_SRC_RGB = 0x80C9; constexpr unsigned BLEND_DST_ALPHA = 0x80CA; diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index 31f1ad6..608c720 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -19,6 +19,7 @@ #include "iee/core/pattern_scanner.h" #include "iee/core/performance_samples.h" #include "iee/features/tile_render.h" +#include "iee/features/world_post.h" #include "iee/frame_hook.h" #include "iee/game/game_types.h" #include "iee/game/animation_interp.h" @@ -54,6 +55,12 @@ using VidCellAccessorFn = int (*)(void*, void*); static core::Hook g_vidCellGetFrameHook; static core::Hook g_vidCellCenterPointHook; static core::Hook g_vidCellFrameSizeHook; +// CInfinity::RenderFog(CVidMode*, CVisibilityMap*) and the GL backend's +// argument-less queue flush, used together by the world post passes. +using RenderFogFn = void (*)(void*, void*, void*); +using DrawFlushFn = void (*)(); +static core::Hook g_renderFogHook; +static DrawFlushFn g_drawFlush = nullptr; static AppContext* g_ctx = nullptr; // Raised by LoadArea; the sprite-smoothing tracker clears itself on its own thread. @@ -262,6 +269,7 @@ static void* detour_load_area(void* thisPtr, void* pAreaNameString, unsigned cha ctx.reset_area_state(); features::request_tile_render_state_reset(); g_spriteMotionReset.store(true, std::memory_order_release); + features::world_post_on_area_load(); game::request_texture_configuration_cache_reset(); } catch (const std::exception& e) { LOG_ERROR("LoadArea pre-dispatch failed; continuing with the engine path: {}", e.what()); @@ -431,6 +439,32 @@ static constexpr std::array object_render_deto static constexpr auto kObjectRenderDetours = object_render_detours(std::make_index_sequence{}); +// World post passes (soft fog of war, bloom). The engine queues every draw +// and submits the queue in DrawFlush_GL at the end of the frame, so the world +// only exists in the framebuffer once we flush, and the fog only lands in our +// target if we flush again while it is bound. With both effects off this +// detour is a plain pass-through: no flush, the frame is the engine's own. +static void detour_render_fog(void* infinity, void* vidMode, void* visibility) { + const auto original = g_renderFogHook.original(); + if (g_ctx && g_ctx->cfg.enableDebugHotkeys) features::world_post_poll_hotkeys(); + if (!g_ctx || !g_drawFlush || !features::world_post_active()) { + original(infinity, vidMode, visibility); + return; + } + float viewWorldWidth = 0.0f; + if (const auto* activeArea = g_ctx->activeArea.load()) { + area::ViewTransform view{}; + if (area::read_view_transform(activeArea, view)) viewWorldWidth = view.viewWorldW; + } + g_drawFlush(); + const bool capturing = features::world_post_before_fog(viewWorldWidth); + original(infinity, vidMode, visibility); + if (capturing) { + g_drawFlush(); + features::world_post_after_fog(); + } +} + // CGameStatic::Render hook: while the fpSEAM point effects are active, the // authored fire/smoke BAM draws are replaced by our textured effects, so the // engine's own little flame/puff loops are skipped. Everything else (lights, @@ -625,6 +659,22 @@ bool install_all(AppContext& ctx) { LOG_INFO("Object movement smoothing hooks installed: {}", installed); } + if (ctx.addrs.RenderFog && ctx.addrs.DrawFlush) { + try { + features::world_post_configure(ctx.cfg); + g_drawFlush = reinterpret_cast(ctx.addrs.DrawFlush); + g_renderFogHook.create(reinterpret_cast(ctx.addrs.RenderFog), + reinterpret_cast(&detour_render_fog)); + g_renderFogHook.enable(); + LOG_INFO("CInfinity::RenderFog hook installed (soft fog of war={}, bloom={})", + ctx.cfg.softFogOfWar, ctx.cfg.bloom); + } catch (...) { + g_drawFlush = nullptr; + (void)g_renderFogHook.remove(); + LOG_WARN("CInfinity::RenderFog hook failed; fog of war and bloom stay vanilla"); + } + } + g_loadAreaHook.enable(); LOG_INFO("LoadArea hook enabled"); @@ -666,6 +716,9 @@ void uninstall_all() noexcept { } catch (...) { } + (void)g_renderFogHook.remove(); + g_drawFlush = nullptr; + features::world_post_forget(); g_animationInterpActive = false; (void)g_vidCellFrameSizeHook.remove(); (void)g_vidCellCenterPointHook.remove(); @@ -694,6 +747,7 @@ void prepare_for_shutdown() noexcept { // state are torn down. MinHook itself stays initialized until // uninstall_all(), after every MinHook-backed subsystem has removed its // hooks. + (void)g_renderFogHook.disable(); (void)g_vidCellFrameSizeHook.disable(); (void)g_vidCellCenterPointHook.disable(); (void)g_vidCellGetFrameHook.disable(); From e021baa173a2d1cd9eb0b515ecb8813446658c7f Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Wed, 7 Oct 2026 21:09:47 +0200 Subject: [PATCH 39/57] docs: record the engine draw queue and the soft fog/bloom status --- CLAUDE.md | 2 ++ ...026-06-10-graphics-enhancement-roadmap-design.md | 3 +++ .../2026-10-03-graphics-uplift-roadmap-design.md | 13 ++++++++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 44ccc11..7019472 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,6 +44,8 @@ Operational instructions for AI coding agents working in this repository. - The engine compiles nine named GLSL programs (`fpSEAM`, `fpSprite`, `fpSELECT`, `fpCatRom`, ...). The verified slot table and shader-replacement strategy live in the graphics roadmap spec (see Docs). - The engine does **zero** lighting work. Day/night is a swap of authored assets. `CGameArea` holds authored per-area bitmaps (`m_bmLum` +0x260, `m_pbmLumNight` +0x380, `m_bmHeight` +0x388) usable as static lookup data only. - Prefer `SDL_GL_SwapWindow` (SDL2.dll export) over `DrawFlip` patterns for the frame boundary. +- The engine queues every draw (`DrawEnd_GL` only appends to `gl.cmds`) and submits the whole frame in `DrawFlush_GL` (2.7.3 RVA `0x42B350`), normally once at flip, and earlier whenever its 1024x1024 sprite atlas fills. Binding a framebuffer around an engine render function captures nothing, and an immediate GL draw of ours lands under the still-queued world. Flush first (see `features/world_post.*`), and restore every GL state touched: the flush issues changes relative to the engine's cached `gl.hwState`. +- `gl.pp.enabled` is forced false in 2.7.3 (`DrawInit_GL`): the engine's own offscreen target and `fpCatRom` are dead. ## Runtime Facts — Hooking Safety diff --git a/docs/superpowers/specs/2026-06-10-graphics-enhancement-roadmap-design.md b/docs/superpowers/specs/2026-06-10-graphics-enhancement-roadmap-design.md index 2b41559..209b11d 100644 --- a/docs/superpowers/specs/2026-06-10-graphics-enhancement-roadmap-design.md +++ b/docs/superpowers/specs/2026-06-10-graphics-enhancement-roadmap-design.md @@ -310,6 +310,9 @@ New infrastructure: ### Phase 4 — Fog (P3) +> Superseded by `2026-10-07-soft-fog-and-bloom-design.md`: the engine queues +> its draws, so an FBO bracket around `RenderFog` captures nothing. + - Bracket architecture: pre-`RenderFog` bind an R8 FBO sized to the viewport; call the original untouched (`BltFogOWar3d`/`DrawVisible` render into our target); post: separable blur (radius scaled by `m_fZoom`), composite over the diff --git a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md index 9e4fcc5..2d64a65 100644 --- a/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md +++ b/docs/superpowers/specs/2026-10-03-graphics-uplift-roadmap-design.md @@ -191,6 +191,12 @@ Owner decision 2026-10-03: stay in `fpSEAM` for now and fix placement there. ### D. Offscreen world pass +D1 and D2 are implemented (2026-10-07) behind `[Rendering] SoftFogOfWar` and +`Bloom`, default off, 2.7.3 only; in-game validation pending. Design and the +engine draw-queue evidence: `2026-10-07-soft-fog-and-bloom-design.md`. The +description below predates that evidence: the engine queues its draws, so +D1 flushes the queue and captures the fog rather than bracketing it. + - D1 soft fog of war: render `RenderFog` into an offscreen target, blur (radius scaled by zoom), composite. - D2 bloom on the world pass only. @@ -276,13 +282,14 @@ Revised 2026-10-04 after A and F's engine side were confirmed in game. | Phase | Items | Why here | |---|---|---| -| 1 | F: in-betweens-only pack with source fingerprints, in-memory merge, standalone generator, vanilla pack as its own mod; narrow the all-cells rule to frame-synced cells | The owner's favourite feature currently depends on hand-generated whole files | +| paused | F: in-betweens-only pack with source fingerprints, in-memory merge, standalone generator, vanilla pack as its own mod; narrow the all-cells rule to frame-synced cells | The owner's favourite feature currently depends on hand-generated whole files | | 2 | B1, B2 | Sprites are the most visible mismatch against 4x maps | | 3 | C1 confirmation, C2 (blood first), C3 | Effects plus the light they cast | -| 4 | D1, D2 | Shared offscreen plumbing | +| done (unvalidated) | D1, D2 | Built first by owner decision 2026-10-07: larger visual gain than the F pack | | Later | B3, B4, E1 -> E2 -> E3, C4, D3, G | Polish and the large asset-driven items | -Done: A (movement smoothing), F's draw-time swap. +Done: A (movement smoothing), F's draw-time swap. Owner decision 2026-10-07: +the F pack work is paused; effect-cast light (C3) follows D1/D2. ## 4. Reversed decisions From 5d89d50b6bc43d28dfda145a60833977699ab824 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Wed, 7 Oct 2026 21:14:03 +0200 Subject: [PATCH 40/57] docs: state the exact GL state the world post passes restore --- .../2026-10-07-soft-fog-and-bloom-design.md | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md index cc3b3c8..b3f36fb 100644 --- a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md +++ b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md @@ -171,14 +171,18 @@ Radius 0 skips step 2 and composites the capture directly. The engine believes the hardware state equals `gl.hwState` after a flush and only issues changes relative to it. Everything our passes change must be put -back exactly. `GlPassGuard` saves and restores: draw and read framebuffer, -viewport, scissor box and enable, blend enable and function, depth test and -depth mask, colour mask, cull face, current program, active texture and -client active texture, the 2D binding and enable on units 0 and 1, vertex -array object and array buffer binding, clear colour. - -The engine's texture-environment setup on unit 1 is not touched (we only use -our own programs and rebind the saved textures). +back exactly. The rule is "restore everything our passes change", which is: + +- `GlPassGuard`: current program, vertex array and array buffer bindings, + viewport, blend enable and functions, scissor, depth, stencil, alpha-test + and cull-face enables, clear colour. +- `GlStateGuard({0})`: active texture and the 2D binding on unit 0. Our + passes only ever bind textures on unit 0. +- The feature module itself: the draw and read framebuffer bindings, read at + the hook and rebound before returning. + +Not touched, so not saved: depth and colour masks (depth test is off, so +nothing writes depth), client-active texture, unit 1, texture environment. All of this runs on the render thread inside an engine render call, the same thread and context as the existing draw-time hooks. From af50b7596845cf2b2be5cc5e6bb9a5f019aa1652 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Wed, 7 Oct 2026 22:23:36 +0200 Subject: [PATCH 41/57] Bloom: keep bright pixels at full strength; sample INI keys for soft fog and bloom --- src/iee/features/world_post.cpp | 10 +++++----- tools/InfinityEngine-Enhancer.sample.ini | 11 +++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/iee/features/world_post.cpp b/src/iee/features/world_post.cpp index b0a471b..ed18f25 100644 --- a/src/iee/features/world_post.cpp +++ b/src/iee/features/world_post.cpp @@ -140,8 +140,10 @@ void main() { } )glsl"; -// Keeps what is brighter than uThreshold, with a soft knee so the cut-off -// does not flicker. +// Keeps what is brighter than uThreshold at full strength, with a soft knee +// so the cut-off does not flicker. The image is 8-bit, so nothing is brighter +// than white: subtracting the threshold (as HDR bloom does) would leave at +// most a few percent to glow with. constexpr const char* kBrightSource = R"glsl(#version 330 uniform sampler2D uTexture; uniform float uThreshold; @@ -151,9 +153,7 @@ void main() { vec3 color = texture(uTexture, vUv).rgb; float level = max(color.r, max(color.g, color.b)); const float knee = 0.1; - float soft = clamp(level - uThreshold + knee, 0.0, 2.0 * knee); - soft = soft * soft / (4.0 * knee); - float weight = max(soft, level - uThreshold) / max(level, 1e-4); + float weight = smoothstep(uThreshold - knee, uThreshold + knee, level); fragColor = vec4(color * weight, 1.0); } )glsl"; diff --git a/tools/InfinityEngine-Enhancer.sample.ini b/tools/InfinityEngine-Enhancer.sample.ini index 922d2f3..0a42ea6 100644 --- a/tools/InfinityEngine-Enhancer.sample.ini +++ b/tools/InfinityEngine-Enhancer.sample.ini @@ -17,6 +17,17 @@ SmoothSpriteMovement = false ; (expanded BAMs, one per original resref). Animations without an expanded ; file are unaffected. Only useful with EEex "Uncap FPS"; BGEE 2.7.3 only. InterpolateAnimations = false +; Smooth the fog-of-war edge. SoftFogRadius is the blur in world pixels; +; 0 keeps the engine's own edge. BGEE 2.7.3 only. With EnableDebugHotkeys, +; F8 toggles it. +SoftFogOfWar = false +SoftFogRadius = 24 +; Glow on bright parts of the world view (never the UI). BloomThreshold is the +; brightness (0..1) above which pixels glow. BGEE 2.7.3 only. With +; EnableDebugHotkeys, F9 toggles it. +Bloom = false +BloomThreshold = 0.80 +BloomStrength = 0.35 [Detection] ; Collect the current area's authored ambient animations from engine memory From d04fda29578d06ef6ed34b4ff1501670f3e4fc04 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Wed, 7 Oct 2026 22:35:48 +0200 Subject: [PATCH 42/57] Bloom: glow and light spill from the engine's additive draws instead of a brightness threshold --- CLAUDE.md | 1 + CMakeLists.txt | 1 + .../2026-10-07-soft-fog-and-bloom-design.md | 33 ++++ src/iee/core/config.cpp | 10 +- src/iee/core/config.h | 3 +- src/iee/features/world_post.cpp | 160 +++++++++++------- src/iee/features/world_post.h | 20 ++- src/iee/frame_hook.cpp | 2 + src/iee/game/build_manifest.cpp | 5 + src/iee/game/build_manifest.h | 21 +++ src/iee/game/draw_queue.cpp | 40 +++++ src/iee/game/draw_queue.h | 35 ++++ src/iee/game/game_addrs.cpp | 13 ++ src/iee/game/game_addrs.h | 4 + src/iee/hooks.cpp | 46 +++++ tests/iee_tests.cpp | 76 ++++++++- tools/InfinityEngine-Enhancer.sample.ini | 9 +- 17 files changed, 398 insertions(+), 81 deletions(-) create mode 100644 src/iee/game/draw_queue.cpp create mode 100644 src/iee/game/draw_queue.h diff --git a/CLAUDE.md b/CLAUDE.md index 7019472..eac5b81 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,6 +45,7 @@ Operational instructions for AI coding agents working in this repository. - The engine does **zero** lighting work. Day/night is a swap of authored assets. `CGameArea` holds authored per-area bitmaps (`m_bmLum` +0x260, `m_pbmLumNight` +0x380, `m_bmHeight` +0x388) usable as static lookup data only. - Prefer `SDL_GL_SwapWindow` (SDL2.dll export) over `DrawFlip` patterns for the frame boundary. - The engine queues every draw (`DrawEnd_GL` only appends to `gl.cmds`) and submits the whole frame in `DrawFlush_GL` (2.7.3 RVA `0x42B350`), normally once at flip, and earlier whenever its 1024x1024 sprite atlas fills. Binding a framebuffer around an engine render function captures nothing, and an immediate GL draw of ours lands under the still-queued world. Flush first (see `features/world_post.*`), and restore every GL state touched: the flush issues changes relative to the engine's cached `gl.hwState`. +- Each queued draw command is 12 bytes (state, first vertex, count) in `gl.cmds`, counted by `gl.n`. The state word carries the blend factors (source bits 9-12, destination bits 13-16, index 1 = `GL_ONE`). The engine draws its light-emitting art (sprite render flags `0x8` / `0x200`: fires, spell effects, glows) with destination `GL_ONE`, so "additive command" is a reliable "this emits light" signal; bloom is fed from it (`game/draw_queue.*`, `detour_draw_flush`). - `gl.pp.enabled` is forced false in 2.7.3 (`DrawInit_GL`): the engine's own offscreen target and `fpCatRom` are dead. ## Runtime Facts — Hooking Safety diff --git a/CMakeLists.txt b/CMakeLists.txt index 6fb0de3..245880c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,7 @@ add_library(iee_common STATIC src/iee/game/area_texture.cpp src/iee/game/build_manifest.cpp src/iee/game/dds_texture.cpp + src/iee/game/draw_queue.cpp src/iee/game/game_addrs.cpp src/iee/game/object_statics.cpp src/iee/game/resref_runtime.cpp diff --git a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md index b3f36fb..7051d94 100644 --- a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md +++ b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md @@ -248,3 +248,36 @@ clamping) and INI parsing. GL behaviour is validated in game only. Light-source bloom, effect-cast light, normal-derived relief, FXAA, water reflections, changes to the engine's fog rules or colours. + +## 11. Revision 2026-10-07: bloom from the engine's additive draws + +In-game result of the threshold bloom (owner): bright painted art such as +paper glowed as much as flames. The threshold approach is removed. + +`CVidCell::RenderTexture` (`0x425530`) selects the blend mode from the sprite +render flags: flag `0x8` or `0x200` gives a destination factor of `GL_ONE`, +everything else is normal alpha blending. The engine's own art flags therefore +mark what emits light. Every queued command keeps its blend factors in its +state word (`DrawBlendFunc_GL`: source at bits 9-12, destination at 13-16; +factor table at `0x5C12B0`, index 1 = `GL_ONE`). + +Design: + +- `DrawFlush_GL` is detoured. During the world pass (frame boundary to + `RenderFog`) the detour copies the queue's additive commands, calls the + original, then binds a black "emissive" target, writes those commands back + into the queue and calls the original again. Vertex data and atlas textures + from the first flush are still valid: the engine overwrites them only when + new draws are queued. +- `gl.n` is located from the rip-relative compare at `DrawFlush_GL+0x24`; + `gl.cmds` lies `0x180A8` bytes before it (`BuildManifest::drawQueue`). +- Before the fog, the emissive image is halved, blurred through the bloom + chain, and applied twice: `framebuffer += framebuffer * light * LightSpill` + (surfaces near a source brighten and take its colour) and + `framebuffer += light * BloomStrength` (the halo). +- INI: `BloomThreshold` is gone; `LightSpill` (default 1.5, 0 = off, max 8). + +Known limits: the replay has no depth buffer, so a light source hidden behind +a wall still glows; the light spill is screen-space, with no falloff model and +no wall occlusion; art drawn with flag `0x200` (`DST_COLOR, ONE`) contributes +nothing to a black target. diff --git a/src/iee/core/config.cpp b/src/iee/core/config.cpp index f03004a..014a11a 100644 --- a/src/iee/core/config.cpp +++ b/src/iee/core/config.cpp @@ -59,10 +59,10 @@ static void normalize(EngineConfig& cfg) noexcept { cfg.maxAnisotropy = std::clamp(cfg.maxAnisotropy, 1.0f, 64.0f); cfg.lodBias = std::clamp(cfg.lodBias, -4.0f, 4.0f); if (!std::isfinite(cfg.softFogRadius)) cfg.softFogRadius = 24.0f; - if (!std::isfinite(cfg.bloomThreshold)) cfg.bloomThreshold = 0.80f; + if (!std::isfinite(cfg.lightSpill)) cfg.lightSpill = 1.5f; if (!std::isfinite(cfg.bloomStrength)) cfg.bloomStrength = 0.35f; cfg.softFogRadius = std::clamp(cfg.softFogRadius, 0.0f, 256.0f); - cfg.bloomThreshold = std::clamp(cfg.bloomThreshold, 0.0f, 1.0f); + cfg.lightSpill = std::clamp(cfg.lightSpill, 0.0f, 8.0f); cfg.bloomStrength = std::clamp(cfg.bloomStrength, 0.0f, 2.0f); } @@ -110,8 +110,8 @@ static void apply_kv(EngineConfig& cfg, const std::string& section, const std::s assign_float(cfg.softFogRadius); else if (iequals(key, "Bloom")) assign_bool(cfg.bloom); - else if (iequals(key, "BloomThreshold")) - assign_float(cfg.bloomThreshold); + else if (iequals(key, "LightSpill")) + assign_float(cfg.lightSpill); else if (iequals(key, "BloomStrength")) assign_float(cfg.bloomStrength); return; @@ -214,7 +214,7 @@ bool ConfigManager::save(const std::filesystem::path& path, const EngineConfig& write_bool(f, "SoftFogOfWar", cfg.softFogOfWar); f << "SoftFogRadius = " << cfg.softFogRadius << "\n"; write_bool(f, "Bloom", cfg.bloom); - f << "BloomThreshold = " << cfg.bloomThreshold << "\n"; + f << "LightSpill = " << cfg.lightSpill << "\n"; f << "BloomStrength = " << cfg.bloomStrength << "\n"; write_section(f, "Detection"); diff --git a/src/iee/core/config.h b/src/iee/core/config.h index b1a4543..3aee54b 100644 --- a/src/iee/core/config.h +++ b/src/iee/core/config.h @@ -12,8 +12,9 @@ struct EngineConfig { bool softFogOfWar = false; float softFogRadius = 24.0f; // world pixels; 0 = capture without blur bool bloom = false; - float bloomThreshold = 0.80f; float bloomStrength = 0.35f; + // How strongly light-emitting effects brighten what is around them; 0 = off. + float lightSpill = 1.5f; bool dumpEngineShaders = false; bool enableDebugHotkeys = false; diff --git a/src/iee/features/world_post.cpp b/src/iee/features/world_post.cpp index ed18f25..b9d64f4 100644 --- a/src/iee/features/world_post.cpp +++ b/src/iee/features/world_post.cpp @@ -34,11 +34,6 @@ struct BlurProgram { int step{-1}; }; -struct BrightProgram { - unsigned id{}; - int threshold{-1}; -}; - // Level i is the base image halved i + 1 times. struct BlurChain { std::array levels{}; @@ -49,8 +44,8 @@ struct Settings { bool softFog{}; bool bloom{}; float fogRadius{}; - float bloomThreshold{}; float bloomStrength{}; + float lightSpill{}; }; // Everything that lives and dies with one GL context. @@ -62,11 +57,11 @@ struct Resources { CompositeProgram composite{}; BlurProgram blurDown{}; BlurProgram blurUp{}; - BrightProgram bright{}; Target fogCapture{}; BlurChain fogChain{}; - Target bloomScene{}; // half-size copy of the world - Target bloomBright{}; // its bright parts + // The engine's additive (light-emitting) draws of this frame, on black. + Target emissive{}; + Target emissiveHalf{}; BlurChain bloomChain{}; bool fogDrawnOnce{}; bool bloomDrawnOnce{}; @@ -79,6 +74,14 @@ int g_engineDrawFramebuffer = 0; int g_engineReadFramebuffer = 0; float g_pixelsPerWorldPixel = 1.0f; std::atomic g_logNextFrame{true}; +// Frame bookkeeping for the emissive capture. The world pass runs from the +// frame boundary to the fog; additive draws queued later belong to the UI. +unsigned g_frame = 1; +unsigned g_emissiveFrame = 0; +bool g_worldPassOpen = false; +int g_emissiveCommands = 0; +int g_savedDrawFramebuffer = 0; +int g_savedReadFramebuffer = 0; constexpr const char* kVertexSource = R"glsl(#version 330 layout(location = 0) in vec2 aPosition; @@ -140,24 +143,6 @@ void main() { } )glsl"; -// Keeps what is brighter than uThreshold at full strength, with a soft knee -// so the cut-off does not flicker. The image is 8-bit, so nothing is brighter -// than white: subtracting the threshold (as HDR bloom does) would leave at -// most a few percent to glow with. -constexpr const char* kBrightSource = R"glsl(#version 330 -uniform sampler2D uTexture; -uniform float uThreshold; -in vec2 vUv; -out vec4 fragColor; -void main() { - vec3 color = texture(uTexture, vUv).rgb; - float level = max(color.r, max(color.g, color.b)); - const float knee = 0.1; - float weight = smoothstep(uThreshold - knee, uThreshold + knee, level); - fragColor = vec4(color * weight, 1.0); -} -)glsl"; - void fail(const char* reason) noexcept { g_failed = true; try { @@ -356,16 +341,11 @@ bool create_shared_resources() noexcept { resources.composite.id = build_program(kCompositeSource); resources.blurDown.id = build_program(kBlurDownSource); resources.blurUp.id = build_program(kBlurUpSource); - resources.bright.id = build_program(kBrightSource); - if (!resources.composite.id || !resources.blurDown.id || !resources.blurUp.id || - !resources.bright.id) { - return false; - } + if (!resources.composite.id || !resources.blurDown.id || !resources.blurUp.id) return false; resources.composite.scale = fn.glGetUniformLocation(resources.composite.id, "uScale"); resources.composite.dither = fn.glGetUniformLocation(resources.composite.id, "uDither"); resources.blurDown.step = fn.glGetUniformLocation(resources.blurDown.id, "uStep"); resources.blurUp.step = fn.glGetUniformLocation(resources.blurUp.id, "uStep"); - resources.bright.threshold = fn.glGetUniformLocation(resources.bright.id, "uThreshold"); return true; } @@ -374,8 +354,8 @@ bool create_sized_resources(game::Extent viewport) noexcept { const auto half = game::half_extent(viewport); if (!create_target(resources.fogCapture, viewport, gl::RGBA8, gl::UNSIGNED_BYTE)) return false; if (!create_chain(resources.fogChain, viewport)) return false; - if (!create_target(resources.bloomScene, half, gl::RGBA16F, gl::HALF_FLOAT)) return false; - if (!create_target(resources.bloomBright, half, gl::RGBA16F, gl::HALF_FLOAT)) return false; + if (!create_target(resources.emissive, viewport, gl::RGBA8, gl::UNSIGNED_BYTE)) return false; + if (!create_target(resources.emissiveHalf, half, gl::RGBA16F, gl::HALF_FLOAT)) return false; if (!create_chain(resources.bloomChain, half)) return false; resources.viewport = viewport; return true; @@ -403,39 +383,42 @@ bool ensure_resources(game::Extent viewport) noexcept { return true; } -// Adds a glow of the bright parts of the world image to the engine's -// framebuffer. Runs before the fog so unexplored areas stay dark, and before -// the UI is drawn so the UI cannot glow. Requires set_pass_state(). +// Glow and light spill from the frame's emissive image. Runs before the fog +// so unexplored areas stay dark, and before the UI is drawn so the UI cannot +// glow. Requires set_pass_state(). void draw_bloom() noexcept { const auto& fn = gl::get_gl_functions(); auto& resources = g_resources; - const auto half = resources.bloomScene.extent; + if (g_emissiveFrame != g_frame) return; // nothing emitted light this frame + const auto half = resources.emissiveHalf.extent; const int planned = game::bloom_levels(half); - if (planned <= 0 || !(g_settings.bloomStrength > 0.0f)) return; + if (planned <= 0) return; - fn.glBindFramebuffer(gl::READ_FRAMEBUFFER, static_cast(g_engineDrawFramebuffer)); - fn.glBindFramebuffer(gl::DRAW_FRAMEBUFFER, resources.bloomScene.framebuffer); + fn.glBindFramebuffer(gl::READ_FRAMEBUFFER, resources.emissive.framebuffer); + fn.glBindFramebuffer(gl::DRAW_FRAMEBUFFER, resources.emissiveHalf.framebuffer); fn.glBlitFramebuffer(0, 0, resources.viewport.width, resources.viewport.height, 0, 0, half.width, half.height, gl::COLOR_BUFFER_BIT, gl::LINEAR); - - fn.glBindFramebuffer(gl::FRAMEBUFFER, resources.bloomBright.framebuffer); - fn.glViewport(0, 0, half.width, half.height); - fn.glUseProgram(resources.bright.id); - fn.glUniform1f(resources.bright.threshold, g_settings.bloomThreshold); - fn.glBindTexture(gl::TEXTURE_2D, resources.bloomScene.texture); - fn.glDrawArrays(gl::TRIANGLES, 0, 3); - - const int levels = run_blur(resources.bloomBright, resources.bloomChain, {planned, 1.0f}, true); + const int levels = + run_blur(resources.emissiveHalf, resources.bloomChain, {planned, 1.0f}, true); bind_engine_framebuffer(); fn.glViewport(0, 0, resources.viewport.width, resources.viewport.height); if (levels > 0) { + // Each level adds its share on the way up; normalise so the strengths + // mean the same thing at every resolution. + const float perLevel = 1.0f / static_cast(levels); + const unsigned glow = resources.bloomChain.levels[0].texture; fn.glEnable(gl::BLEND); - fn.glBlendFunc(gl::ONE, gl::ONE); - // Each level adds its share on the way up; normalise so strength means - // the same thing at every resolution. - draw_composite(resources.bloomChain.levels[0].texture, - g_settings.bloomStrength / static_cast(levels), 0.0f); + if (g_settings.lightSpill > 0.0f) { + // framebuffer += framebuffer * light: surfaces near a light source get + // brighter and take its colour; black stays black. + fn.glBlendFunc(gl::DST_COLOR, gl::ONE); + draw_composite(glow, g_settings.lightSpill * perLevel, 0.0f); + } + if (g_settings.bloomStrength > 0.0f) { + fn.glBlendFunc(gl::ONE, gl::ONE); + draw_composite(glow, g_settings.bloomStrength * perLevel, 0.0f); + } fn.glDisable(gl::BLEND); } @@ -454,8 +437,8 @@ void draw_bloom() noexcept { } // namespace void world_post_configure(const core::EngineConfig& cfg) noexcept { - g_settings = {cfg.softFogOfWar, cfg.bloom, cfg.softFogRadius, cfg.bloomThreshold, - cfg.bloomStrength}; + g_settings = {cfg.softFogOfWar, cfg.bloom, cfg.softFogRadius, cfg.bloomStrength, + cfg.lightSpill}; } bool world_post_active() noexcept { @@ -480,8 +463,60 @@ void world_post_poll_hotkeys() noexcept { } } +void world_post_on_frame() noexcept { + ++g_frame; + g_worldPassOpen = true; +} + +bool world_post_wants_emissive() noexcept { + return g_worldPassOpen && g_settings.bloom && !g_failed; +} + +bool world_post_begin_emissive() noexcept { + const auto& fn = gl::get_gl_functions(); + if (!fn.postProcessAvailable) return false; + fn.glGetIntegerv(gl::DRAW_FRAMEBUFFER_BINDING, &g_savedDrawFramebuffer); + fn.glGetIntegerv(gl::READ_FRAMEBUFFER_BINDING, &g_savedReadFramebuffer); + int viewport[4]{}; + fn.glGetIntegerv(gl::VIEWPORT, viewport); + if (viewport[0] != 0 || viewport[1] != 0 || viewport[2] <= 0 || viewport[3] <= 0) return false; + const game::Extent extent{viewport[2], viewport[3]}; + const bool firstThisFrame = g_emissiveFrame != g_frame; + if (firstThisFrame || !(extent == g_resources.viewport) || + gl::current_context() != g_resources.context) { + core::GlStateGuard textures({0}); + core::GlPassGuard pass; + const bool ready = ensure_resources(extent); + if (ready) { + fn.glBindFramebuffer(gl::FRAMEBUFFER, g_resources.emissive.framebuffer); + fn.glDisable(gl::SCISSOR_TEST); + fn.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + fn.glClear(gl::COLOR_BUFFER_BIT); + g_emissiveCommands = 0; + } + fn.glBindFramebuffer(gl::DRAW_FRAMEBUFFER, static_cast(g_savedDrawFramebuffer)); + fn.glBindFramebuffer(gl::READ_FRAMEBUFFER, static_cast(g_savedReadFramebuffer)); + if (!ready) { + fail("GL resources could not be created"); + return false; + } + g_emissiveFrame = g_frame; + } + // State is the engine's again; only the framebuffer differs for the replay. + fn.glBindFramebuffer(gl::FRAMEBUFFER, g_resources.emissive.framebuffer); + return true; +} + +void world_post_end_emissive(int commands) noexcept { + const auto& fn = gl::get_gl_functions(); + g_emissiveCommands += commands; + fn.glBindFramebuffer(gl::DRAW_FRAMEBUFFER, static_cast(g_savedDrawFramebuffer)); + fn.glBindFramebuffer(gl::READ_FRAMEBUFFER, static_cast(g_savedReadFramebuffer)); +} + bool world_post_before_fog(float viewWorldWidth) noexcept { const auto& fn = gl::get_gl_functions(); + g_worldPassOpen = false; if (!fn.postProcessAvailable) { fail("the GL context lacks framebuffer or vertex-array support"); return false; @@ -520,11 +555,12 @@ bool world_post_before_fog(float viewWorldWidth) noexcept { try { LOG_INFO( "World post: softFog={} (radius {} world px, blur levels={} offset={:.2f}), " - "bloom={}, viewport {}x{}, {:.3f} px per world px, engine framebuffer draw={} " - "read={}", + "bloom={} (strength {}, light spill {}, {} additive draws this frame), viewport " + "{}x{}, {:.3f} px per world px, engine framebuffer draw={} read={}", g_settings.softFog, g_settings.fogRadius, plan.levels, plan.offset, - g_settings.bloom, viewport[2], viewport[3], g_pixelsPerWorldPixel, - g_engineDrawFramebuffer, g_engineReadFramebuffer); + g_settings.bloom, g_settings.bloomStrength, g_settings.lightSpill, + g_emissiveFrame == g_frame ? g_emissiveCommands : 0, viewport[2], viewport[3], + g_pixelsPerWorldPixel, g_engineDrawFramebuffer, g_engineReadFramebuffer); } catch (...) { } } diff --git a/src/iee/features/world_post.h b/src/iee/features/world_post.h index 95385c4..b4258c6 100644 --- a/src/iee/features/world_post.h +++ b/src/iee/features/world_post.h @@ -5,12 +5,13 @@ struct EngineConfig; } namespace iee::features { -// World post passes: soft fog of war and bloom, run from the +// World post passes: soft fog of war, and glow plus light spill from the +// engine's light-emitting draws, run from the // CInfinity::RenderFog detour. The engine queues the whole frame and only // draws in DrawFlush_GL, so the caller flushes that queue around these calls: // // flush(); // world is now in the framebuffer -// bool capturing = world_post_before_fog(); // bloom; fog target bound if true +// bool capturing = world_post_before_fog(); // glow + light spill; fog target bound if true // original RenderFog // queues the fog triangles // if (capturing) { flush(); world_post_after_fog(); } // @@ -22,6 +23,21 @@ bool world_post_active() noexcept; // Debug A/B: F8 toggles soft fog of war, F9 toggles bloom. Call once per // frame, only when debug hotkeys are enabled. void world_post_poll_hotkeys() noexcept; +// Frame boundary: the next world pass starts. +void world_post_on_frame() noexcept; +// Bloom is fed by the engine's own additive draws (fires, spell effects, +// glows). The DrawFlush_GL detour replays them into a black target: +// +// if (world_post_wants_emissive()) { +// collect the queue's additive commands; original flush; +// if (any && world_post_begin_emissive()) { // our target is bound +// put them back in the queue; original flush; +// world_post_end_emissive(count); // engine framebuffer rebound +// } +// } +bool world_post_wants_emissive() noexcept; +bool world_post_begin_emissive() noexcept; +void world_post_end_emissive(int commands) noexcept; // viewWorldWidth: world pixels visible across the view (0 when unknown). bool world_post_before_fog(float viewWorldWidth) noexcept; void world_post_after_fog() noexcept; diff --git a/src/iee/frame_hook.cpp b/src/iee/frame_hook.cpp index 22a221a..775bfd9 100644 --- a/src/iee/frame_hook.cpp +++ b/src/iee/frame_hook.cpp @@ -9,6 +9,7 @@ #include "iee/core/logger.h" #include "iee/core/pattern_scanner.h" #include "iee/core/performance_samples.h" +#include "iee/features/world_post.h" #include "iee/hooks.h" #include "iee/shader_probe.h" @@ -64,6 +65,7 @@ void frame_tick() { g_frames.fetch_add(1, std::memory_order_relaxed); hooks::retry_shader_probe_install(); probe::on_frame_tick(seconds_since_install()); + features::world_post_on_frame(); } void detour_sdl_swap(void* window) { diff --git a/src/iee/game/build_manifest.cpp b/src/iee/game/build_manifest.cpp index c9cd6a8..8b91c65 100644 --- a/src/iee/game/build_manifest.cpp +++ b/src/iee/game/build_manifest.cpp @@ -237,6 +237,11 @@ constexpr BuildManifest kKnownBuilds[] = { {"CGameChunk::Render", "4C 8B DC 55 56 57 41 55 41 57 49 8D 6B A1", 0x36B7A0}, {"CVEFVidCell::Render", "4C 8B DC 55 56 41 57 49 8D 6B A1 48 81 EC D0", 0x254B00}, }}, + // Disassembly-verified on the Steam 2.7.3.0 binary: DrawFlush_GL+0x24 + // compares gl.n (0x2F73FD0); DrawEnd_GL writes gl.cmds at 0x2F5BF28 + // (12 bytes each); DrawBlendFunc_GL packs src<<9 | dst<<13; the + // factor table at 0x5C12B0 has GL_ONE at index 1. + {0x24, 0x180A8, 8192, 8, 13, 1}, }, }; diff --git a/src/iee/game/build_manifest.h b/src/iee/game/build_manifest.h index b5f70ce..0f98244 100644 --- a/src/iee/game/build_manifest.h +++ b/src/iee/game/build_manifest.h @@ -111,6 +111,25 @@ struct ObjectRenderTarget { }; inline constexpr std::size_t kMaxSmoothedObjectRenders = 8; +// The GL backend's draw queue (gl.cmds / gl.n), consumed by DrawFlush_GL. +// Used to pick out the engine's additive (light-emitting) draws. +struct DrawQueueLayout { + // Offset inside DrawFlush_GL of `cmp [rip + gl.n], r14d`. + std::size_t countCompareOffset{}; + // gl.cmds starts this many bytes before gl.n. + std::uintptr_t commandsBeforeCount{}; + std::uint32_t maxCommands{}; + // Command state word: blend enable bit, destination factor field (4 bits), + // and the factor index that means GL_ONE. + std::uint32_t blendEnableBit{}; + std::uint32_t blendDstShift{}; + std::uint32_t blendFactorOne{}; + + [[nodiscard]] constexpr bool valid() const noexcept { + return countCompareOffset != 0 && commandsBeforeCount != 0 && maxCommands != 0; + } +}; + struct BuildManifest { std::string_view buildId{}; std::array supportedProductNames{}; @@ -122,6 +141,8 @@ struct BuildManifest { // Moving non-creature objects (projectiles, spell effect cells, debris). // Unused slots stay empty. std::array smoothedObjectRenders{}; + // Optional; an invalid (empty) layout leaves emissive bloom off. + DrawQueueLayout drawQueue{}; [[nodiscard]] constexpr bool validate() const noexcept { if (buildId.empty() || supportedProductNames[0].empty() || executableVersion.major == 0 || diff --git a/src/iee/game/draw_queue.cpp b/src/iee/game/draw_queue.cpp new file mode 100644 index 0000000..3dbeb8d --- /dev/null +++ b/src/iee/game/draw_queue.cpp @@ -0,0 +1,40 @@ +#include "iee/game/draw_queue.h" + +#include + +namespace iee::game { +bool is_additive(const DrawQueueLayout& layout, std::uint32_t state) noexcept { + if (((state >> layout.blendEnableBit) & 1u) == 0) return false; + return ((state >> layout.blendDstShift) & 0xFu) == layout.blendFactorOne; +} + +std::size_t collect_additive(const DrawQueueLayout& layout, const DrawCommand* commands, + int count, std::span out) noexcept { + if (!commands || count <= 0 || static_cast(count) > layout.maxCommands) return 0; + std::size_t collected = 0; + for (int index = 0; index < count && collected < out.size(); ++index) { + const auto& command = commands[index]; + if (command.primCount <= 0 || !is_additive(layout, command.state)) continue; + out[collected++] = command; + } + return collected; +} + +std::uintptr_t draw_queue_count_address(const DrawQueueLayout& layout, + std::uintptr_t drawFlush) noexcept { + if (!drawFlush || !layout.valid()) return 0; + const auto* instruction = reinterpret_cast(drawFlush + layout.countCompareOffset); + // cmp dword ptr [rip + disp32], r14d + if (instruction[0] != 0x44 || instruction[1] != 0x39 || instruction[2] != 0x35) return 0; + std::int32_t displacement = 0; + std::memcpy(&displacement, instruction + 3, sizeof(displacement)); + const auto next = drawFlush + layout.countCompareOffset + 7; + return next + static_cast(static_cast(displacement)); +} + +std::uintptr_t draw_queue_commands_address(const DrawQueueLayout& layout, + std::uintptr_t countAddress) noexcept { + if (!countAddress || !layout.valid()) return 0; + return countAddress - layout.commandsBeforeCount; +} +} // namespace iee::game diff --git a/src/iee/game/draw_queue.h b/src/iee/game/draw_queue.h new file mode 100644 index 0000000..44d20ad --- /dev/null +++ b/src/iee/game/draw_queue.h @@ -0,0 +1,35 @@ +#pragma once +#include +#include +#include + +#include "iee/game/build_manifest.h" + +namespace iee::game { +// The GL backend queues every draw as one of these and submits the queue in +// DrawFlush_GL. Layout verified against DrawEnd_GL / DrawFlush_GL (2.7.3). +struct DrawCommand { + std::uint32_t state{}; + std::int32_t primStart{}; + // -1 marks a clear command; it carries no vertices. + std::int32_t primCount{}; +}; +static_assert(sizeof(DrawCommand) == 12, "DrawCommand must match the engine's 12-byte command"); + +// True when the command adds its colour to what is already drawn (destination +// factor GL_ONE). The engine draws its light-emitting art this way: fires, +// spell effects, glows. +bool is_additive(const DrawQueueLayout& layout, std::uint32_t state) noexcept; + +// Copies the additive, drawable commands of a queue into `out`, in order. +// Returns how many were copied; 0 for an implausible queue. +std::size_t collect_additive(const DrawQueueLayout& layout, const DrawCommand* commands, + int count, std::span out) noexcept; + +// Address of the queue's command count, read from the rip-relative compare at +// the start of DrawFlush_GL. 0 when the bytes there are not that instruction. +std::uintptr_t draw_queue_count_address(const DrawQueueLayout& layout, + std::uintptr_t drawFlush) noexcept; +std::uintptr_t draw_queue_commands_address(const DrawQueueLayout& layout, + std::uintptr_t countAddress) noexcept; +} // namespace iee::game diff --git a/src/iee/game/game_addrs.cpp b/src/iee/game/game_addrs.cpp index 24c583c..b9298db 100644 --- a/src/iee/game/game_addrs.cpp +++ b/src/iee/game/game_addrs.cpp @@ -1,3 +1,4 @@ +#include "iee/game/draw_queue.h" #include "game_addrs.h" #include @@ -154,6 +155,18 @@ namespace iee::game { if (out.RenderFog && out.DrawFlush) { LOG_INFO("World post targets resolved at RVA 0x{:X} / 0x{:X}", out.RenderFog - moduleBase, out.DrawFlush - moduleBase); + out.DrawQueueCount = draw_queue_count_address(manifest.drawQueue, out.DrawFlush); + out.DrawQueueCommands = + draw_queue_commands_address(manifest.drawQueue, out.DrawQueueCount); + if (out.DrawQueueCount && out.DrawQueueCommands) { + LOG_INFO("Draw queue resolved: count at RVA 0x{:X}, commands at RVA 0x{:X}", + out.DrawQueueCount - moduleBase, out.DrawQueueCommands - moduleBase); + } else { + out.DrawQueueCount = out.DrawQueueCommands = 0; + if (cfg.bloom) { + LOG_WARN("Bloom disabled: the draw queue was not found in DrawFlush_GL"); + } + } } else { const bool known = !manifest.patterns.renderFog.empty() && !manifest.patterns.drawFlush.empty(); diff --git a/src/iee/game/game_addrs.h b/src/iee/game/game_addrs.h index 65eb8a2..2e9ce92 100644 --- a/src/iee/game/game_addrs.h +++ b/src/iee/game/game_addrs.h @@ -31,6 +31,10 @@ namespace iee::game { // bloom) need the fog render to hook and the queue flush to call. std::uintptr_t RenderFog = 0; std::uintptr_t DrawFlush = 0; + // Optional: the draw queue DrawFlush_GL consumes (count and command + // array). 0 leaves the fog and flush hooks working without bloom. + std::uintptr_t DrawQueueCount = 0; + std::uintptr_t DrawQueueCommands = 0; bool initialized = false; }; diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index 608c720..b7f1d32 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,7 @@ #include "iee/frame_hook.h" #include "iee/game/game_types.h" #include "iee/game/animation_interp.h" +#include "iee/game/draw_queue.h" #include "iee/game/renderer.h" #include "iee/game/runtime_types_x64.h" #include "iee/game/sprite_motion.h" @@ -61,6 +63,12 @@ using RenderFogFn = void (*)(void*, void*, void*); using DrawFlushFn = void (*)(); static core::Hook g_renderFogHook; static DrawFlushFn g_drawFlush = nullptr; +// DrawFlush_GL is also detoured, to replay the queue's additive draws into +// the bloom's emissive target. g_drawFlush stays the engine's entry point, so +// our own flushes go through the detour too. +static core::Hook g_drawFlushHook; +static std::int32_t* g_drawQueueCount = nullptr; +static game::DrawCommand* g_drawQueueCommands = nullptr; static AppContext* g_ctx = nullptr; // Raised by LoadArea; the sprite-smoothing tracker clears itself on its own thread. @@ -439,6 +447,29 @@ static constexpr std::array object_render_deto static constexpr auto kObjectRenderDetours = object_render_detours(std::make_index_sequence{}); +// Bloom source. The engine draws its light-emitting art (fires, spell +// effects, glows) additively, and every queued command records its blend +// mode. During the world pass each flush is followed by a second one that +// holds only those commands, with our black emissive target bound. The vertex +// data and textures of the first flush are still in place: the engine only +// overwrites them when new draws are queued. Render thread only. +static void detour_draw_flush() { + const auto original = g_drawFlushHook.original(); + if (!g_ctx || !g_drawQueueCount || !features::world_post_wants_emissive()) { + original(); + return; + } + static std::array additive; + const auto collected = game::collect_additive(g_ctx->manifest->drawQueue, g_drawQueueCommands, + *g_drawQueueCount, additive); + original(); + if (collected == 0 || !features::world_post_begin_emissive()) return; + std::memcpy(g_drawQueueCommands, additive.data(), collected * sizeof(game::DrawCommand)); + *g_drawQueueCount = static_cast(collected); + original(); + features::world_post_end_emissive(static_cast(collected)); +} + // World post passes (soft fog of war, bloom). The engine queues every draw // and submits the queue in DrawFlush_GL at the end of the frame, so the world // only exists in the framebuffer once we flush, and the fog only lands in our @@ -668,7 +699,18 @@ bool install_all(AppContext& ctx) { g_renderFogHook.enable(); LOG_INFO("CInfinity::RenderFog hook installed (soft fog of war={}, bloom={})", ctx.cfg.softFogOfWar, ctx.cfg.bloom); + if (ctx.addrs.DrawQueueCount && ctx.addrs.DrawQueueCommands) { + g_drawQueueCount = reinterpret_cast(ctx.addrs.DrawQueueCount); + g_drawQueueCommands = reinterpret_cast(ctx.addrs.DrawQueueCommands); + g_drawFlushHook.create(reinterpret_cast(ctx.addrs.DrawFlush), + reinterpret_cast(&detour_draw_flush)); + g_drawFlushHook.enable(); + LOG_INFO("DrawFlush_GL hook installed (bloom from the engine's additive draws)"); + } } catch (...) { + g_drawQueueCount = nullptr; + g_drawQueueCommands = nullptr; + (void)g_drawFlushHook.remove(); g_drawFlush = nullptr; (void)g_renderFogHook.remove(); LOG_WARN("CInfinity::RenderFog hook failed; fog of war and bloom stay vanilla"); @@ -716,6 +758,9 @@ void uninstall_all() noexcept { } catch (...) { } + (void)g_drawFlushHook.remove(); + g_drawQueueCount = nullptr; + g_drawQueueCommands = nullptr; (void)g_renderFogHook.remove(); g_drawFlush = nullptr; features::world_post_forget(); @@ -747,6 +792,7 @@ void prepare_for_shutdown() noexcept { // state are torn down. MinHook itself stays initialized until // uninstall_all(), after every MinHook-backed subsystem has removed its // hooks. + (void)g_drawFlushHook.disable(); (void)g_renderFogHook.disable(); (void)g_vidCellFrameSizeHook.disable(); (void)g_vidCellCenterPointHook.disable(); diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index fa81b6d..df8dead 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -26,6 +26,7 @@ #include "iee/game/object_statics.h" #include "iee/game/build_manifest.h" #include "iee/game/dds_texture.h" +#include "iee/game/draw_queue.h" #include "iee/game/eeex_doc_layouts_x64.h" #include "iee/game/file_formats.h" #include "iee/game/runtime_types_x64.h" @@ -1271,7 +1272,7 @@ void test_config_world_post() { iee::core::EngineConfig defaults{}; expect_true(!defaults.softFogOfWar && !defaults.bloom, "World post effects default off"); expect_eq(defaults.softFogRadius, 24.0f, "Soft fog radius default"); - expect_eq(defaults.bloomThreshold, 0.80f, "Bloom threshold default"); + expect_eq(defaults.lightSpill, 1.5f, "Light spill default"); expect_eq(defaults.bloomStrength, 0.35f, "Bloom strength default"); const auto tempPath = @@ -1282,40 +1283,40 @@ void test_config_world_post() { out << "SoftFogOfWar = true\n"; out << "SoftFogRadius = 40\n"; out << "Bloom = true\n"; - out << "BloomThreshold = 0.6\n"; + out << "LightSpill = 3\n"; out << "BloomStrength = 0.5\n"; } iee::core::EngineConfig cfg{}; expect_true(iee::core::ConfigManager::load(tempPath, cfg), "World post keys should load"); expect_true(cfg.softFogOfWar && cfg.bloom, "World post bools should parse"); expect_eq(cfg.softFogRadius, 40.0f, "Soft fog radius should parse"); - expect_eq(cfg.bloomThreshold, 0.6f, "Bloom threshold should parse"); + expect_eq(cfg.lightSpill, 3.0f, "Light spill should parse"); expect_eq(cfg.bloomStrength, 0.5f, "Bloom strength should parse"); { std::ofstream out(tempPath, std::ios::trunc); out << "[Rendering]\n"; out << "SoftFogRadius = 100000\n"; - out << "BloomThreshold = 7\n"; + out << "LightSpill = 700\n"; out << "BloomStrength = -3\n"; } cfg = {}; expect_true(iee::core::ConfigManager::load(tempPath, cfg), "Out-of-range values should load"); expect_eq(cfg.softFogRadius, 256.0f, "Soft fog radius is clamped to its maximum"); - expect_eq(cfg.bloomThreshold, 1.0f, "Bloom threshold is clamped to 1"); + expect_eq(cfg.lightSpill, 8.0f, "Light spill is clamped to its maximum"); expect_eq(cfg.bloomStrength, 0.0f, "Bloom strength is clamped to 0"); { std::ofstream out(tempPath, std::ios::trunc); out << "[Rendering]\n"; out << "SoftFogRadius = nan\n"; - out << "BloomThreshold = inf\n"; + out << "LightSpill = inf\n"; out << "BloomStrength = nan\n"; } cfg = {}; expect_true(iee::core::ConfigManager::load(tempPath, cfg), "Non-finite values should load"); expect_eq(cfg.softFogRadius, 24.0f, "A non-finite radius falls back to the default"); - expect_eq(cfg.bloomThreshold, 0.80f, "A non-finite threshold falls back to the default"); + expect_eq(cfg.lightSpill, 1.5f, "A non-finite light spill falls back to the default"); expect_eq(cfg.bloomStrength, 0.35f, "A non-finite strength falls back to the default"); std::error_code error; @@ -1335,6 +1336,66 @@ void test_manifest_world_post_targets() { "The 2.6.6 manifest has no world post targets"); } +void test_draw_queue() { + using namespace iee::game; + const auto found = find_manifest("BGEE 2.7.3.x"); + expect_true(found.has_value(), "The 2.7.3 manifest should be registered"); + if (!found) return; + const DrawQueueLayout& layout = found->get().drawQueue; + expect_true(layout.valid(), "2.7.3 should describe the engine draw queue"); + expect_true(!current_manifest().drawQueue.valid(), "2.6.6 has no draw queue layout"); + + // State word: blend enable is bit 8, source factor bits 9-12, destination + // factor bits 13-16; factor index 1 is GL_ONE, 6/7 are SRC_ALPHA and its inverse. + const auto state = [](bool blend, unsigned src, unsigned dst) { + return static_cast((blend ? 1u << 8 : 0u) | (src << 9) | (dst << 13) | 0x3u); + }; + expect_true(is_additive(layout, state(true, 4, 1)), "ONE_MINUS_DST_COLOR / ONE is additive"); + expect_true(is_additive(layout, state(true, 3, 1)), "SRC_COLOR / ONE is additive"); + expect_true(!is_additive(layout, state(true, 6, 7)), "Normal alpha blending is not additive"); + expect_true(!is_additive(layout, state(false, 4, 1)), "Blending switched off is not additive"); + + const std::array queue{{ + {state(true, 6, 7), 0, 6}, // normal sprite + {state(true, 4, 1), 6, 12}, // glow + {state(true, 4, 1), 18, -1}, // a clear command must never be replayed + {state(true, 4, 1), 18, 0}, // empty + {state(true, 3, 1), 18, 6}, // glow + }}; + std::array out{}; + expect_eq(collect_additive(layout, queue.data(), 5, out), std::size_t{2}, + "Only drawable additive commands are collected"); + expect_eq(out[0].primStart, 6, "Collected commands keep their order"); + expect_eq(out[1].primStart, 18, "Collected commands keep their vertex range"); + std::array tiny{}; + expect_eq(collect_additive(layout, queue.data(), 5, tiny), std::size_t{1}, + "Collection stops at the output capacity"); + expect_eq(collect_additive(layout, queue.data(), -3, out), std::size_t{0}, + "A negative count collects nothing"); + expect_eq(collect_additive(layout, queue.data(), 1 << 20, out), std::size_t{0}, + "A count beyond the engine's queue capacity collects nothing"); + expect_eq(collect_additive(layout, nullptr, 5, out), std::size_t{0}, + "A missing queue collects nothing"); + + // cmp [rip + disp32], r14d at function offset 0x24: 44 39 35 . + std::array code{}; + code[0x24] = 0x44; + code[0x25] = 0x39; + code[0x26] = 0x35; + const std::int32_t disp = 0x1000; + std::memcpy(&code[0x27], &disp, sizeof(disp)); + const auto base = reinterpret_cast(code.data()); + expect_eq(draw_queue_count_address(layout, base), base + 0x2B + 0x1000, + "The count address is the rip-relative target of the compare"); + code[0x25] = 0x00; + expect_eq(draw_queue_count_address(layout, base), std::uintptr_t{0}, + "An unexpected instruction yields no address"); + expect_eq(draw_queue_count_address(layout, 0), std::uintptr_t{0}, + "A missing function yields no address"); + expect_eq(draw_queue_commands_address(layout, 0x200000), std::uintptr_t{0x200000 - 0x180A8}, + "The command array sits a fixed distance before the count"); +} + void test_world_post_plan() { using namespace iee::game; @@ -2194,6 +2255,7 @@ int main() { test_sprite_motion_tracker(); test_animation_interpolation(); test_world_post_plan(); + test_draw_queue(); test_build_area_effect_points(); test_config_detection_section(); test_config_world_post(); diff --git a/tools/InfinityEngine-Enhancer.sample.ini b/tools/InfinityEngine-Enhancer.sample.ini index 0a42ea6..228d02d 100644 --- a/tools/InfinityEngine-Enhancer.sample.ini +++ b/tools/InfinityEngine-Enhancer.sample.ini @@ -22,12 +22,13 @@ InterpolateAnimations = false ; F8 toggles it. SoftFogOfWar = false SoftFogRadius = 24 -; Glow on bright parts of the world view (never the UI). BloomThreshold is the -; brightness (0..1) above which pixels glow. BGEE 2.7.3 only. With -; EnableDebugHotkeys, F9 toggles it. +; Glow around the engine's light-emitting effects (fires, torches, spells), +; never the UI or merely bright art. BloomStrength is the halo; LightSpill is +; how much those effects brighten and tint what is around them (0 = off). +; BGEE 2.7.3 only. With EnableDebugHotkeys, F9 toggles it. Bloom = false -BloomThreshold = 0.80 BloomStrength = 0.35 +LightSpill = 1.5 [Detection] ; Collect the current area's authored ambient animations from engine memory From d0cd02d25f803e81ed7b784b0d36017737ff9e9b Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Wed, 7 Oct 2026 22:47:00 +0200 Subject: [PATCH 43/57] World post: drifting soft fog edge; light spill with a falloff curve from the wide blur levels --- .../2026-10-07-soft-fog-and-bloom-design.md | 14 +++ src/iee/core/config.cpp | 5 + src/iee/core/config.h | 1 + src/iee/features/world_post.cpp | 106 ++++++++++++++++-- src/iee/features/world_post.h | 11 +- src/iee/hooks.cpp | 8 +- tests/iee_tests.cpp | 5 + tools/InfinityEngine-Enhancer.sample.ini | 2 + 8 files changed, 135 insertions(+), 17 deletions(-) diff --git a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md index 7051d94..0abd403 100644 --- a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md +++ b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md @@ -281,3 +281,17 @@ Known limits: the replay has no depth buffer, so a light source hidden behind a wall still glows; the light spill is screen-space, with no falloff model and no wall occlusion; art drawn with flag `0x200` (`DST_COLOR, ONE`) contributes nothing to a black target. + +## 12. Revision 2026-10-07 (later): fog drift and light falloff + +Owner feedback on section 11's build: bloom and fog both "feel better"; asked +for more. + +- **Fog drift.** The blurred fog is composited by its own program, which + offsets the lookup by slow two-octave value noise anchored to world + coordinates (`SoftFogDrift`, world pixels, default 10, 0 = still). Flat + regions are unaffected, so only the edge moves. Needs the view transform; + without it the edge stays still. +- **Light falloff.** The light spill now samples a deeper level of the bloom + chain (wide blurs only) and maps it through `1 - exp(-x)`, so a small source + lights a wider area and the area next to it saturates instead of clipping. diff --git a/src/iee/core/config.cpp b/src/iee/core/config.cpp index 014a11a..38d09a4 100644 --- a/src/iee/core/config.cpp +++ b/src/iee/core/config.cpp @@ -59,9 +59,11 @@ static void normalize(EngineConfig& cfg) noexcept { cfg.maxAnisotropy = std::clamp(cfg.maxAnisotropy, 1.0f, 64.0f); cfg.lodBias = std::clamp(cfg.lodBias, -4.0f, 4.0f); if (!std::isfinite(cfg.softFogRadius)) cfg.softFogRadius = 24.0f; + if (!std::isfinite(cfg.softFogDrift)) cfg.softFogDrift = 10.0f; if (!std::isfinite(cfg.lightSpill)) cfg.lightSpill = 1.5f; if (!std::isfinite(cfg.bloomStrength)) cfg.bloomStrength = 0.35f; cfg.softFogRadius = std::clamp(cfg.softFogRadius, 0.0f, 256.0f); + cfg.softFogDrift = std::clamp(cfg.softFogDrift, 0.0f, 64.0f); cfg.lightSpill = std::clamp(cfg.lightSpill, 0.0f, 8.0f); cfg.bloomStrength = std::clamp(cfg.bloomStrength, 0.0f, 2.0f); } @@ -108,6 +110,8 @@ static void apply_kv(EngineConfig& cfg, const std::string& section, const std::s assign_bool(cfg.softFogOfWar); else if (iequals(key, "SoftFogRadius")) assign_float(cfg.softFogRadius); + else if (iequals(key, "SoftFogDrift")) + assign_float(cfg.softFogDrift); else if (iequals(key, "Bloom")) assign_bool(cfg.bloom); else if (iequals(key, "LightSpill")) @@ -213,6 +217,7 @@ bool ConfigManager::save(const std::filesystem::path& path, const EngineConfig& write_bool(f, "InterpolateAnimations", cfg.interpolateAnimations); write_bool(f, "SoftFogOfWar", cfg.softFogOfWar); f << "SoftFogRadius = " << cfg.softFogRadius << "\n"; + f << "SoftFogDrift = " << cfg.softFogDrift << "\n"; write_bool(f, "Bloom", cfg.bloom); f << "LightSpill = " << cfg.lightSpill << "\n"; f << "BloomStrength = " << cfg.bloomStrength << "\n"; diff --git a/src/iee/core/config.h b/src/iee/core/config.h index 3aee54b..9b06362 100644 --- a/src/iee/core/config.h +++ b/src/iee/core/config.h @@ -11,6 +11,7 @@ struct EngineConfig { bool interpolateAnimations = false; bool softFogOfWar = false; float softFogRadius = 24.0f; // world pixels; 0 = capture without blur + float softFogDrift = 10.0f; // world pixels the soft edge drifts by; 0 = still bool bloom = false; float bloomStrength = 0.35f; // How strongly light-emitting effects brighten what is around them; 0 = off. diff --git a/src/iee/features/world_post.cpp b/src/iee/features/world_post.cpp index b9d64f4..9725f3f 100644 --- a/src/iee/features/world_post.cpp +++ b/src/iee/features/world_post.cpp @@ -27,6 +27,15 @@ struct CompositeProgram { unsigned id{}; int scale{-1}; int dither{-1}; + int curve{-1}; +}; + +struct FogProgram { + unsigned id{}; + int worldOrigin{-1}; + int worldSize{-1}; + int drift{-1}; + int time{-1}; }; struct BlurProgram { @@ -46,6 +55,7 @@ struct Settings { float fogRadius{}; float bloomStrength{}; float lightSpill{}; + float fogDrift{}; }; // Everything that lives and dies with one GL context. @@ -55,6 +65,7 @@ struct Resources { unsigned vertexArray{}; unsigned vertexBuffer{}; CompositeProgram composite{}; + FogProgram fog{}; BlurProgram blurDown{}; BlurProgram blurUp{}; Target fogCapture{}; @@ -73,6 +84,7 @@ bool g_failed = false; int g_engineDrawFramebuffer = 0; int g_engineReadFramebuffer = 0; float g_pixelsPerWorldPixel = 1.0f; +WorldView g_view{}; std::atomic g_logNextFrame{true}; // Frame bookkeeping for the emissive capture. The world pass runs from the // frame boundary to the fog; additive draws queued later belong to the UI. @@ -92,22 +104,65 @@ void main() { } )glsl"; -// Writes the texture scaled by uScale; the caller's blend mode decides whether -// that multiplies (fog) or adds (bloom). uDither adds +-0.5/255 of ordered -// noise to hide banding in long dark gradients. +// Writes the texture scaled by uScale; the caller's blend mode decides how it +// lands. uDither adds +-0.5/255 of ordered noise to hide banding in long dark +// gradients. With uCurve set the value is treated as light and mapped through +// 1 - exp(-x): faint light far from a source still registers, and light next +// to the source saturates instead of clipping. constexpr const char* kCompositeSource = R"glsl(#version 330 uniform sampler2D uTexture; uniform float uScale; uniform float uDither; +uniform float uCurve; in vec2 vUv; out vec4 fragColor; void main() { vec3 color = texture(uTexture, vUv).rgb * uScale; + if (uCurve > 0.5) color = vec3(1.0) - exp(-color); float noise = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))); fragColor = vec4(color + uDither * (noise - 0.5) / 255.0, 1.0); } )glsl"; +// Multiplies the scene by the blurred fog capture (white = fully visible). +// The lookup is pushed around by slow noise anchored to the world, so the +// soft edge drifts like mist; where the capture is flat the push changes +// nothing, so explored and unexplored areas keep the engine's exact shade. +constexpr const char* kFogSource = R"glsl(#version 330 +uniform sampler2D uTexture; +uniform vec2 uWorldOrigin; +uniform vec2 uWorldSize; +uniform float uDrift; +uniform float uTime; +in vec2 vUv; +out vec4 fragColor; +float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} +float noise(vec2 p) { + vec2 cell = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(cell), hash(cell + vec2(1.0, 0.0)), f.x), + mix(hash(cell + vec2(0.0, 1.0)), hash(cell + vec2(1.0, 1.0)), f.x), f.y); +} +void main() { + vec2 uv = vUv; + if (uDrift > 0.0 && uWorldSize.x > 0.0 && uWorldSize.y > 0.0) { + vec2 world = uWorldOrigin + vec2(vUv.x, 1.0 - vUv.y) * uWorldSize; + vec2 p = world / 96.0; + vec2 push = vec2(noise(p + vec2(uTime * 0.07, uTime * 0.05)) + + 0.5 * noise(p * 2.3 - vec2(uTime * 0.11, 0.0)), + noise(p + vec2(31.7, 17.3) - vec2(uTime * 0.06, uTime * 0.08)) + + 0.5 * noise(p * 2.3 + vec2(5.1, uTime * 0.09))) / 1.5 - 0.5; + uv += push * 2.0 * uDrift / uWorldSize; + } + vec3 visible = texture(uTexture, uv).rgb; + float grain = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))); + fragColor = vec4(visible + (grain - 0.5) / 255.0, 1.0); +} +)glsl"; + // Dual-filter blur (down then up through half-size levels). uStep is the // sample spread in source UV units. constexpr const char* kBlurDownSource = R"glsl(#version 330 @@ -276,11 +331,12 @@ void set_pass_state() noexcept { fn.glActiveTexture(gl::TEXTURE0); } -void draw_composite(unsigned texture, float scale, float dither) noexcept { +void draw_composite(unsigned texture, float scale, float dither, bool lightCurve = false) noexcept { const auto& fn = gl::get_gl_functions(); fn.glUseProgram(g_resources.composite.id); fn.glUniform1f(g_resources.composite.scale, scale); fn.glUniform1f(g_resources.composite.dither, dither); + fn.glUniform1f(g_resources.composite.curve, lightCurve ? 1.0f : 0.0f); fn.glBindTexture(gl::TEXTURE_2D, texture); fn.glDrawArrays(gl::TRIANGLES, 0, 3); } @@ -341,7 +397,16 @@ bool create_shared_resources() noexcept { resources.composite.id = build_program(kCompositeSource); resources.blurDown.id = build_program(kBlurDownSource); resources.blurUp.id = build_program(kBlurUpSource); - if (!resources.composite.id || !resources.blurDown.id || !resources.blurUp.id) return false; + resources.fog.id = build_program(kFogSource); + if (!resources.composite.id || !resources.blurDown.id || !resources.blurUp.id || + !resources.fog.id) { + return false; + } + resources.composite.curve = fn.glGetUniformLocation(resources.composite.id, "uCurve"); + resources.fog.worldOrigin = fn.glGetUniformLocation(resources.fog.id, "uWorldOrigin"); + resources.fog.worldSize = fn.glGetUniformLocation(resources.fog.id, "uWorldSize"); + resources.fog.drift = fn.glGetUniformLocation(resources.fog.id, "uDrift"); + resources.fog.time = fn.glGetUniformLocation(resources.fog.id, "uTime"); resources.composite.scale = fn.glGetUniformLocation(resources.composite.id, "uScale"); resources.composite.dither = fn.glGetUniformLocation(resources.composite.id, "uDither"); resources.blurDown.step = fn.glGetUniformLocation(resources.blurDown.id, "uStep"); @@ -411,9 +476,13 @@ void draw_bloom() noexcept { fn.glEnable(gl::BLEND); if (g_settings.lightSpill > 0.0f) { // framebuffer += framebuffer * light: surfaces near a light source get - // brighter and take its colour; black stays black. + // brighter and take its colour; black stays black. The light comes + // from a deep level of the chain, i.e. only the wide blurs, so its + // reach does not depend on how sharp the source sprite is. + const int wide = levels > 3 ? 2 : (levels > 1 ? 1 : 0); fn.glBlendFunc(gl::DST_COLOR, gl::ONE); - draw_composite(glow, g_settings.lightSpill * perLevel, 0.0f); + draw_composite(resources.bloomChain.levels[static_cast(wide)].texture, + g_settings.lightSpill * 3.0f * perLevel, 0.0f, true); } if (g_settings.bloomStrength > 0.0f) { fn.glBlendFunc(gl::ONE, gl::ONE); @@ -438,7 +507,7 @@ void draw_bloom() noexcept { void world_post_configure(const core::EngineConfig& cfg) noexcept { g_settings = {cfg.softFogOfWar, cfg.bloom, cfg.softFogRadius, cfg.bloomStrength, - cfg.lightSpill}; + cfg.lightSpill, cfg.softFogDrift}; } bool world_post_active() noexcept { @@ -514,7 +583,8 @@ void world_post_end_emissive(int commands) noexcept { fn.glBindFramebuffer(gl::READ_FRAMEBUFFER, static_cast(g_savedReadFramebuffer)); } -bool world_post_before_fog(float viewWorldWidth) noexcept { +bool world_post_before_fog(const WorldView& view) noexcept { + g_view = view; const auto& fn = gl::get_gl_functions(); g_worldPassOpen = false; if (!fn.postProcessAvailable) { @@ -535,7 +605,7 @@ bool world_post_before_fog(float viewWorldWidth) noexcept { fail("GL resources could not be created"); } else { g_pixelsPerWorldPixel = - game::pixels_per_world_pixel(static_cast(viewport[2]), viewWorldWidth); + game::pixels_per_world_pixel(static_cast(viewport[2]), view.width); set_pass_state(); if (g_settings.bloom) { draw_bloom(); @@ -587,8 +657,20 @@ void world_post_after_fog() noexcept { // framebuffer = framebuffer * visible fraction fn.glEnable(gl::BLEND); fn.glBlendFunc(gl::DST_COLOR, gl::ZERO); - draw_composite(blurred ? resources.fogChain.levels[0].texture : resources.fogCapture.texture, - 1.0f, blurred ? 1.0f : 0.0f); + if (blurred) { + fn.glUseProgram(resources.fog.id); + fn.glUniform2f(resources.fog.worldOrigin, g_view.scrollX, g_view.scrollY); + fn.glUniform2f(resources.fog.worldSize, g_view.width, g_view.height); + fn.glUniform1f(resources.fog.drift, g_settings.fogDrift); + // Wrapped hourly so the float keeps sub-frame precision. + fn.glUniform1f(resources.fog.time, + static_cast(GetTickCount64() % 3600000ULL) / 1000.0f); + fn.glBindTexture(gl::TEXTURE_2D, resources.fogChain.levels[0].texture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); + } else { + // Unblurred capture: must reproduce the engine's fog exactly. + draw_composite(resources.fogCapture.texture, 1.0f, 0.0f); + } if (!resources.fogDrawnOnce) { resources.fogDrawnOnce = true; if (!gl::check_error("world post fog composite")) { diff --git a/src/iee/features/world_post.h b/src/iee/features/world_post.h index b4258c6..c7cca1b 100644 --- a/src/iee/features/world_post.h +++ b/src/iee/features/world_post.h @@ -5,6 +5,14 @@ struct EngineConfig; } namespace iee::features { +// The part of the world the view shows, in world pixels. All zero when unknown. +struct WorldView { + float scrollX{}; + float scrollY{}; + float width{}; + float height{}; +}; + // World post passes: soft fog of war, and glow plus light spill from the // engine's light-emitting draws, run from the // CInfinity::RenderFog detour. The engine queues the whole frame and only @@ -38,8 +46,7 @@ void world_post_on_frame() noexcept; bool world_post_wants_emissive() noexcept; bool world_post_begin_emissive() noexcept; void world_post_end_emissive(int commands) noexcept; -// viewWorldWidth: world pixels visible across the view (0 when unknown). -bool world_post_before_fog(float viewWorldWidth) noexcept; +bool world_post_before_fog(const WorldView& view) noexcept; void world_post_after_fog() noexcept; // Any thread. The next frame logs what engaged. void world_post_on_area_load() noexcept; diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index b7f1d32..88a56c8 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -482,13 +482,15 @@ static void detour_render_fog(void* infinity, void* vidMode, void* visibility) { original(infinity, vidMode, visibility); return; } - float viewWorldWidth = 0.0f; + features::WorldView worldView{}; if (const auto* activeArea = g_ctx->activeArea.load()) { area::ViewTransform view{}; - if (area::read_view_transform(activeArea, view)) viewWorldWidth = view.viewWorldW; + if (area::read_view_transform(activeArea, view)) { + worldView = {view.scrollX, view.scrollY, view.viewWorldW, view.viewWorldH}; + } } g_drawFlush(); - const bool capturing = features::world_post_before_fog(viewWorldWidth); + const bool capturing = features::world_post_before_fog(worldView); original(infinity, vidMode, visibility); if (capturing) { g_drawFlush(); diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index df8dead..583bb4f 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1273,6 +1273,7 @@ void test_config_world_post() { expect_true(!defaults.softFogOfWar && !defaults.bloom, "World post effects default off"); expect_eq(defaults.softFogRadius, 24.0f, "Soft fog radius default"); expect_eq(defaults.lightSpill, 1.5f, "Light spill default"); + expect_eq(defaults.softFogDrift, 10.0f, "Soft fog drift default"); expect_eq(defaults.bloomStrength, 0.35f, "Bloom strength default"); const auto tempPath = @@ -1284,6 +1285,7 @@ void test_config_world_post() { out << "SoftFogRadius = 40\n"; out << "Bloom = true\n"; out << "LightSpill = 3\n"; + out << "SoftFogDrift = 20\n"; out << "BloomStrength = 0.5\n"; } iee::core::EngineConfig cfg{}; @@ -1291,6 +1293,7 @@ void test_config_world_post() { expect_true(cfg.softFogOfWar && cfg.bloom, "World post bools should parse"); expect_eq(cfg.softFogRadius, 40.0f, "Soft fog radius should parse"); expect_eq(cfg.lightSpill, 3.0f, "Light spill should parse"); + expect_eq(cfg.softFogDrift, 20.0f, "Soft fog drift should parse"); expect_eq(cfg.bloomStrength, 0.5f, "Bloom strength should parse"); { @@ -1298,12 +1301,14 @@ void test_config_world_post() { out << "[Rendering]\n"; out << "SoftFogRadius = 100000\n"; out << "LightSpill = 700\n"; + out << "SoftFogDrift = -4\n"; out << "BloomStrength = -3\n"; } cfg = {}; expect_true(iee::core::ConfigManager::load(tempPath, cfg), "Out-of-range values should load"); expect_eq(cfg.softFogRadius, 256.0f, "Soft fog radius is clamped to its maximum"); expect_eq(cfg.lightSpill, 8.0f, "Light spill is clamped to its maximum"); + expect_eq(cfg.softFogDrift, 0.0f, "Soft fog drift is clamped to 0"); expect_eq(cfg.bloomStrength, 0.0f, "Bloom strength is clamped to 0"); { diff --git a/tools/InfinityEngine-Enhancer.sample.ini b/tools/InfinityEngine-Enhancer.sample.ini index 228d02d..809c0ac 100644 --- a/tools/InfinityEngine-Enhancer.sample.ini +++ b/tools/InfinityEngine-Enhancer.sample.ini @@ -22,6 +22,8 @@ InterpolateAnimations = false ; F8 toggles it. SoftFogOfWar = false SoftFogRadius = 24 +; How far (world pixels) the soft edge drifts, like slow mist; 0 = still. +SoftFogDrift = 10 ; Glow around the engine's light-emitting effects (fires, torches, spells), ; never the UI or merely bright art. BloomStrength is the halo; LightSpill is ; how much those effects brighten and tint what is around them (0 = off). From 8334c3d1ae703c56ddc933358d8475121d9d7f51 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Wed, 7 Oct 2026 22:54:54 +0200 Subject: [PATCH 44/57] World post: smooth the fog over time so it no longer steps on the logic tick --- .../2026-10-07-soft-fog-and-bloom-design.md | 20 +++ src/iee/core/config.cpp | 5 + src/iee/core/config.h | 1 + src/iee/features/world_post.cpp | 114 +++++++++++++++++- src/iee/game/world_post_plan.cpp | 8 ++ src/iee/game/world_post_plan.h | 4 + tests/iee_tests.cpp | 16 +++ tools/InfinityEngine-Enhancer.sample.ini | 3 + 8 files changed, 166 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md index 0abd403..c69f2a8 100644 --- a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md +++ b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md @@ -295,3 +295,23 @@ for more. - **Light falloff.** The light spill now samples a deeper level of the bloom chain (wide blurs only) and maps it through `1 - exp(-x)`, so a small source lights a wider area and the area next to it saturates instead of clipping. + +## 13. Revision 2026-10-07 (later): fog smoothed over time + +Owner observation: the fog is "pinned to one tick". The engine recomputes +visibility on its logic tick, so the reveal steps while sprites and the camera +move at display rate. + +The blurred fog is now blended with the image shown on the previous frame +(`SoftFogSmoothing`, seconds, default 0.25, 0 = off): each frame the shown fog +moves `1 - exp(-dt / smoothing)` of the way to the new capture +(`game::temporal_blend`). The previous image is sampled at the same world +position, using the current and previous view transforms, so scrolling and +zooming do not smear it; positions that were off screen take the new value. +History is dropped on area load, on resize, after a gap of more than 0.5 s, +and whenever the view transform is unknown. The unblurred path +(`SoftFogRadius = 0`) is not smoothed and still reproduces the engine's fog. + +This relies on `area::read_view_transform` being correct at `RenderFog` time, +which is not yet confirmed in game: a wrong scroll would show as fog trailing +behind the camera. diff --git a/src/iee/core/config.cpp b/src/iee/core/config.cpp index 38d09a4..2c981bb 100644 --- a/src/iee/core/config.cpp +++ b/src/iee/core/config.cpp @@ -60,10 +60,12 @@ static void normalize(EngineConfig& cfg) noexcept { cfg.lodBias = std::clamp(cfg.lodBias, -4.0f, 4.0f); if (!std::isfinite(cfg.softFogRadius)) cfg.softFogRadius = 24.0f; if (!std::isfinite(cfg.softFogDrift)) cfg.softFogDrift = 10.0f; + if (!std::isfinite(cfg.softFogSmoothing)) cfg.softFogSmoothing = 0.25f; if (!std::isfinite(cfg.lightSpill)) cfg.lightSpill = 1.5f; if (!std::isfinite(cfg.bloomStrength)) cfg.bloomStrength = 0.35f; cfg.softFogRadius = std::clamp(cfg.softFogRadius, 0.0f, 256.0f); cfg.softFogDrift = std::clamp(cfg.softFogDrift, 0.0f, 64.0f); + cfg.softFogSmoothing = std::clamp(cfg.softFogSmoothing, 0.0f, 2.0f); cfg.lightSpill = std::clamp(cfg.lightSpill, 0.0f, 8.0f); cfg.bloomStrength = std::clamp(cfg.bloomStrength, 0.0f, 2.0f); } @@ -112,6 +114,8 @@ static void apply_kv(EngineConfig& cfg, const std::string& section, const std::s assign_float(cfg.softFogRadius); else if (iequals(key, "SoftFogDrift")) assign_float(cfg.softFogDrift); + else if (iequals(key, "SoftFogSmoothing")) + assign_float(cfg.softFogSmoothing); else if (iequals(key, "Bloom")) assign_bool(cfg.bloom); else if (iequals(key, "LightSpill")) @@ -218,6 +222,7 @@ bool ConfigManager::save(const std::filesystem::path& path, const EngineConfig& write_bool(f, "SoftFogOfWar", cfg.softFogOfWar); f << "SoftFogRadius = " << cfg.softFogRadius << "\n"; f << "SoftFogDrift = " << cfg.softFogDrift << "\n"; + f << "SoftFogSmoothing = " << cfg.softFogSmoothing << "\n"; write_bool(f, "Bloom", cfg.bloom); f << "LightSpill = " << cfg.lightSpill << "\n"; f << "BloomStrength = " << cfg.bloomStrength << "\n"; diff --git a/src/iee/core/config.h b/src/iee/core/config.h index 9b06362..c784807 100644 --- a/src/iee/core/config.h +++ b/src/iee/core/config.h @@ -12,6 +12,7 @@ struct EngineConfig { bool softFogOfWar = false; float softFogRadius = 24.0f; // world pixels; 0 = capture without blur float softFogDrift = 10.0f; // world pixels the soft edge drifts by; 0 = still + float softFogSmoothing = 0.25f; // seconds the fog takes to follow a visibility change bool bloom = false; float bloomStrength = 0.35f; // How strongly light-emitting effects brighten what is around them; 0 = off. diff --git a/src/iee/features/world_post.cpp b/src/iee/features/world_post.cpp index 9725f3f..2fbde22 100644 --- a/src/iee/features/world_post.cpp +++ b/src/iee/features/world_post.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include "iee/core/config.h" @@ -30,6 +31,17 @@ struct CompositeProgram { int curve{-1}; }; +// Blends the new fog image with the one shown last frame, looked up at the +// same place on the map. +struct FogTemporalProgram { + unsigned id{}; + int origin{-1}; + int size{-1}; + int previousOrigin{-1}; + int previousSize{-1}; + int blend{-1}; +}; + struct FogProgram { unsigned id{}; int worldOrigin{-1}; @@ -56,6 +68,7 @@ struct Settings { float bloomStrength{}; float lightSpill{}; float fogDrift{}; + float fogSmoothing{}; }; // Everything that lives and dies with one GL context. @@ -66,10 +79,17 @@ struct Resources { unsigned vertexBuffer{}; CompositeProgram composite{}; FogProgram fog{}; + FogTemporalProgram fogTemporal{}; BlurProgram blurDown{}; BlurProgram blurUp{}; Target fogCapture{}; BlurChain fogChain{}; + // The fog as shown last frame and the one being built, swapped each frame. + std::array fogHistory{}; + int fogHistoryCurrent{}; + bool fogHistoryValid{}; + WorldView fogHistoryView{}; + std::chrono::steady_clock::time_point fogHistoryTime{}; // The engine's additive (light-emitting) draws of this frame, on black. Target emissive{}; Target emissiveHalf{}; @@ -86,6 +106,7 @@ int g_engineReadFramebuffer = 0; float g_pixelsPerWorldPixel = 1.0f; WorldView g_view{}; std::atomic g_logNextFrame{true}; +std::atomic g_dropFogHistory{false}; // Frame bookkeeping for the emissive capture. The world pass runs from the // frame boundary to the fog; additive draws queued later belong to the UI. unsigned g_frame = 1; @@ -124,6 +145,33 @@ void main() { } )glsl"; +// The engine recomputes visibility on its logic tick, so the fog would step +// while everything else moves smoothly. This moves the shown fog a fraction +// (uBlend) of the way to the new one each frame. The previous image is read +// at the same world position, so scrolling and zooming do not smear it; +// where that position was off screen the new image is used as is. +constexpr const char* kFogTemporalSource = R"glsl(#version 330 +uniform sampler2D uTexture; +uniform sampler2D uHistory; +uniform vec2 uOrigin; +uniform vec2 uSize; +uniform vec2 uPreviousOrigin; +uniform vec2 uPreviousSize; +uniform float uBlend; +in vec2 vUv; +out vec4 fragColor; +void main() { + vec3 current = texture(uTexture, vUv).rgb; + vec2 world = uOrigin + vec2(vUv.x, 1.0 - vUv.y) * uSize; + vec2 previous = (world - uPreviousOrigin) / uPreviousSize; + previous.y = 1.0 - previous.y; + float inside = step(0.0, previous.x) * step(previous.x, 1.0) * step(0.0, previous.y) * + step(previous.y, 1.0); + vec3 history = texture(uHistory, previous).rgb; + fragColor = vec4(mix(current, history, inside * (1.0 - uBlend)), 1.0); +} +)glsl"; + // Multiplies the scene by the blurred fog capture (white = fully visible). // The lookup is pushed around by slow noise anchored to the world, so the // soft edge drifts like mist; where the capture is flat the push changes @@ -398,10 +446,19 @@ bool create_shared_resources() noexcept { resources.blurDown.id = build_program(kBlurDownSource); resources.blurUp.id = build_program(kBlurUpSource); resources.fog.id = build_program(kFogSource); + resources.fogTemporal.id = build_program(kFogTemporalSource); if (!resources.composite.id || !resources.blurDown.id || !resources.blurUp.id || - !resources.fog.id) { + !resources.fog.id || !resources.fogTemporal.id) { return false; } + auto& temporal = resources.fogTemporal; + temporal.origin = fn.glGetUniformLocation(temporal.id, "uOrigin"); + temporal.size = fn.glGetUniformLocation(temporal.id, "uSize"); + temporal.previousOrigin = fn.glGetUniformLocation(temporal.id, "uPreviousOrigin"); + temporal.previousSize = fn.glGetUniformLocation(temporal.id, "uPreviousSize"); + temporal.blend = fn.glGetUniformLocation(temporal.id, "uBlend"); + fn.glUseProgram(temporal.id); + fn.glUniform1i(fn.glGetUniformLocation(temporal.id, "uHistory"), 1); resources.composite.curve = fn.glGetUniformLocation(resources.composite.id, "uCurve"); resources.fog.worldOrigin = fn.glGetUniformLocation(resources.fog.id, "uWorldOrigin"); resources.fog.worldSize = fn.glGetUniformLocation(resources.fog.id, "uWorldSize"); @@ -419,6 +476,10 @@ bool create_sized_resources(game::Extent viewport) noexcept { const auto half = game::half_extent(viewport); if (!create_target(resources.fogCapture, viewport, gl::RGBA8, gl::UNSIGNED_BYTE)) return false; if (!create_chain(resources.fogChain, viewport)) return false; + for (auto& history : resources.fogHistory) { + if (!create_target(history, half, gl::RGBA16F, gl::HALF_FLOAT)) return false; + } + resources.fogHistoryValid = false; if (!create_target(resources.emissive, viewport, gl::RGBA8, gl::UNSIGNED_BYTE)) return false; if (!create_target(resources.emissiveHalf, half, gl::RGBA16F, gl::HALF_FLOAT)) return false; if (!create_chain(resources.bloomChain, half)) return false; @@ -507,7 +568,7 @@ void draw_bloom() noexcept { void world_post_configure(const core::EngineConfig& cfg) noexcept { g_settings = {cfg.softFogOfWar, cfg.bloom, cfg.softFogRadius, cfg.bloomStrength, - cfg.lightSpill, cfg.softFogDrift}; + cfg.lightSpill, cfg.softFogDrift, cfg.softFogSmoothing}; } bool world_post_active() noexcept { @@ -646,12 +707,52 @@ bool world_post_before_fog(const WorldView& view) noexcept { void world_post_after_fog() noexcept { const auto& fn = gl::get_gl_functions(); auto& resources = g_resources; - core::GlStateGuard textures({0}); + core::GlStateGuard textures({0, 1}); core::GlPassGuard pass; set_pass_state(); const auto plan = game::blur_plan_for_radius(g_settings.fogRadius * g_pixelsPerWorldPixel, resources.viewport); const bool blurred = run_blur(resources.fogCapture, resources.fogChain, plan, false) > 0; + unsigned fogTexture = resources.fogChain.levels[0].texture; + const bool viewKnown = g_view.width > 0.0f && g_view.height > 0.0f; + if (g_dropFogHistory.exchange(false, std::memory_order_relaxed)) { + resources.fogHistoryValid = false; + } + if (blurred && viewKnown && g_settings.fogSmoothing > 0.0f) { + const auto now = std::chrono::steady_clock::now(); + const float step = std::chrono::duration(now - resources.fogHistoryTime).count(); + const float blend = resources.fogHistoryValid + ? game::temporal_blend(step, g_settings.fogSmoothing) + : 1.0f; + const auto& shown = resources.fogHistory[static_cast(resources.fogHistoryCurrent)]; + const int next = 1 - resources.fogHistoryCurrent; + const auto& target = resources.fogHistory[static_cast(next)]; + const auto& temporal = resources.fogTemporal; + fn.glBindFramebuffer(gl::FRAMEBUFFER, target.framebuffer); + fn.glViewport(0, 0, target.extent.width, target.extent.height); + fn.glUseProgram(temporal.id); + fn.glUniform2f(temporal.origin, g_view.scrollX, g_view.scrollY); + fn.glUniform2f(temporal.size, g_view.width, g_view.height); + fn.glUniform2f(temporal.previousOrigin, resources.fogHistoryView.scrollX, + resources.fogHistoryView.scrollY); + // A zero size would divide by zero; with blend 1 the history is unused. + fn.glUniform2f(temporal.previousSize, + resources.fogHistoryValid ? resources.fogHistoryView.width : 1.0f, + resources.fogHistoryValid ? resources.fogHistoryView.height : 1.0f); + fn.glUniform1f(temporal.blend, blend); + fn.glActiveTexture(gl::TEXTURE0 + 1); + fn.glBindTexture(gl::TEXTURE_2D, shown.texture); + fn.glActiveTexture(gl::TEXTURE0); + fn.glBindTexture(gl::TEXTURE_2D, fogTexture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); + resources.fogHistoryCurrent = next; + resources.fogHistoryValid = true; + resources.fogHistoryView = g_view; + resources.fogHistoryTime = now; + fogTexture = target.texture; + } else { + resources.fogHistoryValid = false; + } bind_engine_framebuffer(); fn.glViewport(0, 0, resources.viewport.width, resources.viewport.height); // framebuffer = framebuffer * visible fraction @@ -665,7 +766,7 @@ void world_post_after_fog() noexcept { // Wrapped hourly so the float keeps sub-frame precision. fn.glUniform1f(resources.fog.time, static_cast(GetTickCount64() % 3600000ULL) / 1000.0f); - fn.glBindTexture(gl::TEXTURE_2D, resources.fogChain.levels[0].texture); + fn.glBindTexture(gl::TEXTURE_2D, fogTexture); fn.glDrawArrays(gl::TRIANGLES, 0, 3); } else { // Unblurred capture: must reproduce the engine's fog exactly. @@ -684,7 +785,10 @@ void world_post_after_fog() noexcept { } } -void world_post_on_area_load() noexcept { g_logNextFrame.store(true, std::memory_order_relaxed); } +void world_post_on_area_load() noexcept { + g_logNextFrame.store(true, std::memory_order_relaxed); + g_dropFogHistory.store(true, std::memory_order_relaxed); +} void world_post_forget() noexcept { g_resources = {}; } } // namespace iee::features diff --git a/src/iee/game/world_post_plan.cpp b/src/iee/game/world_post_plan.cpp index 9fe62ec..3c2ae2e 100644 --- a/src/iee/game/world_post_plan.cpp +++ b/src/iee/game/world_post_plan.cpp @@ -39,6 +39,14 @@ int bloom_levels(Extent base) noexcept { return std::min(kMaxBloomLevels, available_blur_levels(base)); } +float temporal_blend(float stepSeconds, float smoothingSeconds) noexcept { + constexpr float kStaleSeconds = 0.5f; + if (!(smoothingSeconds > 0.0f)) return 1.0f; + // Written so NaN drops the history. + if (!(stepSeconds >= 0.0f) || stepSeconds > kStaleSeconds) return 1.0f; + return 1.0f - std::exp(-stepSeconds / smoothingSeconds); +} + float pixels_per_world_pixel(float viewportWidth, float viewWorldWidth) noexcept { if (!std::isfinite(viewportWidth) || !std::isfinite(viewWorldWidth)) return 1.0f; if (!(viewportWidth > 0.0f) || !(viewWorldWidth > 0.0f)) return 1.0f; diff --git a/src/iee/game/world_post_plan.h b/src/iee/game/world_post_plan.h index 82aba06..9638c22 100644 --- a/src/iee/game/world_post_plan.h +++ b/src/iee/game/world_post_plan.h @@ -25,6 +25,10 @@ int available_blur_levels(Extent base) noexcept; // The plan whose blur reaches about `radiusPixels` on an image of `base` size. BlurPlan blur_plan_for_radius(float radiusPixels, Extent base) noexcept; int bloom_levels(Extent base) noexcept; +// How far to move the shown image towards the new one after `stepSeconds`, +// for an exponential approach with time constant `smoothingSeconds`. 1 means +// "show the new image": no smoothing, or a history too old to trust. +float temporal_blend(float stepSeconds, float smoothingSeconds) noexcept; // Screen pixels per world pixel at the current zoom; 1 when either is unknown. float pixels_per_world_pixel(float viewportWidth, float viewWorldWidth) noexcept; } // namespace iee::game diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 583bb4f..6fd9b1c 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1274,6 +1274,7 @@ void test_config_world_post() { expect_eq(defaults.softFogRadius, 24.0f, "Soft fog radius default"); expect_eq(defaults.lightSpill, 1.5f, "Light spill default"); expect_eq(defaults.softFogDrift, 10.0f, "Soft fog drift default"); + expect_eq(defaults.softFogSmoothing, 0.25f, "Soft fog smoothing default"); expect_eq(defaults.bloomStrength, 0.35f, "Bloom strength default"); const auto tempPath = @@ -1286,6 +1287,7 @@ void test_config_world_post() { out << "Bloom = true\n"; out << "LightSpill = 3\n"; out << "SoftFogDrift = 20\n"; + out << "SoftFogSmoothing = 0.5\n"; out << "BloomStrength = 0.5\n"; } iee::core::EngineConfig cfg{}; @@ -1294,6 +1296,7 @@ void test_config_world_post() { expect_eq(cfg.softFogRadius, 40.0f, "Soft fog radius should parse"); expect_eq(cfg.lightSpill, 3.0f, "Light spill should parse"); expect_eq(cfg.softFogDrift, 20.0f, "Soft fog drift should parse"); + expect_eq(cfg.softFogSmoothing, 0.5f, "Soft fog smoothing should parse"); expect_eq(cfg.bloomStrength, 0.5f, "Bloom strength should parse"); { @@ -1302,6 +1305,7 @@ void test_config_world_post() { out << "SoftFogRadius = 100000\n"; out << "LightSpill = 700\n"; out << "SoftFogDrift = -4\n"; + out << "SoftFogSmoothing = 99\n"; out << "BloomStrength = -3\n"; } cfg = {}; @@ -1309,6 +1313,7 @@ void test_config_world_post() { expect_eq(cfg.softFogRadius, 256.0f, "Soft fog radius is clamped to its maximum"); expect_eq(cfg.lightSpill, 8.0f, "Light spill is clamped to its maximum"); expect_eq(cfg.softFogDrift, 0.0f, "Soft fog drift is clamped to 0"); + expect_eq(cfg.softFogSmoothing, 2.0f, "Soft fog smoothing is clamped to its maximum"); expect_eq(cfg.bloomStrength, 0.0f, "Bloom strength is clamped to 0"); { @@ -1437,6 +1442,17 @@ void test_world_post_plan() { expect_eq(bloom_levels({960, 540}), 5, "Bloom uses at most five levels"); expect_eq(bloom_levels({40, 40}), 2, "Bloom levels shrink with the image"); + expect_eq(temporal_blend(0.1f, 0.0f), 1.0f, "No smoothing time means the new image is shown at once"); + expect_eq(temporal_blend(0.0f, 0.25f), 0.0f, "No elapsed time keeps the shown image"); + expect_true(std::abs(temporal_blend(0.25f, 0.25f) - 0.6321f) < 0.001f, + "One smoothing time moves 63% of the way to the new image"); + expect_true(temporal_blend(0.01f, 0.25f) < temporal_blend(0.02f, 0.25f), + "A longer frame moves further"); + expect_eq(temporal_blend(0.8f, 0.25f), 1.0f, "After a long gap the history is stale and dropped"); + expect_eq(temporal_blend(-1.0f, 0.25f), 1.0f, "A negative step drops the history"); + expect_eq(temporal_blend(std::numeric_limits::quiet_NaN(), 0.25f), 1.0f, + "A NaN step drops the history"); + expect_eq(pixels_per_world_pixel(3840.0f, 1920.0f), 2.0f, "Scale is viewport over world width"); expect_eq(pixels_per_world_pixel(3840.0f, 0.0f), 1.0f, "An unknown world width falls back to 1"); expect_eq(pixels_per_world_pixel(0.0f, 1920.0f), 1.0f, "An unknown viewport falls back to 1"); diff --git a/tools/InfinityEngine-Enhancer.sample.ini b/tools/InfinityEngine-Enhancer.sample.ini index 809c0ac..e7ac8ac 100644 --- a/tools/InfinityEngine-Enhancer.sample.ini +++ b/tools/InfinityEngine-Enhancer.sample.ini @@ -24,6 +24,9 @@ SoftFogOfWar = false SoftFogRadius = 24 ; How far (world pixels) the soft edge drifts, like slow mist; 0 = still. SoftFogDrift = 10 +; Seconds the fog takes to follow a visibility change, so it fades instead of +; stepping on the engine's logic tick; 0 = follow at once. +SoftFogSmoothing = 0.25 ; Glow around the engine's light-emitting effects (fires, torches, spells), ; never the UI or merely bright art. BloomStrength is the halo; LightSpill is ; how much those effects brighten and tint what is around them (0 = off). From a64abebb6d151496cfe8f7e4e81f8213094ea21c Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Wed, 7 Oct 2026 23:07:44 +0200 Subject: [PATCH 45/57] World post: heat shimmer above warm light sources --- .../2026-10-07-soft-fog-and-bloom-design.md | 20 +++ src/iee/core/config.cpp | 5 + src/iee/core/config.h | 2 + src/iee/features/world_post.cpp | 134 +++++++++++++++--- tests/iee_tests.cpp | 5 + tools/InfinityEngine-Enhancer.sample.ini | 3 + 6 files changed, 149 insertions(+), 20 deletions(-) diff --git a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md index c69f2a8..60d3639 100644 --- a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md +++ b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md @@ -315,3 +315,23 @@ and whenever the view transform is unknown. The unblurred path This relies on `area::read_view_transform` being correct at `RenderFog` time, which is not yet confirmed in game: a wrong scroll would show as fog trailing behind the camera. + +## 14. Revision 2026-10-07 (later): heat shimmer + +Owner verdict on section 13: fog smoothing is "perfect". Next pick: heat haze. +Rejected by the owner: smoothing day/night transitions (areas swap maps, so +there is nothing to blend) and radius-based light sources with wall blocking +(not convinced it would work). + +`HeatShimmer` (world pixels, default 2.5, 0 = off, needs `Bloom`): after the +emissive blur and before the glow is added, the world image is copied and +redrawn with its lookup rippled by world-anchored, upward-flowing noise. The +ripple is scaled by how warm the blurred emissive image is (red minus blue), +read at the pixel and at two points below it so the haze sits above its +source. Cold and white effects do not shimmer. + +All shaders are checked offline with `glslangValidator` (extract the +`R"glsl(...)glsl"` bodies from `world_post.cpp`; append the shared noise +source to those that declare `float noise(vec2 p);`). A shader that fails to +compile in game switches fog and bloom off for the session, so run this +before pushing shader changes. diff --git a/src/iee/core/config.cpp b/src/iee/core/config.cpp index 2c981bb..71d7202 100644 --- a/src/iee/core/config.cpp +++ b/src/iee/core/config.cpp @@ -62,11 +62,13 @@ static void normalize(EngineConfig& cfg) noexcept { if (!std::isfinite(cfg.softFogDrift)) cfg.softFogDrift = 10.0f; if (!std::isfinite(cfg.softFogSmoothing)) cfg.softFogSmoothing = 0.25f; if (!std::isfinite(cfg.lightSpill)) cfg.lightSpill = 1.5f; + if (!std::isfinite(cfg.heatShimmer)) cfg.heatShimmer = 2.5f; if (!std::isfinite(cfg.bloomStrength)) cfg.bloomStrength = 0.35f; cfg.softFogRadius = std::clamp(cfg.softFogRadius, 0.0f, 256.0f); cfg.softFogDrift = std::clamp(cfg.softFogDrift, 0.0f, 64.0f); cfg.softFogSmoothing = std::clamp(cfg.softFogSmoothing, 0.0f, 2.0f); cfg.lightSpill = std::clamp(cfg.lightSpill, 0.0f, 8.0f); + cfg.heatShimmer = std::clamp(cfg.heatShimmer, 0.0f, 12.0f); cfg.bloomStrength = std::clamp(cfg.bloomStrength, 0.0f, 2.0f); } @@ -120,6 +122,8 @@ static void apply_kv(EngineConfig& cfg, const std::string& section, const std::s assign_bool(cfg.bloom); else if (iequals(key, "LightSpill")) assign_float(cfg.lightSpill); + else if (iequals(key, "HeatShimmer")) + assign_float(cfg.heatShimmer); else if (iequals(key, "BloomStrength")) assign_float(cfg.bloomStrength); return; @@ -225,6 +229,7 @@ bool ConfigManager::save(const std::filesystem::path& path, const EngineConfig& f << "SoftFogSmoothing = " << cfg.softFogSmoothing << "\n"; write_bool(f, "Bloom", cfg.bloom); f << "LightSpill = " << cfg.lightSpill << "\n"; + f << "HeatShimmer = " << cfg.heatShimmer << "\n"; f << "BloomStrength = " << cfg.bloomStrength << "\n"; write_section(f, "Detection"); diff --git a/src/iee/core/config.h b/src/iee/core/config.h index c784807..856b7fe 100644 --- a/src/iee/core/config.h +++ b/src/iee/core/config.h @@ -17,6 +17,8 @@ struct EngineConfig { float bloomStrength = 0.35f; // How strongly light-emitting effects brighten what is around them; 0 = off. float lightSpill = 1.5f; + // World pixels the air above warm light sources ripples by; 0 = off. + float heatShimmer = 2.5f; bool dumpEngineShaders = false; bool enableDebugHotkeys = false; diff --git a/src/iee/features/world_post.cpp b/src/iee/features/world_post.cpp index 2fbde22..b2817df 100644 --- a/src/iee/features/world_post.cpp +++ b/src/iee/features/world_post.cpp @@ -42,6 +42,14 @@ struct FogTemporalProgram { int blend{-1}; }; +struct ShimmerProgram { + unsigned id{}; + int worldOrigin{-1}; + int worldSize{-1}; + int strength{-1}; + int time{-1}; +}; + struct FogProgram { unsigned id{}; int worldOrigin{-1}; @@ -69,6 +77,7 @@ struct Settings { float lightSpill{}; float fogDrift{}; float fogSmoothing{}; + float heatShimmer{}; }; // Everything that lives and dies with one GL context. @@ -80,6 +89,7 @@ struct Resources { CompositeProgram composite{}; FogProgram fog{}; FogTemporalProgram fogTemporal{}; + ShimmerProgram shimmer{}; BlurProgram blurDown{}; BlurProgram blurUp{}; Target fogCapture{}; @@ -94,6 +104,7 @@ struct Resources { Target emissive{}; Target emissiveHalf{}; BlurChain bloomChain{}; + Target sceneCopy{}; // the world image, read back while it is redrawn rippled bool fogDrawnOnce{}; bool bloomDrawnOnce{}; }; @@ -125,6 +136,22 @@ void main() { } )glsl"; +// Smooth 2D value noise in [0, 1], shared by the shaders that animate. It is +// handed to the compiler as a second source string after the shader that +// declares `float noise(vec2 p);`. +constexpr const char* kValueNoiseSource = R"glsl( +float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} +float noise(vec2 p) { + vec2 cell = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(cell), hash(cell + vec2(1.0, 0.0)), f.x), + mix(hash(cell + vec2(0.0, 1.0)), hash(cell + vec2(1.0, 1.0)), f.x), f.y); +} +)glsl"; + // Writes the texture scaled by uScale; the caller's blend mode decides how it // lands. uDither adds +-0.5/255 of ordered noise to hide banding in long dark // gradients. With uCurve set the value is treated as light and mapped through @@ -184,16 +211,7 @@ uniform float uDrift; uniform float uTime; in vec2 vUv; out vec4 fragColor; -float hash(vec2 p) { - return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); -} -float noise(vec2 p) { - vec2 cell = floor(p); - vec2 f = fract(p); - f = f * f * (3.0 - 2.0 * f); - return mix(mix(hash(cell), hash(cell + vec2(1.0, 0.0)), f.x), - mix(hash(cell + vec2(0.0, 1.0)), hash(cell + vec2(1.0, 1.0)), f.x), f.y); -} +float noise(vec2 p); void main() { vec2 uv = vUv; if (uDrift > 0.0 && uWorldSize.x > 0.0 && uWorldSize.y > 0.0) { @@ -211,6 +229,37 @@ void main() { } )glsl"; +// Heat haze. Redraws the scene with its lookup rippled where the blurred +// emissive image (uMask) is warm, i.e. redder than it is blue, so fires and +// torches shimmer and cold or white magic does not. The mask is also read +// from below each pixel, because hot air rises above its source. The ripple +// is anchored to the world and flows upward. +constexpr const char* kShimmerSource = R"glsl(#version 330 +uniform sampler2D uTexture; +uniform sampler2D uMask; +uniform vec2 uWorldOrigin; +uniform vec2 uWorldSize; +uniform float uStrength; +uniform float uTime; +in vec2 vUv; +out vec4 fragColor; +float noise(vec2 p); +float warmth(vec2 uv) { + vec3 light = texture(uMask, uv).rgb; + return light.r - light.b; +} +void main() { + vec2 rise = vec2(0.0, 26.0 / uWorldSize.y); + float heat = max(warmth(vUv), max(0.8 * warmth(vUv - rise), 0.5 * warmth(vUv - 2.0 * rise))); + heat = clamp(heat * 4.0, 0.0, 1.0); + vec2 world = uWorldOrigin + vec2(vUv.x, 1.0 - vUv.y) * uWorldSize; + vec2 p = world / 9.0; + vec2 ripple = vec2(noise(p + vec2(0.0, uTime * 2.2)), noise(p * 1.3 + vec2(7.0, uTime * 2.8))) - 0.5; + vec2 offset = ripple * 2.0 * uStrength * heat / uWorldSize; + fragColor = vec4(texture(uTexture, vUv + offset).rgb, 1.0); +} +)glsl"; + // Dual-filter blur (down then up through half-size levels). uStep is the // sample spread in source UV units. constexpr const char* kBlurDownSource = R"glsl(#version 330 @@ -254,11 +303,14 @@ void fail(const char* reason) noexcept { } } -unsigned compile_shader(unsigned type, const char* source) noexcept { +// `appended` is an optional second source string (shared helper functions). +unsigned compile_shader(unsigned type, const char* source, + const char* appended = nullptr) noexcept { const auto& fn = gl::get_gl_functions(); const unsigned shader = fn.glCreateShader(type); if (!shader) return 0; - fn.glShaderSource(shader, 1, &source, nullptr); + const char* sources[2] = {source, appended}; + fn.glShaderSource(shader, appended ? 2 : 1, sources, nullptr); fn.glCompileShader(shader); int status = 0; fn.glGetShaderiv(shader, gl::COMPILE_STATUS, &status); @@ -275,10 +327,10 @@ unsigned compile_shader(unsigned type, const char* source) noexcept { return shader; } -unsigned build_program(const char* fragmentSource) noexcept { +unsigned build_program(const char* fragmentSource, const char* appended = nullptr) noexcept { const auto& fn = gl::get_gl_functions(); const unsigned vertex = compile_shader(gl::VERTEX_SHADER, kVertexSource); - const unsigned fragment = compile_shader(gl::FRAGMENT_SHADER, fragmentSource); + const unsigned fragment = compile_shader(gl::FRAGMENT_SHADER, fragmentSource, appended); unsigned program = 0; if (vertex && fragment) { program = fn.glCreateProgram(); @@ -379,6 +431,11 @@ void set_pass_state() noexcept { fn.glActiveTexture(gl::TEXTURE0); } +// Wrapped hourly so the float keeps sub-frame precision. +float animation_seconds() noexcept { + return static_cast(GetTickCount64() % 3600000ULL) / 1000.0f; +} + void draw_composite(unsigned texture, float scale, float dither, bool lightCurve = false) noexcept { const auto& fn = gl::get_gl_functions(); fn.glUseProgram(g_resources.composite.id); @@ -445,12 +502,21 @@ bool create_shared_resources() noexcept { resources.composite.id = build_program(kCompositeSource); resources.blurDown.id = build_program(kBlurDownSource); resources.blurUp.id = build_program(kBlurUpSource); - resources.fog.id = build_program(kFogSource); + resources.fog.id = build_program(kFogSource, kValueNoiseSource); resources.fogTemporal.id = build_program(kFogTemporalSource); if (!resources.composite.id || !resources.blurDown.id || !resources.blurUp.id || !resources.fog.id || !resources.fogTemporal.id) { return false; } + resources.shimmer.id = build_program(kShimmerSource, kValueNoiseSource); + if (!resources.shimmer.id) return false; + auto& shimmer = resources.shimmer; + shimmer.worldOrigin = fn.glGetUniformLocation(shimmer.id, "uWorldOrigin"); + shimmer.worldSize = fn.glGetUniformLocation(shimmer.id, "uWorldSize"); + shimmer.strength = fn.glGetUniformLocation(shimmer.id, "uStrength"); + shimmer.time = fn.glGetUniformLocation(shimmer.id, "uTime"); + fn.glUseProgram(shimmer.id); + fn.glUniform1i(fn.glGetUniformLocation(shimmer.id, "uMask"), 1); auto& temporal = resources.fogTemporal; temporal.origin = fn.glGetUniformLocation(temporal.id, "uOrigin"); temporal.size = fn.glGetUniformLocation(temporal.id, "uSize"); @@ -482,6 +548,7 @@ bool create_sized_resources(game::Extent viewport) noexcept { resources.fogHistoryValid = false; if (!create_target(resources.emissive, viewport, gl::RGBA8, gl::UNSIGNED_BYTE)) return false; if (!create_target(resources.emissiveHalf, half, gl::RGBA16F, gl::HALF_FLOAT)) return false; + if (!create_target(resources.sceneCopy, viewport, gl::RGBA8, gl::UNSIGNED_BYTE)) return false; if (!create_chain(resources.bloomChain, half)) return false; resources.viewport = viewport; return true; @@ -527,6 +594,35 @@ void draw_bloom() noexcept { const int levels = run_blur(resources.emissiveHalf, resources.bloomChain, {planned, 1.0f}, true); + if (levels > 0 && g_settings.heatShimmer > 0.0f) { + // Before the glow is added, so the haze bends the scene and not the halo. + fn.glBindFramebuffer(gl::READ_FRAMEBUFFER, static_cast(g_engineDrawFramebuffer)); + fn.glBindFramebuffer(gl::DRAW_FRAMEBUFFER, resources.sceneCopy.framebuffer); + fn.glBlitFramebuffer(0, 0, resources.viewport.width, resources.viewport.height, 0, 0, + resources.viewport.width, resources.viewport.height, + gl::COLOR_BUFFER_BIT, gl::NEAREST); + bind_engine_framebuffer(); + fn.glViewport(0, 0, resources.viewport.width, resources.viewport.height); + // Without the view transform, fall back to screen pixels as "world". + const bool viewKnown = g_view.width > 0.0f && g_view.height > 0.0f; + const auto& shimmer = resources.shimmer; + const int mask = levels > 1 ? 1 : 0; + fn.glUseProgram(shimmer.id); + fn.glUniform2f(shimmer.worldOrigin, viewKnown ? g_view.scrollX : 0.0f, + viewKnown ? g_view.scrollY : 0.0f); + fn.glUniform2f(shimmer.worldSize, + viewKnown ? g_view.width : static_cast(resources.viewport.width), + viewKnown ? g_view.height : static_cast(resources.viewport.height)); + fn.glUniform1f(shimmer.strength, g_settings.heatShimmer); + fn.glUniform1f(shimmer.time, animation_seconds()); + fn.glActiveTexture(gl::TEXTURE0 + 1); + fn.glBindTexture(gl::TEXTURE_2D, + resources.bloomChain.levels[static_cast(mask)].texture); + fn.glActiveTexture(gl::TEXTURE0); + fn.glBindTexture(gl::TEXTURE_2D, resources.sceneCopy.texture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); + } + bind_engine_framebuffer(); fn.glViewport(0, 0, resources.viewport.width, resources.viewport.height); if (levels > 0) { @@ -568,7 +664,7 @@ void draw_bloom() noexcept { void world_post_configure(const core::EngineConfig& cfg) noexcept { g_settings = {cfg.softFogOfWar, cfg.bloom, cfg.softFogRadius, cfg.bloomStrength, - cfg.lightSpill, cfg.softFogDrift, cfg.softFogSmoothing}; + cfg.lightSpill, cfg.softFogDrift, cfg.softFogSmoothing, cfg.heatShimmer}; } bool world_post_active() noexcept { @@ -657,7 +753,7 @@ bool world_post_before_fog(const WorldView& view) noexcept { bool capturing = false; { - core::GlStateGuard textures({0}); + core::GlStateGuard textures({0, 1}); core::GlPassGuard pass; const int* viewport = pass.viewport(); if (viewport[0] != 0 || viewport[1] != 0 || viewport[2] <= 0 || viewport[3] <= 0) { @@ -763,9 +859,7 @@ void world_post_after_fog() noexcept { fn.glUniform2f(resources.fog.worldOrigin, g_view.scrollX, g_view.scrollY); fn.glUniform2f(resources.fog.worldSize, g_view.width, g_view.height); fn.glUniform1f(resources.fog.drift, g_settings.fogDrift); - // Wrapped hourly so the float keeps sub-frame precision. - fn.glUniform1f(resources.fog.time, - static_cast(GetTickCount64() % 3600000ULL) / 1000.0f); + fn.glUniform1f(resources.fog.time, animation_seconds()); fn.glBindTexture(gl::TEXTURE_2D, fogTexture); fn.glDrawArrays(gl::TRIANGLES, 0, 3); } else { diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 6fd9b1c..9e72756 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1275,6 +1275,7 @@ void test_config_world_post() { expect_eq(defaults.lightSpill, 1.5f, "Light spill default"); expect_eq(defaults.softFogDrift, 10.0f, "Soft fog drift default"); expect_eq(defaults.softFogSmoothing, 0.25f, "Soft fog smoothing default"); + expect_eq(defaults.heatShimmer, 2.5f, "Heat shimmer default"); expect_eq(defaults.bloomStrength, 0.35f, "Bloom strength default"); const auto tempPath = @@ -1288,6 +1289,7 @@ void test_config_world_post() { out << "LightSpill = 3\n"; out << "SoftFogDrift = 20\n"; out << "SoftFogSmoothing = 0.5\n"; + out << "HeatShimmer = 4\n"; out << "BloomStrength = 0.5\n"; } iee::core::EngineConfig cfg{}; @@ -1297,6 +1299,7 @@ void test_config_world_post() { expect_eq(cfg.lightSpill, 3.0f, "Light spill should parse"); expect_eq(cfg.softFogDrift, 20.0f, "Soft fog drift should parse"); expect_eq(cfg.softFogSmoothing, 0.5f, "Soft fog smoothing should parse"); + expect_eq(cfg.heatShimmer, 4.0f, "Heat shimmer should parse"); expect_eq(cfg.bloomStrength, 0.5f, "Bloom strength should parse"); { @@ -1306,6 +1309,7 @@ void test_config_world_post() { out << "LightSpill = 700\n"; out << "SoftFogDrift = -4\n"; out << "SoftFogSmoothing = 99\n"; + out << "HeatShimmer = 500\n"; out << "BloomStrength = -3\n"; } cfg = {}; @@ -1314,6 +1318,7 @@ void test_config_world_post() { expect_eq(cfg.lightSpill, 8.0f, "Light spill is clamped to its maximum"); expect_eq(cfg.softFogDrift, 0.0f, "Soft fog drift is clamped to 0"); expect_eq(cfg.softFogSmoothing, 2.0f, "Soft fog smoothing is clamped to its maximum"); + expect_eq(cfg.heatShimmer, 12.0f, "Heat shimmer is clamped to its maximum"); expect_eq(cfg.bloomStrength, 0.0f, "Bloom strength is clamped to 0"); { diff --git a/tools/InfinityEngine-Enhancer.sample.ini b/tools/InfinityEngine-Enhancer.sample.ini index e7ac8ac..1195c8a 100644 --- a/tools/InfinityEngine-Enhancer.sample.ini +++ b/tools/InfinityEngine-Enhancer.sample.ini @@ -34,6 +34,9 @@ SoftFogSmoothing = 0.25 Bloom = false BloomStrength = 0.35 LightSpill = 1.5 +; Heat haze above fires and other warm light sources: how far (world pixels) +; the air ripples; 0 = off. Needs Bloom. +HeatShimmer = 2.5 [Detection] ; Collect the current area's authored ambient animations from engine memory From c55f98798f9a95618742579a2ce9a24e0463e872 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Wed, 7 Oct 2026 23:18:11 +0200 Subject: [PATCH 46/57] Smooth rain, snow, sparkles and floating text between logic ticks; lower the heat shimmer default --- .../2026-10-07-soft-fog-and-bloom-design.md | 32 ++++++++ src/iee/core/config.cpp | 2 +- src/iee/core/config.h | 2 +- src/iee/game/build_manifest.cpp | 17 +++- src/iee/game/build_manifest.h | 9 ++- src/iee/game/game_addrs.cpp | 9 +++ src/iee/game/game_addrs.h | 8 +- src/iee/game/runtime_types_x64.h | 25 ++++++ src/iee/game/sprite_motion.cpp | 31 ++++++++ src/iee/game/sprite_motion.h | 45 +++++++++++ src/iee/hooks.cpp | 77 +++++++++++++++++++ tests/iee_tests.cpp | 57 +++++++++++++- tools/InfinityEngine-Enhancer.sample.ini | 5 +- 13 files changed, 310 insertions(+), 9 deletions(-) diff --git a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md index 60d3639..39d0281 100644 --- a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md +++ b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md @@ -335,3 +335,35 @@ All shaders are checked offline with `glslangValidator` (extract the source to those that declare `float noise(vec2 p);`). A shader that fails to compile in game switches fog and bloom off for the session, so run this before pushing shader changes. + +## 15. Revision 2026-10-07 (later): what else is stepped on the logic tick + +Owner request: find everything pinned to the engine tick and decouple it. +Survey of the world-pass draws against their per-tick updates (2.7.3): + +| Thing | Per-tick update | Status | +|---|---|---| +| Creatures, circles, health bars, 8 projectile/effect types | `m_pos` | Smoothed earlier (item A) | +| Fog of war | visibility map | Smoothed (section 13) | +| Rain, snow, sparkles | `CParticle::AsynchronousUpdate` (`0x423C30`) adds velocity to position | **Smoothed now** | +| Floating text over creatures | `CGameText::AIUpdate` copies the target's `m_pos` | **Smoothed now** | +| Selection-circle pulse, talk markers | `CMarker::AsynchronousUpdate` counters | Not done: a size animation, not a position; low value | +| `CProjectileSkyStrikeBAM`, spell-hit and travel-door projectiles | not drawn from `m_pos` | Not done | +| Animation frames of effects and ambient animations | frame counters | Needs in-between art (paused item F) | +| Screen fades, day/night tint | stepped values | Not done; day/night rejected by the owner | +| Camera scroll | — | Already at display rate under EEex | + +Particles: `CParticle::Render` (`0x425BE0`; called by `CVidMode::BKRender` +for rain and snow and by `CSparkleCluster::Render`) is wrapped. A `TickClock` +fed from the particle update gives the fraction of the current tick elapsed; +the particle is drawn at `position - velocity * (1 - fraction)`, which is +exact because the update is a plain add. Gravity-only particles step back by +their gravity in height. The position is restored after the engine call +unless the engine changed it meanwhile. Layout mirrored as `game::CParticle`. + +Floating text: `CGameText::Render` (`0x1F39A0`) joins the smoothed-object +table, so it slides with its creature. + +Both ride on `SmoothSpriteMovement`. Known gap: the tick clock is global, so +if some particles keep updating while others are frozen (time stop during +rain), the frozen ones jitter by one step. diff --git a/src/iee/core/config.cpp b/src/iee/core/config.cpp index 71d7202..165fb2d 100644 --- a/src/iee/core/config.cpp +++ b/src/iee/core/config.cpp @@ -62,7 +62,7 @@ static void normalize(EngineConfig& cfg) noexcept { if (!std::isfinite(cfg.softFogDrift)) cfg.softFogDrift = 10.0f; if (!std::isfinite(cfg.softFogSmoothing)) cfg.softFogSmoothing = 0.25f; if (!std::isfinite(cfg.lightSpill)) cfg.lightSpill = 1.5f; - if (!std::isfinite(cfg.heatShimmer)) cfg.heatShimmer = 2.5f; + if (!std::isfinite(cfg.heatShimmer)) cfg.heatShimmer = 1.5f; if (!std::isfinite(cfg.bloomStrength)) cfg.bloomStrength = 0.35f; cfg.softFogRadius = std::clamp(cfg.softFogRadius, 0.0f, 256.0f); cfg.softFogDrift = std::clamp(cfg.softFogDrift, 0.0f, 64.0f); diff --git a/src/iee/core/config.h b/src/iee/core/config.h index 856b7fe..539de2b 100644 --- a/src/iee/core/config.h +++ b/src/iee/core/config.h @@ -18,7 +18,7 @@ struct EngineConfig { // How strongly light-emitting effects brighten what is around them; 0 = off. float lightSpill = 1.5f; // World pixels the air above warm light sources ripples by; 0 = off. - float heatShimmer = 2.5f; + float heatShimmer = 1.5f; bool dumpEngineShaders = false; bool enableDebugHotkeys = false; diff --git a/src/iee/game/build_manifest.cpp b/src/iee/game/build_manifest.cpp index 8b91c65..e3f4286 100644 --- a/src/iee/game/build_manifest.cpp +++ b/src/iee/game/build_manifest.cpp @@ -202,9 +202,15 @@ constexpr BuildManifest kKnownBuilds[] = { "48 8B D9 E8 ? ? ? ? B9 FF FF FF FF E8 ? ? ? ? B9 E1 0D 00 00", "4C 8B DC 55 41 56 41 57 49 8D 6B D8 48 81 EC 10 01 00 00 48 8B 05 ? ? ? ? " "48 33 C4 48 89 45 90 45 33 F6 44 39 35 ? ? ? ? 45 8B FE 0F 84", + // CParticle::AsynchronousUpdate and CParticle::Render (PDB-named, + // each offline-verified unique). + "48 89 5C 24 18 57 48 83 EC 20 8B 51 10 45 33 C0 48 8B D9 41 8B F8 8D 42 FF 89 41 10 " + "85 D2", + "48 89 5C 24 18 48 89 54 24 10 55 56 57 41 54 41 55 41 56 41 57 48 83 EC 40 " + "44 0F B7 29 4C 8B F9 66 44 3B 69 02", }, {0x27EBD0, 0x4257C0, 0x276700, 0x1F27D0, 0x36BA50, 0x36F170, 0x36E820, 0x4118A0, 0x411660, - 0x411780, 0x2A1B60, 0x42B350}, + 0x411780, 0x2A1B60, 0x42B350, 0x423C30, 0x425BE0}, {0x100, 0x1DC, 0x14, 0x6590, 0x6598, 0x65F8}, {{ {"CRes_Demand", 0x36, BranchInstructionKind::CallRel32, 0xE8, 1, 5, true}, @@ -236,6 +242,12 @@ constexpr BuildManifest kKnownBuilds[] = { {"CGameTemporal::Render", "4C 8B DC 55 56 57 41 57 49 8D 6B A1 48 81 EC", 0x36CD80}, {"CGameChunk::Render", "4C 8B DC 55 56 57 41 55 41 57 49 8D 6B A1", 0x36B7A0}, {"CVEFVidCell::Render", "4C 8B DC 55 56 41 57 49 8D 6B A1 48 81 EC D0", 0x254B00}, + // Floating text over a creature: each tick it copies its target's + // m_pos into its own and draws from there. + {"CGameText::Render", + "40 57 48 81 EC B0 00 00 00 48 8B 05 ? ? ? ? 48 33 C4 48 89 44 24 78 48 8B C2 " + "48 8B F9 4D 85 C0 74 15 48 63 51 48", + 0x1F39A0}, }}, // Disassembly-verified on the Steam 2.7.3.0 binary: DrawFlush_GL+0x24 // compares gl.n (0x2F73FD0); DrawEnd_GL writes gl.cmds at 0x2F5BF28 @@ -274,6 +286,9 @@ static_assert([] { } return true; }(), "2.7.3 smoothed object render pattern format is invalid"); +static_assert(validate_pattern_format(kKnownBuilds[1].patterns.particleUpdate) && + validate_pattern_format(kKnownBuilds[1].patterns.particleRender), + "2.7.3 particle pattern format is invalid"); static_assert(validate_pattern_format(kKnownBuilds[1].patterns.renderFog) && validate_pattern_format(kKnownBuilds[1].patterns.drawFlush), "2.7.3 world post pattern format is invalid"); diff --git a/src/iee/game/build_manifest.h b/src/iee/game/build_manifest.h index 0f98244..fd75c97 100644 --- a/src/iee/game/build_manifest.h +++ b/src/iee/game/build_manifest.h @@ -58,6 +58,11 @@ struct PatternSet { // fog of war, bloom); either empty or non-unique leaves both off. std::string_view renderFog{}; std::string_view drawFlush{}; + // Optional pair: CParticle::AsynchronousUpdate and CParticle::Render (rain, + // snow, sparkles). The update marks the logic tick; the render is wrapped + // to draw each particle part of the way through its step. + std::string_view particleUpdate{}; + std::string_view particleRender{}; }; struct ReferenceRvas { @@ -74,6 +79,8 @@ struct ReferenceRvas { std::uintptr_t vidCellGetCurrentFrameSize{}; std::uintptr_t renderFog{}; std::uintptr_t drawFlush{}; + std::uintptr_t particleUpdate{}; + std::uintptr_t particleRender{}; }; struct RuntimeOffsets { @@ -109,7 +116,7 @@ struct ObjectRenderTarget { std::string_view pattern{}; std::uintptr_t referenceRva{}; }; -inline constexpr std::size_t kMaxSmoothedObjectRenders = 8; +inline constexpr std::size_t kMaxSmoothedObjectRenders = 12; // The GL backend's draw queue (gl.cmds / gl.n), consumed by DrawFlush_GL. // Used to pick out the engine's additive (light-emitting) draws. diff --git a/src/iee/game/game_addrs.cpp b/src/iee/game/game_addrs.cpp index b9298db..88652b9 100644 --- a/src/iee/game/game_addrs.cpp +++ b/src/iee/game/game_addrs.cpp @@ -134,6 +134,15 @@ namespace iee::game { out.SmoothedObjectRenders[index] = resolveOptional(target.name, target.pattern, target.referenceRva); } + if (cfg.smoothSpriteMovement) { + out.ParticleUpdate = resolveOptional("CParticle::AsynchronousUpdate", + patterns.particleUpdate, rvas.particleUpdate); + out.ParticleRender = resolveOptional("CParticle::Render", patterns.particleRender, + rvas.particleRender); + if (!(out.ParticleUpdate && out.ParticleRender)) { + out.ParticleUpdate = out.ParticleRender = 0; + } + } if (out.SpriteRender && out.SpriteRenderMarkers && out.SpriteRenderHealthBar) { LOG_INFO("Sprite render targets resolved at RVA 0x{:X} / 0x{:X} / 0x{:X}", out.SpriteRender - moduleBase, out.SpriteRenderMarkers - moduleBase, diff --git a/src/iee/game/game_addrs.h b/src/iee/game/game_addrs.h index 2e9ce92..52e33c0 100644 --- a/src/iee/game/game_addrs.h +++ b/src/iee/game/game_addrs.h @@ -2,12 +2,13 @@ #include #include +#include "iee/game/build_manifest.h" + namespace iee { namespace core { struct EngineConfig; } } namespace iee::game { - struct BuildManifest; struct GameAddresses { std::uintptr_t LoadArea = 0; @@ -26,7 +27,10 @@ namespace iee::game { std::uintptr_t VidCellGetCurrentFrameSize = 0; // Optional, each independent; 0 where unresolved. Indexed like // BuildManifest::smoothedObjectRenders. - std::array SmoothedObjectRenders{}; + std::array SmoothedObjectRenders{}; + // Optional pair: particle (rain, snow, sparkle) movement smoothing. + std::uintptr_t ParticleUpdate = 0; + std::uintptr_t ParticleRender = 0; // Optional, all-or-nothing: the world post passes (soft fog of war, // bloom) need the fog render to hook and the queue flush to call. std::uintptr_t RenderFog = 0; diff --git a/src/iee/game/runtime_types_x64.h b/src/iee/game/runtime_types_x64.h index 0839c4b..aca652f 100644 --- a/src/iee/game/runtime_types_x64.h +++ b/src/iee/game/runtime_types_x64.h @@ -431,6 +431,27 @@ namespace iee::game { std::array _tail{}; }; + // CParticle: a rain drop, snow flake or sparkle. Positions and velocities + // are fixed point; CParticle::AsynchronousUpdate adds the velocity to the + // position once per logic tick. Offsets from that function's 2.7.3 + // disassembly; only the leading fields are described. + struct CParticle { + std::int16_t m_nTimeStamp{}; + std::int16_t m_nTailLength{}; + std::byte _pad0[8]{}; + std::uint8_t m_wType{}; // bit 0: gravity only, no vertical velocity + std::byte _pad1[3]{}; + std::int32_t m_nLifeSpan{}; + std::byte _pad2[4]{}; + std::int32_t m_posX{}; + std::int32_t m_posY{}; + std::int32_t m_posZ{}; + std::int32_t m_velX{}; + std::int32_t m_velY{}; + std::int32_t m_velZ{}; + std::int32_t m_nGravity{}; + }; + // CGameObject::m_objectType for ARE "static" ambient animations // (script share type '0'; CGameStatic). inline constexpr std::uint8_t kGameObjectTypeStatic = 0x30; @@ -565,6 +586,10 @@ namespace iee::game { static_assert(offsetof(CGameArea, m_visibility) == 0xBB0); static_assert(offsetof(CGameArea, m_lTiledObjects) == 0xED0); static_assert(offsetof(CGameArea, m_ptOldViewPos) == 0xF78); + static_assert(offsetof(CParticle, m_wType) == 0xC); + static_assert(offsetof(CParticle, m_posX) == 0x18); + static_assert(offsetof(CParticle, m_velX) == 0x24); + static_assert(offsetof(CParticle, m_nGravity) == 0x30); static_assert(offsetof(CGameObject, m_objectType) == 0x8); static_assert(offsetof(CGameObject, m_pos) == 0xC); static_assert(offsetof(CGameObject, m_pArea) == 0x18); diff --git a/src/iee/game/sprite_motion.cpp b/src/iee/game/sprite_motion.cpp index b1a19c9..861b4f1 100644 --- a/src/iee/game/sprite_motion.cpp +++ b/src/iee/game/sprite_motion.cpp @@ -51,4 +51,35 @@ MotionPoint SpriteMotionTracker::sample(const void* key, MotionPoint current, do } return shown(state, now); } + +void TickClock::on_update(double now) noexcept { + const double last = lastUpdate_.exchange(now, std::memory_order_relaxed); + if (last >= 0.0 && now - last < kBurstSeconds && now >= last) return; + const double previousTick = tickStart_.exchange(now, std::memory_order_relaxed); + const double observed = now - previousTick; + interval_.store(previousTick >= 0.0 && observed >= kMinInterval && observed <= kMaxInterval + ? observed + : kDefaultInterval, + std::memory_order_relaxed); +} + +double TickClock::phase(double now) const noexcept { + const double start = tickStart_.load(std::memory_order_relaxed); + if (start < 0.0) return 1.0; + const double elapsed = (now - start) / interval_.load(std::memory_order_relaxed); + // Written so NaN lands on the logic position. + if (!(elapsed < 1.0)) return 1.0; + return elapsed > 0.0 ? elapsed : 0.0; +} + +ParticlePoint particle_draw_position(const ParticleState& particle, double phase) noexcept { + if (!(phase >= 0.0) || !(phase < 1.0)) return particle.position; + const double back = 1.0 - phase; + const auto step = [back](std::int32_t delta) { + return static_cast(std::lround(static_cast(delta) * back)); + }; + const auto heightStep = particle.gravityOnly ? -particle.gravity : particle.velocity.z; + return {particle.position.x - step(particle.velocity.x), + particle.position.y - step(particle.velocity.y), particle.position.z - step(heightStep)}; +} } // namespace iee::game diff --git a/src/iee/game/sprite_motion.h b/src/iee/game/sprite_motion.h index 9eda569..6a7e630 100644 --- a/src/iee/game/sprite_motion.h +++ b/src/iee/game/sprite_motion.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -62,4 +63,48 @@ class SpriteMotionTracker { std::int32_t snapDistance_; std::unordered_map states_; }; + +// The engine's logic tick, as seen from any function it calls once (or many +// times in a burst) per tick. Things the engine advances by a known amount per +// tick can then be drawn part of the way through the step. on_update may run +// on a different thread from phase. +class TickClock { + public: + static constexpr double kDefaultInterval = 1.0 / 30.0; + static constexpr double kMinInterval = 1.0 / 90.0; + static constexpr double kMaxInterval = 1.0 / 14.0; + // Calls closer together than this belong to the same tick. + static constexpr double kBurstSeconds = 0.004; + + void on_update(double now) noexcept; + // 0 right after a tick, rising to 1 one tick interval later and staying + // there (so nothing drifts while the game is paused). 1 before any tick. + [[nodiscard]] double phase(double now) const noexcept; + + private: + std::atomic tickStart_{-1.0}; + std::atomic lastUpdate_{-1.0}; + std::atomic interval_{kDefaultInterval}; +}; + +struct ParticlePoint { + std::int32_t x{}; + std::int32_t y{}; + std::int32_t z{}; + friend bool operator==(const ParticlePoint&, const ParticlePoint&) = default; +}; + +// A CParticle after its per-tick update: the engine added `velocity` to the +// position (CParticle::AsynchronousUpdate). A gravity-only particle instead +// lost `gravity` in height. +struct ParticleState { + ParticlePoint position{}; + ParticlePoint velocity{}; + std::int32_t gravity{}; + bool gravityOnly{}; +}; + +// Where to draw a particle `phase` of the way through the current tick: its +// previous position at 0, its logic position at 1. +ParticlePoint particle_draw_position(const ParticleState& particle, double phase) noexcept; } // namespace iee::game diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index 88a56c8..db58b5d 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -70,6 +70,12 @@ static core::Hook g_drawFlushHook; static std::int32_t* g_drawQueueCount = nullptr; static game::DrawCommand* g_drawQueueCommands = nullptr; +// CParticle::AsynchronousUpdate() and CParticle::Render(CPoint&, CRect&, type, n). +using ParticleUpdateFn = unsigned char (*)(void*); +using ParticleRenderFn = void (*)(void*, void*, void*, unsigned short, unsigned short); +static core::Hook g_particleUpdateHook; +static core::Hook g_particleRenderHook; + static AppContext* g_ctx = nullptr; // Raised by LoadArea; the sprite-smoothing tracker clears itself on its own thread. static std::atomic g_spriteMotionReset{false}; @@ -427,6 +433,57 @@ static int detour_vid_cell_frame_size(void* cell, void* out) { g_animationInterp.touch(static_cast(cell), sprite_motion_frame_seconds()); return g_vidCellFrameSizeHook.original()(cell, out); } +// Particle smoothing (rain, snow, sparkles). The engine adds each particle's +// velocity to its position once per logic tick, so between ticks its previous +// position is known exactly. Drawing it part of the way from there removes +// the stepping. The update may run on another thread: the tick clock is +// atomic, and the position is only put back if the engine has not moved the +// particle in the meantime. +static game::TickClock g_particleTick; + +static double monotonic_seconds() noexcept { + static const double frequency = [] { + LARGE_INTEGER value{}; + QueryPerformanceFrequency(&value); + return static_cast(value.QuadPart); + }(); + LARGE_INTEGER counter{}; + QueryPerformanceCounter(&counter); + return static_cast(counter.QuadPart) / frequency; +} + +static unsigned char detour_particle_update(void* particle) { + g_particleTick.on_update(monotonic_seconds()); + return g_particleUpdateHook.original()(particle); +} + +static void detour_particle_render(void* self, void* origin, void* clip, unsigned short type, + unsigned short count) { + const auto original = g_particleRenderHook.original(); + auto* particle = static_cast(self); + const double phase = g_particleTick.phase(sprite_motion_frame_seconds()); + if (!particle || phase >= 1.0) { + original(self, origin, clip, type, count); + return; + } + const game::ParticlePoint logic{particle->m_posX, particle->m_posY, particle->m_posZ}; + const auto shown = game::particle_draw_position( + {logic, + {particle->m_velX, particle->m_velY, particle->m_velZ}, + particle->m_nGravity, + (particle->m_wType & 1) != 0}, + phase); + particle->m_posX = shown.x; + particle->m_posY = shown.y; + particle->m_posZ = shown.z; + original(self, origin, clip, type, count); + if (particle->m_posX == shown.x && particle->m_posY == shown.y && particle->m_posZ == shown.z) { + particle->m_posX = logic.x; + particle->m_posY = logic.y; + particle->m_posZ = logic.z; + } +} + static void detour_sprite_markers(void* sprite, void* a, void* b) { call_with_smoothed_position(g_spriteMarkersHook, g_spriteMotion, sprite, a, b); } @@ -690,6 +747,22 @@ bool install_all(AppContext& ctx) { } } LOG_INFO("Object movement smoothing hooks installed: {}", installed); + + if (ctx.addrs.ParticleUpdate && ctx.addrs.ParticleRender) { + try { + g_particleUpdateHook.create(reinterpret_cast(ctx.addrs.ParticleUpdate), + reinterpret_cast(&detour_particle_update)); + g_particleRenderHook.create(reinterpret_cast(ctx.addrs.ParticleRender), + reinterpret_cast(&detour_particle_render)); + g_particleUpdateHook.enable(); + g_particleRenderHook.enable(); + LOG_INFO("Particle movement smoothing hooks installed (rain, snow, sparkles)"); + } catch (...) { + (void)g_particleRenderHook.remove(); + (void)g_particleUpdateHook.remove(); + LOG_WARN("Particle movement smoothing hooks failed; particles move at the logic rate"); + } + } } if (ctx.addrs.RenderFog && ctx.addrs.DrawFlush) { @@ -770,6 +843,8 @@ void uninstall_all() noexcept { (void)g_vidCellFrameSizeHook.remove(); (void)g_vidCellCenterPointHook.remove(); (void)g_vidCellGetFrameHook.remove(); + (void)g_particleRenderHook.remove(); + (void)g_particleUpdateHook.remove(); for (auto& hook : g_objectRenderHooks) (void)hook.remove(); (void)g_spriteHealthBarHook.remove(); (void)g_spriteMarkersHook.remove(); @@ -799,6 +874,8 @@ void prepare_for_shutdown() noexcept { (void)g_vidCellFrameSizeHook.disable(); (void)g_vidCellCenterPointHook.disable(); (void)g_vidCellGetFrameHook.disable(); + (void)g_particleRenderHook.disable(); + (void)g_particleUpdateHook.disable(); for (auto& hook : g_objectRenderHooks) (void)hook.disable(); (void)g_spriteHealthBarHook.disable(); (void)g_spriteMarkersHook.disable(); diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 9e72756..a7aab18 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1275,7 +1275,7 @@ void test_config_world_post() { expect_eq(defaults.lightSpill, 1.5f, "Light spill default"); expect_eq(defaults.softFogDrift, 10.0f, "Soft fog drift default"); expect_eq(defaults.softFogSmoothing, 0.25f, "Soft fog smoothing default"); - expect_eq(defaults.heatShimmer, 2.5f, "Heat shimmer default"); + expect_eq(defaults.heatShimmer, 1.5f, "Heat shimmer default"); expect_eq(defaults.bloomStrength, 0.35f, "Bloom strength default"); const auto tempPath = @@ -1349,6 +1349,19 @@ void test_manifest_world_post_targets() { expect_eq(manifest.referenceRvas.drawFlush, std::uintptr_t{0x42B350}, "DrawFlush reference RVA"); expect_true(iee::game::current_manifest().patterns.renderFog.empty(), "The 2.6.6 manifest has no world post targets"); + expect_true(!manifest.patterns.particleUpdate.empty() && !manifest.patterns.particleRender.empty(), + "2.7.3 should carry the particle update and render patterns"); + expect_eq(manifest.referenceRvas.particleUpdate, std::uintptr_t{0x423C30}, + "CParticle::AsynchronousUpdate reference RVA"); + expect_eq(manifest.referenceRvas.particleRender, std::uintptr_t{0x425BE0}, + "CParticle::Render reference RVA"); + bool smoothsFloatingText = false; + for (const auto& target : manifest.smoothedObjectRenders) { + if (target.name && std::string_view(target.name) == "CGameText::Render") { + smoothsFloatingText = target.referenceRva == 0x1F39A0 && !target.pattern.empty(); + } + } + expect_true(smoothsFloatingText, "2.7.3 should smooth floating text with the other objects"); } void test_draw_queue() { @@ -1411,6 +1424,47 @@ void test_draw_queue() { "The command array sits a fixed distance before the count"); } +void test_tick_clock_and_particle_backstep() { + using namespace iee::game; + TickClock clock; + expect_eq(clock.phase(5.0), 1.0, "With no tick seen, things are drawn where the engine has them"); + + clock.on_update(10.0); + expect_eq(clock.phase(10.0), 0.0, "A tick starts a new step"); + expect_true(std::abs(clock.phase(10.0 + TickClock::kDefaultInterval / 2.0) - 0.5) < 1e-9, + "Half a default interval later the step is half done"); + expect_eq(clock.phase(10.5), 1.0, "The step ends after one interval and stays there"); + expect_eq(clock.phase(9.0), 0.0, "A time before the tick is clamped to the start"); + + // Hundreds of particles update within one tick: that burst is one tick. + clock.on_update(10.001); + clock.on_update(10.002); + expect_eq(clock.phase(10.0), 0.0, "Updates within a burst do not restart the step"); + clock.on_update(10.05); + expect_true(std::abs(clock.phase(10.075) - 0.5) < 1e-9, + "The observed interval between ticks sets the step length"); + + clock.on_update(20.0); // a 10 s gap is a pause, not a tick length + expect_true(std::abs(clock.phase(20.0 + TickClock::kDefaultInterval / 2.0) - 0.5) < 1e-9, + "An implausible interval falls back to the default"); + + const ParticleState falling{{1000, 2000, 300}, {40, -20, -8}, 0, false}; + expect_true(particle_draw_position(falling, 1.0) == ParticlePoint{1000, 2000, 300}, + "At the end of a step a particle is drawn at its logic position"); + expect_true(particle_draw_position(falling, 0.0) == ParticlePoint{960, 2020, 308}, + "At the start of a step it is drawn one tick back along its velocity"); + expect_true(particle_draw_position(falling, 0.5) == ParticlePoint{980, 2010, 304}, + "In between it is drawn part of the way"); + const ParticleState gravityOnly{{1000, 2000, 300}, {40, -20, 0}, 16, true}; + expect_true(particle_draw_position(gravityOnly, 0.0) == ParticlePoint{960, 2020, 316}, + "A gravity-only particle steps back by its gravity in height"); + expect_true(particle_draw_position(falling, std::numeric_limits::quiet_NaN()) == + ParticlePoint{1000, 2000, 300}, + "A NaN phase draws the logic position"); + expect_true(particle_draw_position(falling, 7.0) == ParticlePoint{1000, 2000, 300}, + "A phase past the end draws the logic position"); +} + void test_world_post_plan() { using namespace iee::game; @@ -2281,6 +2335,7 @@ int main() { test_sprite_motion_tracker(); test_animation_interpolation(); test_world_post_plan(); + test_tick_clock_and_particle_backstep(); test_draw_queue(); test_build_area_effect_points(); test_config_detection_section(); diff --git a/tools/InfinityEngine-Enhancer.sample.ini b/tools/InfinityEngine-Enhancer.sample.ini index 1195c8a..f1c84f0 100644 --- a/tools/InfinityEngine-Enhancer.sample.ini +++ b/tools/InfinityEngine-Enhancer.sample.ini @@ -11,7 +11,8 @@ EnableAnisotropicFiltering = false MaxAnisotropy = 8.0 LODBias = -0.25 ; Draw creatures gliding between the engine's logic ticks instead of stepping -; at the logic rate. Only useful with EEex "Uncap FPS"; BGEE 2.7.3 only. +; at the logic rate. Also smooths projectiles, floating text, rain, snow and +; sparkles. Only useful with EEex "Uncap FPS"; BGEE 2.7.3 only. SmoothSpriteMovement = false ; Draw creature animations with the in-between frames found in iee-interp/ ; (expanded BAMs, one per original resref). Animations without an expanded @@ -36,7 +37,7 @@ BloomStrength = 0.35 LightSpill = 1.5 ; Heat haze above fires and other warm light sources: how far (world pixels) ; the air ripples; 0 = off. Needs Bloom. -HeatShimmer = 2.5 +HeatShimmer = 1.5 [Detection] ; Collect the current area's authored ambient animations from engine memory From a9716ae44b5b7dc5b844de60a9c081980f837bab Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 8 Oct 2026 00:06:25 +0200 Subject: [PATCH 47/57] Sprite smoothing: FSR1 upscale of the sprite atlas before the engine draws from it --- .../2026-10-07-soft-fog-and-bloom-design.md | 42 +++ src/iee/core/config.cpp | 8 + src/iee/core/config.h | 3 + src/iee/features/world_post.cpp | 254 +++++++++++++++++- src/iee/features/world_post.h | 13 +- src/iee/game/build_manifest.cpp | 12 +- src/iee/game/build_manifest.h | 30 +++ src/iee/game/draw_queue.cpp | 42 +++ src/iee/game/draw_queue.h | 16 ++ src/iee/game/game_addrs.cpp | 15 +- src/iee/game/game_addrs.h | 3 + src/iee/hooks.cpp | 57 +++- tests/iee_tests.cpp | 77 ++++++ tools/InfinityEngine-Enhancer.sample.ini | 7 + 14 files changed, 565 insertions(+), 14 deletions(-) diff --git a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md index 39d0281..719ff9b 100644 --- a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md +++ b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md @@ -367,3 +367,45 @@ table, so it slides with its creature. Both ride on `SmoothSpriteMovement`. Known gap: the tick clock is global, so if some particles keep updating while others are frozen (time stop during rain), the frozen ones jitter by one step. + +## 16. Revision 2026-10-08: sprite smoothing with FSR1 + +Owner's goal after rejecting every detail-adding upscaler (offline comparison +sheets in the workspace's `interp-preview/`): sprites "don't need to be that +much better, just smoother". Chosen filter: FSR1. + +Evidence (2.7.3): + +- Sprites are composited on the CPU into a streaming atlas (`fx[0]` nearest, + `fx[1]` linear; 1024x1024). `DrawFlush_GL` begins by uploading each atlas's + used rows through `TexSubImage` -> `TexSubImage_GL` (`0x42D190`), then draws + the queue. `TexSubImage_GL` binds `gl.textures[selected].name` and marks the + texture for rebinding. +- `gl.textures` (`0x757040`, 0x28-byte entries, GL name first), `gl.user.state` + (`0x2F73F6C`, selected texture in bits 21-29) and `fx[]` (`0x2F74050`, 0x30 + apart) sit at fixed distances from `gl.n` (`BuildManifest::spriteAtlas`). +- `CVidCell::Blt8To32` writes only non-transparent pixels; the atlas buffer is + cleared to zero, so transparent texels are `(0,0,0,0)`. + +Design: + +- `TexSubImage_GL` is detoured. After the engine's upload of an atlas, during + the world pass, the atlas is upscaled 2x into our own texture with EASU + (12-tap edge-adaptive kernel; alpha is filtered with colour and counts + towards edge detection) and sharpened with RCAS. The texture's name in the + engine's table is replaced by ours until the flush ends; the flush detour + restores it. Texture coordinates are normalised by the table's stored size, + so they address the larger texture unchanged. +- The world pass is now "open" only on frames following a frame that drew an + area, so menus and full-screen panels are never touched. +- INI: `SpriteUpscale` (default false), `SpriteSharpness` (0..1, default 0.5); + F7 toggles with debug hotkeys on. +- The shaders' maths was checked offline with a NumPy transcription + (`interp-prototype/sr/easu_check.py`) against the reference FSR1 port. + +Known limits: 2x only (the remaining magnification is the engine's bilinear); +neighbouring sprites on the atlas can bleed up to two texels into each other's +edges; the dark fringe from blending non-premultiplied alpha is unchanged +(fixing it needs the blend mode swapped for alpha-blended sprites only, since +additive sprites share the atlas); BAM v2 (PVRZ) sprites bypass the atlas and +are not filtered. diff --git a/src/iee/core/config.cpp b/src/iee/core/config.cpp index 165fb2d..b04496d 100644 --- a/src/iee/core/config.cpp +++ b/src/iee/core/config.cpp @@ -63,12 +63,14 @@ static void normalize(EngineConfig& cfg) noexcept { if (!std::isfinite(cfg.softFogSmoothing)) cfg.softFogSmoothing = 0.25f; if (!std::isfinite(cfg.lightSpill)) cfg.lightSpill = 1.5f; if (!std::isfinite(cfg.heatShimmer)) cfg.heatShimmer = 1.5f; + if (!std::isfinite(cfg.spriteSharpness)) cfg.spriteSharpness = 0.5f; if (!std::isfinite(cfg.bloomStrength)) cfg.bloomStrength = 0.35f; cfg.softFogRadius = std::clamp(cfg.softFogRadius, 0.0f, 256.0f); cfg.softFogDrift = std::clamp(cfg.softFogDrift, 0.0f, 64.0f); cfg.softFogSmoothing = std::clamp(cfg.softFogSmoothing, 0.0f, 2.0f); cfg.lightSpill = std::clamp(cfg.lightSpill, 0.0f, 8.0f); cfg.heatShimmer = std::clamp(cfg.heatShimmer, 0.0f, 12.0f); + cfg.spriteSharpness = std::clamp(cfg.spriteSharpness, 0.0f, 1.0f); cfg.bloomStrength = std::clamp(cfg.bloomStrength, 0.0f, 2.0f); } @@ -124,6 +126,10 @@ static void apply_kv(EngineConfig& cfg, const std::string& section, const std::s assign_float(cfg.lightSpill); else if (iequals(key, "HeatShimmer")) assign_float(cfg.heatShimmer); + else if (iequals(key, "SpriteUpscale")) + assign_bool(cfg.spriteUpscale); + else if (iequals(key, "SpriteSharpness")) + assign_float(cfg.spriteSharpness); else if (iequals(key, "BloomStrength")) assign_float(cfg.bloomStrength); return; @@ -230,6 +236,8 @@ bool ConfigManager::save(const std::filesystem::path& path, const EngineConfig& write_bool(f, "Bloom", cfg.bloom); f << "LightSpill = " << cfg.lightSpill << "\n"; f << "HeatShimmer = " << cfg.heatShimmer << "\n"; + write_bool(f, "SpriteUpscale", cfg.spriteUpscale); + f << "SpriteSharpness = " << cfg.spriteSharpness << "\n"; f << "BloomStrength = " << cfg.bloomStrength << "\n"; write_section(f, "Detection"); diff --git a/src/iee/core/config.h b/src/iee/core/config.h index 539de2b..6171358 100644 --- a/src/iee/core/config.h +++ b/src/iee/core/config.h @@ -19,6 +19,9 @@ struct EngineConfig { float lightSpill = 1.5f; // World pixels the air above warm light sources ripples by; 0 = off. float heatShimmer = 1.5f; + // Edge-adaptive 2x upscale (FSR1) of the sprite atlas before it is drawn. + bool spriteUpscale = false; + float spriteSharpness = 0.5f; // 0 = no sharpening pass effect, 1 = strongest bool dumpEngineShaders = false; bool enableDebugHotkeys = false; diff --git a/src/iee/features/world_post.cpp b/src/iee/features/world_post.cpp index b2817df..601ec0f 100644 --- a/src/iee/features/world_post.cpp +++ b/src/iee/features/world_post.cpp @@ -42,6 +42,18 @@ struct FogTemporalProgram { int blend{-1}; }; +struct SharpenProgram { + unsigned id{}; + int sharpness{-1}; +}; + +// One sprite atlas's upscaled copies: the edge-adaptive 2x image and the +// sharpened result the engine then draws from. +struct AtlasTargets { + Target upscaled{}; + Target sharpened{}; +}; + struct ShimmerProgram { unsigned id{}; int worldOrigin{-1}; @@ -78,6 +90,8 @@ struct Settings { float fogDrift{}; float fogSmoothing{}; float heatShimmer{}; + bool spriteUpscale{}; + float spriteSharpness{}; }; // Everything that lives and dies with one GL context. @@ -90,6 +104,10 @@ struct Resources { FogProgram fog{}; FogTemporalProgram fogTemporal{}; ShimmerProgram shimmer{}; + unsigned spriteUpscale{}; + SharpenProgram spriteSharpen{}; + std::array atlases{}; + bool atlasDrawnOnce{}; BlurProgram blurDown{}; BlurProgram blurUp{}; Target fogCapture{}; @@ -123,6 +141,7 @@ std::atomic g_dropFogHistory{false}; unsigned g_frame = 1; unsigned g_emissiveFrame = 0; bool g_worldPassOpen = false; +bool g_fogSeenThisFrame = false; int g_emissiveCommands = 0; int g_savedDrawFramebuffer = 0; int g_savedReadFramebuffer = 0; @@ -229,6 +248,133 @@ void main() { } )glsl"; +// FSR1 EASU (edge-adaptive spatial upsampling), after AMD's FidelityFX +// reference (MIT): a 12-tap kernel whose shape follows the local edge +// direction, so diagonal outlines come out smooth instead of stair-stepped. +// Runs on the sprite atlas at exactly 2x, texel to texel: output pixel +// gl_FragCoord maps to input position gl_FragCoord / 2. Alpha is filtered +// with the colour and counts towards the edge detection, because a sprite's +// outline is mostly an alpha edge. +constexpr const char* kSpriteUpscaleSource = R"glsl(#version 330 +uniform sampler2D uTexture; +out vec4 fragColor; +ivec2 size; +vec4 tap(ivec2 p) { + return texelFetch(uTexture, clamp(p, ivec2(0), size - 1), 0); +} +float luma(vec4 c) { + return c.r * 0.5 + c.g + c.b * 0.5 + c.a; +} +void accumulate(inout vec4 colour, inout float weight, vec2 offset, vec2 dir, vec2 len, float lobe, + float clip, vec4 c) { + vec2 v = vec2(offset.x * dir.x + offset.y * dir.y, offset.x * -dir.y + offset.y * dir.x) * len; + float d2 = min(dot(v, v), clip); + float wB = 0.4 * d2 - 1.0; + float wA = lobe * d2 - 1.0; + wB *= wB; + wA *= wA; + wB = 1.5625 * wB - 0.5625; + float w = wB * wA; + colour += c * w; + weight += w; +} +void edge(inout vec2 dir, inout float len, float w, float lA, float lB, float lC, float lD, + float lE) { + float lenX = max(abs(lD - lC), abs(lC - lB)); + float dirX = lD - lB; + dir.x += dirX * w; + lenX = clamp(abs(dirX) / max(lenX, 1e-5), 0.0, 1.0); + len += lenX * lenX * w; + float lenY = max(abs(lE - lC), abs(lC - lA)); + float dirY = lE - lA; + dir.y += dirY * w; + lenY = clamp(abs(dirY) / max(lenY, 1e-5), 0.0, 1.0); + len += lenY * lenY * w; +} +void main() { + size = textureSize(uTexture, 0); + vec2 pp = gl_FragCoord.xy * 0.5 - 0.5; + vec2 fp = floor(pp); + pp -= fp; + ivec2 ip = ivec2(fp); + // b c + // e f g h + // i j k l + // n o + vec4 b = tap(ip + ivec2(0, -1)); + vec4 c = tap(ip + ivec2(1, -1)); + vec4 e = tap(ip + ivec2(-1, 0)); + vec4 f = tap(ip); + vec4 g = tap(ip + ivec2(1, 0)); + vec4 h = tap(ip + ivec2(2, 0)); + vec4 i = tap(ip + ivec2(-1, 1)); + vec4 j = tap(ip + ivec2(0, 1)); + vec4 k = tap(ip + ivec2(1, 1)); + vec4 l = tap(ip + ivec2(2, 1)); + vec4 n = tap(ip + ivec2(0, 2)); + vec4 o = tap(ip + ivec2(1, 2)); + float bL = luma(b), cL = luma(c), eL = luma(e), fL = luma(f), gL = luma(g), hL = luma(h); + float iL = luma(i), jL = luma(j), kL = luma(k), lL = luma(l), nL = luma(n), oL = luma(o); + vec2 dir = vec2(0.0); + float len = 0.0; + edge(dir, len, (1.0 - pp.x) * (1.0 - pp.y), bL, eL, fL, gL, jL); + edge(dir, len, pp.x * (1.0 - pp.y), cL, fL, gL, hL, kL); + edge(dir, len, (1.0 - pp.x) * pp.y, fL, iL, jL, kL, nL); + edge(dir, len, pp.x * pp.y, gL, jL, kL, lL, oL); + float dirR = dot(dir, dir); + bool flat_ = dirR < 1.0 / 32768.0; + dir = flat_ ? vec2(1.0, 0.0) : dir * inversesqrt(dirR); + len = len * 0.5; + len *= len; + float stretch = dot(dir, dir) / max(abs(dir.x), abs(dir.y)); + vec2 len2 = vec2(1.0 + (stretch - 1.0) * len, 1.0 - 0.5 * len); + float lobe = 0.5 + (0.21 - 0.5) * len; + float clip = 1.0 / lobe; + vec4 lowest = min(min(f, g), min(j, k)); + vec4 highest = max(max(f, g), max(j, k)); + vec4 colour = vec4(0.0); + float weight = 0.0; + accumulate(colour, weight, vec2(0.0, -1.0) - pp, dir, len2, lobe, clip, b); + accumulate(colour, weight, vec2(1.0, -1.0) - pp, dir, len2, lobe, clip, c); + accumulate(colour, weight, vec2(-1.0, 1.0) - pp, dir, len2, lobe, clip, i); + accumulate(colour, weight, vec2(0.0, 1.0) - pp, dir, len2, lobe, clip, j); + accumulate(colour, weight, vec2(0.0, 0.0) - pp, dir, len2, lobe, clip, f); + accumulate(colour, weight, vec2(-1.0, 0.0) - pp, dir, len2, lobe, clip, e); + accumulate(colour, weight, vec2(1.0, 1.0) - pp, dir, len2, lobe, clip, k); + accumulate(colour, weight, vec2(2.0, 1.0) - pp, dir, len2, lobe, clip, l); + accumulate(colour, weight, vec2(2.0, 0.0) - pp, dir, len2, lobe, clip, h); + accumulate(colour, weight, vec2(1.0, 0.0) - pp, dir, len2, lobe, clip, g); + accumulate(colour, weight, vec2(1.0, 2.0) - pp, dir, len2, lobe, clip, o); + accumulate(colour, weight, vec2(0.0, 2.0) - pp, dir, len2, lobe, clip, n); + fragColor = clamp(colour / weight, lowest, highest); +} +)glsl"; + +// FSR1 RCAS (robust contrast-adaptive sharpening), same source: sharpens as +// far as it can without pushing any channel outside its neighbourhood's range. +constexpr const char* kSpriteSharpenSource = R"glsl(#version 330 +uniform sampler2D uTexture; +uniform float uSharpness; +out vec4 fragColor; +void main() { + ivec2 size = textureSize(uTexture, 0); + ivec2 p = ivec2(gl_FragCoord.xy); + vec4 b = texelFetch(uTexture, clamp(p + ivec2(0, -1), ivec2(0), size - 1), 0); + vec4 d = texelFetch(uTexture, clamp(p + ivec2(-1, 0), ivec2(0), size - 1), 0); + vec4 e = texelFetch(uTexture, p, 0); + vec4 f = texelFetch(uTexture, clamp(p + ivec2(1, 0), ivec2(0), size - 1), 0); + vec4 h = texelFetch(uTexture, clamp(p + ivec2(0, 1), ivec2(0), size - 1), 0); + vec4 lowest = min(min(b, d), min(f, h)); + vec4 highest = max(max(b, d), max(f, h)); + vec4 hitMin = min(lowest, e) / max(4.0 * highest, vec4(1e-4)); + vec4 hitMax = (vec4(1.0) - max(highest, e)) / min(4.0 * lowest - 4.0, vec4(-1e-4)); + vec4 lobes = max(-hitMin, hitMax); + float lobe = max(-0.1875, min(max(max(lobes.r, lobes.g), max(lobes.b, lobes.a)), 0.0)) * + uSharpness; + fragColor = clamp((lobe * (b + d + f + h) + e) / (4.0 * lobe + 1.0), 0.0, 1.0); +} +)glsl"; + // Heat haze. Redraws the scene with its lookup rippled where the blurred // emissive image (uMask) is warm, i.e. redder than it is blue, so fires and // torches shimmer and cold or white magic does not. The mask is also read @@ -509,7 +655,13 @@ bool create_shared_resources() noexcept { return false; } resources.shimmer.id = build_program(kShimmerSource, kValueNoiseSource); - if (!resources.shimmer.id) return false; + resources.spriteUpscale = build_program(kSpriteUpscaleSource); + resources.spriteSharpen.id = build_program(kSpriteSharpenSource); + if (!resources.shimmer.id || !resources.spriteUpscale || !resources.spriteSharpen.id) { + return false; + } + resources.spriteSharpen.sharpness = + fn.glGetUniformLocation(resources.spriteSharpen.id, "uSharpness"); auto& shimmer = resources.shimmer; shimmer.worldOrigin = fn.glGetUniformLocation(shimmer.id, "uWorldOrigin"); shimmer.worldSize = fn.glGetUniformLocation(shimmer.id, "uWorldSize"); @@ -554,8 +706,9 @@ bool create_sized_resources(game::Extent viewport) noexcept { return true; } -// Called inside the guards' scope: creation changes bindings. -bool ensure_resources(game::Extent viewport) noexcept { +// Programs and the triangle, per GL context. Called inside the guards' scope: +// creation changes bindings. +bool ensure_shared_resources() noexcept { const auto context = gl::current_context(); // Creation binds textures; keep that on unit 0, which the caller's guard restores. gl::get_gl_functions().glActiveTexture(gl::TEXTURE0); @@ -568,6 +721,12 @@ bool ensure_resources(game::Extent viewport) noexcept { return false; } } + return true; +} + +// Called inside the guards' scope: creation changes bindings. +bool ensure_resources(game::Extent viewport) noexcept { + if (!ensure_shared_resources()) return false; if (!(viewport == g_resources.viewport)) { gl::discard_errors(); if (!create_sized_resources(viewport) || !gl::check_error("world post targets")) return false; @@ -664,34 +823,112 @@ void draw_bloom() noexcept { void world_post_configure(const core::EngineConfig& cfg) noexcept { g_settings = {cfg.softFogOfWar, cfg.bloom, cfg.softFogRadius, cfg.bloomStrength, - cfg.lightSpill, cfg.softFogDrift, cfg.softFogSmoothing, cfg.heatShimmer}; + cfg.lightSpill, cfg.softFogDrift, cfg.softFogSmoothing, cfg.heatShimmer, + cfg.spriteUpscale, cfg.spriteSharpness}; } bool world_post_active() noexcept { - return !g_failed && (g_settings.softFog || g_settings.bloom); + // Sprite upscaling needs the world flushed before the UI is queued, which + // is what the fog detour does when this is true. + return !g_failed && (g_settings.softFog || g_settings.bloom || g_settings.spriteUpscale); } void world_post_poll_hotkeys() noexcept { static bool fogKeyWasDown = false; static bool bloomKeyWasDown = false; + static bool spriteKeyWasDown = false; const bool fogKeyDown = (GetAsyncKeyState(VK_F8) & 0x8000) != 0; const bool bloomKeyDown = (GetAsyncKeyState(VK_F9) & 0x8000) != 0; + const bool spriteKeyDown = (GetAsyncKeyState(VK_F7) & 0x8000) != 0; const bool fogPressed = fogKeyDown && !fogKeyWasDown; const bool bloomPressed = bloomKeyDown && !bloomKeyWasDown; + const bool spritePressed = spriteKeyDown && !spriteKeyWasDown; fogKeyWasDown = fogKeyDown; bloomKeyWasDown = bloomKeyDown; - if (!fogPressed && !bloomPressed) return; + spriteKeyWasDown = spriteKeyDown; + if (!fogPressed && !bloomPressed && !spritePressed) return; if (fogPressed) g_settings.softFog = !g_settings.softFog; if (bloomPressed) g_settings.bloom = !g_settings.bloom; + if (spritePressed) g_settings.spriteUpscale = !g_settings.spriteUpscale; try { - LOG_INFO("Hotkey: soft fog of war={}, bloom={}", g_settings.softFog, g_settings.bloom); + LOG_INFO("Hotkey: soft fog of war={}, bloom={}, sprite upscale={}", g_settings.softFog, + g_settings.bloom, g_settings.spriteUpscale); } catch (...) { } } void world_post_on_frame() noexcept { ++g_frame; - g_worldPassOpen = true; + // The world pass is only open on frames that follow a frame which drew an + // area: in menus and full-screen panels every draw is UI. + g_worldPassOpen = g_fogSeenThisFrame; + g_fogSeenThisFrame = false; +} + +bool world_post_wants_atlas_upscale() noexcept { + return g_worldPassOpen && g_settings.spriteUpscale && !g_failed; +} + +unsigned world_post_upscale_atlas(int slot, unsigned sourceTexture, int width, int height, + int rows) noexcept { + const auto& fn = gl::get_gl_functions(); + if (!fn.postProcessAvailable || slot < 0 || slot >= 2 || !sourceTexture || width <= 0 || + height <= 0 || width > 4096 || height > 4096 || rows <= 0) { + return 0; + } + int drawFramebuffer = 0; + int readFramebuffer = 0; + fn.glGetIntegerv(gl::DRAW_FRAMEBUFFER_BINDING, &drawFramebuffer); + fn.glGetIntegerv(gl::READ_FRAMEBUFFER_BINDING, &readFramebuffer); + unsigned result = 0; + { + core::GlStateGuard textures({0}); + core::GlPassGuard pass; + const game::Extent doubled{width * 2, height * 2}; + bool ready = ensure_shared_resources(); + if (ready) { + auto& atlas = g_resources.atlases[static_cast(slot)]; + if (!(atlas.sharpened.extent == doubled)) { + gl::discard_errors(); + ready = create_target(atlas.upscaled, doubled, gl::RGBA8, gl::UNSIGNED_BYTE) && + create_target(atlas.sharpened, doubled, gl::RGBA8, gl::UNSIGNED_BYTE) && + gl::check_error("sprite atlas targets"); + } + if (ready) { + // Only the rows the engine just filled; two rows of margin for the kernel. + const int outputRows = rows * 2 + 4 < doubled.height ? rows * 2 + 4 : doubled.height; + set_pass_state(); + fn.glViewport(0, 0, doubled.width, outputRows); + fn.glBindFramebuffer(gl::FRAMEBUFFER, atlas.upscaled.framebuffer); + fn.glUseProgram(g_resources.spriteUpscale); + fn.glBindTexture(gl::TEXTURE_2D, sourceTexture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); + fn.glBindFramebuffer(gl::FRAMEBUFFER, atlas.sharpened.framebuffer); + fn.glUseProgram(g_resources.spriteSharpen.id); + fn.glUniform1f(g_resources.spriteSharpen.sharpness, g_settings.spriteSharpness); + fn.glBindTexture(gl::TEXTURE_2D, atlas.upscaled.texture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); + result = atlas.sharpened.texture; + if (!g_resources.atlasDrawnOnce) { + g_resources.atlasDrawnOnce = true; + if (!gl::check_error("sprite atlas upscale")) { + result = 0; + ready = false; + } else { + try { + LOG_INFO("World post: first sprite atlas upscaled ({}x{} -> {}x{}, {} rows)", width, + height, doubled.width, doubled.height, rows); + } catch (...) { + } + } + } + } + } + fn.glBindFramebuffer(gl::DRAW_FRAMEBUFFER, static_cast(drawFramebuffer)); + fn.glBindFramebuffer(gl::READ_FRAMEBUFFER, static_cast(readFramebuffer)); + if (!ready) fail("the sprite atlas upscale could not run"); + } + return result; } bool world_post_wants_emissive() noexcept { @@ -744,6 +981,7 @@ bool world_post_before_fog(const WorldView& view) noexcept { g_view = view; const auto& fn = gl::get_gl_functions(); g_worldPassOpen = false; + g_fogSeenThisFrame = true; if (!fn.postProcessAvailable) { fail("the GL context lacks framebuffer or vertex-array support"); return false; diff --git a/src/iee/features/world_post.h b/src/iee/features/world_post.h index c7cca1b..e17b5a4 100644 --- a/src/iee/features/world_post.h +++ b/src/iee/features/world_post.h @@ -28,7 +28,7 @@ struct WorldView { void world_post_configure(const core::EngineConfig& cfg) noexcept; // True when at least one effect is switched on and no pass has failed. bool world_post_active() noexcept; -// Debug A/B: F8 toggles soft fog of war, F9 toggles bloom. Call once per +// Debug A/B: F7 toggles sprite upscaling, F8 soft fog of war, F9 bloom. Call once per // frame, only when debug hotkeys are enabled. void world_post_poll_hotkeys() noexcept; // Frame boundary: the next world pass starts. @@ -46,6 +46,17 @@ void world_post_on_frame() noexcept; bool world_post_wants_emissive() noexcept; bool world_post_begin_emissive() noexcept; void world_post_end_emissive(int commands) noexcept; +// Sprite smoothing. The engine composites sprites on the CPU into a streaming +// atlas and uploads it at the start of a flush. Right after that upload, in +// the world pass, the atlas is upscaled 2x with FSR1 (edge-adaptive upsample, +// then sharpen); the caller makes the engine draw from the returned texture +// for the rest of that flush and puts the engine's own back afterwards. +// Normalised texture coordinates are unchanged by the larger texture. +bool world_post_wants_atlas_upscale() noexcept; +// Returns the GL name of the upscaled texture, or 0 to leave the atlas alone. +// `rows` is how many atlas rows the engine just uploaded. +unsigned world_post_upscale_atlas(int slot, unsigned sourceTexture, int width, int height, + int rows) noexcept; bool world_post_before_fog(const WorldView& view) noexcept; void world_post_after_fog() noexcept; // Any thread. The next frame logs what engaged. diff --git a/src/iee/game/build_manifest.cpp b/src/iee/game/build_manifest.cpp index e3f4286..9b44fc9 100644 --- a/src/iee/game/build_manifest.cpp +++ b/src/iee/game/build_manifest.cpp @@ -208,9 +208,12 @@ constexpr BuildManifest kKnownBuilds[] = { "85 D2", "48 89 5C 24 18 48 89 54 24 10 55 56 57 41 54 41 55 41 56 41 57 48 83 EC 40 " "44 0F B7 29 4C 8B F9 66 44 3B 69 02", + // TexSubImage_GL (PDB-named, offline-verified unique). + "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 48 89 7C 24 20 41 56 48 83 EC 50 " + "44 8B F1 41 8B F0 44 8B 05 ? ? ? ? 48 8D 0D ? ? ? ? 41 8B C0 8B EA", }, {0x27EBD0, 0x4257C0, 0x276700, 0x1F27D0, 0x36BA50, 0x36F170, 0x36E820, 0x4118A0, 0x411660, - 0x411780, 0x2A1B60, 0x42B350, 0x423C30, 0x425BE0}, + 0x411780, 0x2A1B60, 0x42B350, 0x423C30, 0x425BE0, 0x42D190}, {0x100, 0x1DC, 0x14, 0x6590, 0x6598, 0x65F8}, {{ {"CRes_Demand", 0x36, BranchInstructionKind::CallRel32, 0xE8, 1, 5, true}, @@ -254,6 +257,11 @@ constexpr BuildManifest kKnownBuilds[] = { // (12 bytes each); DrawBlendFunc_GL packs src<<9 | dst<<13; the // factor table at 0x5C12B0 has GL_ONE at index 1. {0x24, 0x180A8, 8192, 8, 13, 1}, + // Disassembly-verified: gl.user.state at 0x2F73F6C, gl.textures at + // 0x757040 (0x28-byte entries, GL name first), fx[] at 0x2F74050 + // (0x30 apart: width +0, height +4, CPU buffer +0x20, texture index + // +0x28); the selected texture is bits 21-29 of the state word. + {0x64, 0x281CF90, 0x28, 0x80, 0x30, 2, 0x00, 0x04, 0x20, 0x28, 21, 0x1FF}, }, }; @@ -286,6 +294,8 @@ static_assert([] { } return true; }(), "2.7.3 smoothed object render pattern format is invalid"); +static_assert(validate_pattern_format(kKnownBuilds[1].patterns.textureUpload), + "2.7.3 texture upload pattern format is invalid"); static_assert(validate_pattern_format(kKnownBuilds[1].patterns.particleUpdate) && validate_pattern_format(kKnownBuilds[1].patterns.particleRender), "2.7.3 particle pattern format is invalid"); diff --git a/src/iee/game/build_manifest.h b/src/iee/game/build_manifest.h index fd75c97..758a6a3 100644 --- a/src/iee/game/build_manifest.h +++ b/src/iee/game/build_manifest.h @@ -58,6 +58,9 @@ struct PatternSet { // fog of war, bloom); either empty or non-unique leaves both off. std::string_view renderFog{}; std::string_view drawFlush{}; + // Optional: TexSubImage_GL, the upload of the streaming sprite atlas at the + // start of a flush. Hooked to upscale the atlas before it is drawn from. + std::string_view textureUpload{}; // Optional pair: CParticle::AsynchronousUpdate and CParticle::Render (rain, // snow, sparkles). The update marks the logic tick; the render is wrapped // to draw each particle part of the way through its step. @@ -81,6 +84,7 @@ struct ReferenceRvas { std::uintptr_t drawFlush{}; std::uintptr_t particleUpdate{}; std::uintptr_t particleRender{}; + std::uintptr_t textureUpload{}; }; struct RuntimeOffsets { @@ -111,6 +115,30 @@ struct ExecutableVersion { // One per-object render (virtual Render(CGameArea*, CVidMode*) of a // CGameObject subclass) that draws from CGameObject::m_pos and can therefore // be movement-smoothed with the position swap. Each resolves independently. +// The streaming sprite atlases (fx[0], fx[1]) and the texture table, all in +// the same GL state block as the draw queue and addressed relative to gl.n. +// Sprites are composited on the CPU into an atlas's buffer, uploaded at the +// start of each flush, and drawn from the atlas's texture. +struct SpriteAtlasLayout { + std::uintptr_t userStateBeforeCount{}; // gl.user.state: selected texture in its bits + std::uintptr_t texturesBeforeCount{}; // gl.textures[0] + std::size_t textureEntrySize{}; // GL name is the first field of an entry + std::uintptr_t atlasAfterCount{}; // fx[0] + std::size_t atlasStride{}; + std::size_t atlasCount{}; + std::size_t widthOffset{}; + std::size_t heightOffset{}; + std::size_t texelsOffset{}; + std::size_t textureIndexOffset{}; + std::uint32_t textureShift{}; + std::uint32_t textureMask{}; + + [[nodiscard]] constexpr bool valid() const noexcept { + return texturesBeforeCount != 0 && textureEntrySize != 0 && atlasStride != 0 && + atlasCount != 0 && textureMask != 0; + } +}; + struct ObjectRenderTarget { const char* name{}; std::string_view pattern{}; @@ -150,6 +178,8 @@ struct BuildManifest { std::array smoothedObjectRenders{}; // Optional; an invalid (empty) layout leaves emissive bloom off. DrawQueueLayout drawQueue{}; + // Optional; an invalid (empty) layout leaves sprite upscaling off. + SpriteAtlasLayout spriteAtlas{}; [[nodiscard]] constexpr bool validate() const noexcept { if (buildId.empty() || supportedProductNames[0].empty() || executableVersion.major == 0 || diff --git a/src/iee/game/draw_queue.cpp b/src/iee/game/draw_queue.cpp index 3dbeb8d..4937bc6 100644 --- a/src/iee/game/draw_queue.cpp +++ b/src/iee/game/draw_queue.cpp @@ -37,4 +37,46 @@ std::uintptr_t draw_queue_commands_address(const DrawQueueLayout& layout, if (!countAddress || !layout.valid()) return 0; return countAddress - layout.commandsBeforeCount; } + +namespace { +template +T read_at(std::uintptr_t address) noexcept { + T value{}; + std::memcpy(&value, reinterpret_cast(address), sizeof(T)); + return value; +} +} // namespace + +int atlas_slot_for_upload(const SpriteAtlasLayout& layout, std::uintptr_t countAddress, + const void* pixels) noexcept { + if (!countAddress || !pixels || !layout.valid()) return -1; + const auto state = read_at(countAddress - layout.userStateBeforeCount); + const auto selected = (state >> layout.textureShift) & layout.textureMask; + for (std::size_t slot = 0; slot < layout.atlasCount; ++slot) { + const auto atlas = countAddress + layout.atlasAfterCount + slot * layout.atlasStride; + if (read_at(atlas + layout.texelsOffset) != + reinterpret_cast(pixels)) { + continue; + } + return read_at(atlas + layout.textureIndexOffset) == selected + ? static_cast(slot) + : -1; + } + return -1; +} + +SpriteAtlasInfo atlas_info(const SpriteAtlasLayout& layout, std::uintptr_t countAddress, + int slot) noexcept { + if (!countAddress || !layout.valid() || slot < 0 || + static_cast(slot) >= layout.atlasCount) { + return {}; + } + const auto atlas = + countAddress + layout.atlasAfterCount + static_cast(slot) * layout.atlasStride; + const auto index = read_at(atlas + layout.textureIndexOffset) & layout.textureMask; + return {read_at(atlas + layout.widthOffset), + read_at(atlas + layout.heightOffset), + reinterpret_cast(countAddress - layout.texturesBeforeCount + + index * layout.textureEntrySize)}; +} } // namespace iee::game diff --git a/src/iee/game/draw_queue.h b/src/iee/game/draw_queue.h index 44d20ad..b410a77 100644 --- a/src/iee/game/draw_queue.h +++ b/src/iee/game/draw_queue.h @@ -32,4 +32,20 @@ std::uintptr_t draw_queue_count_address(const DrawQueueLayout& layout, std::uintptr_t drawFlush) noexcept; std::uintptr_t draw_queue_commands_address(const DrawQueueLayout& layout, std::uintptr_t countAddress) noexcept; + +// Which sprite atlas the engine is uploading, given the pixel buffer passed +// to its upload: the buffer must be that atlas's own, and the texture the +// engine has selected must be that atlas's texture. -1 for any other upload. +int atlas_slot_for_upload(const SpriteAtlasLayout& layout, std::uintptr_t countAddress, + const void* pixels) noexcept; + +struct SpriteAtlasInfo { + int width{}; + int height{}; + // The atlas's GL texture name inside the engine's texture table; the engine + // binds whatever is stored here. Null for an invalid slot. + std::uint32_t* textureName{}; +}; +SpriteAtlasInfo atlas_info(const SpriteAtlasLayout& layout, std::uintptr_t countAddress, + int slot) noexcept; } // namespace iee::game diff --git a/src/iee/game/game_addrs.cpp b/src/iee/game/game_addrs.cpp index 88652b9..9331476 100644 --- a/src/iee/game/game_addrs.cpp +++ b/src/iee/game/game_addrs.cpp @@ -156,7 +156,7 @@ namespace iee::game { // Optional targets: world post passes. Resolved only when requested // (or when debug hotkeys can switch them on), and only as a pair. - if (cfg.softFogOfWar || cfg.bloom || cfg.enableDebugHotkeys) { + if (cfg.softFogOfWar || cfg.bloom || cfg.spriteUpscale || cfg.enableDebugHotkeys) { out.RenderFog = resolveOptional("CInfinity::RenderFog", manifest.patterns.renderFog, manifest.referenceRvas.renderFog); out.DrawFlush = resolveOptional("DrawFlush_GL", manifest.patterns.drawFlush, @@ -170,6 +170,15 @@ namespace iee::game { if (out.DrawQueueCount && out.DrawQueueCommands) { LOG_INFO("Draw queue resolved: count at RVA 0x{:X}, commands at RVA 0x{:X}", out.DrawQueueCount - moduleBase, out.DrawQueueCommands - moduleBase); + if (manifest.spriteAtlas.valid() && + (cfg.spriteUpscale || cfg.enableDebugHotkeys)) { + out.TextureUpload = + resolveOptional("TexSubImage_GL", manifest.patterns.textureUpload, + manifest.referenceRvas.textureUpload); + if (!out.TextureUpload && cfg.spriteUpscale) { + LOG_WARN("Sprite upscaling disabled: TexSubImage_GL did not resolve"); + } + } } else { out.DrawQueueCount = out.DrawQueueCommands = 0; if (cfg.bloom) { @@ -180,8 +189,8 @@ namespace iee::game { const bool known = !manifest.patterns.renderFog.empty() && !manifest.patterns.drawFlush.empty(); out.RenderFog = out.DrawFlush = 0; - if (cfg.softFogOfWar || cfg.bloom) { - LOG_WARN("Soft fog of war and bloom disabled: {}", + if (cfg.softFogOfWar || cfg.bloom || cfg.spriteUpscale) { + LOG_WARN("Soft fog of war, bloom and sprite upscaling disabled: {}", known ? "RenderFog/DrawFlush did not resolve uniquely" : "this build has no RenderFog/DrawFlush targets"); } diff --git a/src/iee/game/game_addrs.h b/src/iee/game/game_addrs.h index 52e33c0..22455cf 100644 --- a/src/iee/game/game_addrs.h +++ b/src/iee/game/game_addrs.h @@ -39,6 +39,9 @@ namespace iee::game { // array). 0 leaves the fog and flush hooks working without bloom. std::uintptr_t DrawQueueCount = 0; std::uintptr_t DrawQueueCommands = 0; + // Optional: the sprite atlas upload (TexSubImage_GL), for sprite + // upscaling. Needs the draw queue. + std::uintptr_t TextureUpload = 0; bool initialized = false; }; diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index db58b5d..15c0a1c 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -69,6 +69,16 @@ static DrawFlushFn g_drawFlush = nullptr; static core::Hook g_drawFlushHook; static std::int32_t* g_drawQueueCount = nullptr; static game::DrawCommand* g_drawQueueCommands = nullptr; +// TexSubImage_GL(x, y, width, rows, pixels, secondTexture): the engine's +// texture upload, hooked for the sprite atlas. +using TextureUploadFn = void (*)(int, int, int, int, void*, bool); +static core::Hook g_textureUploadHook; +// Atlas textures replaced by their upscaled copies for the current flush. +struct SwappedAtlas { + std::uint32_t* name{}; + std::uint32_t original{}; +}; +static std::array g_swappedAtlases{}; // CParticle::AsynchronousUpdate() and CParticle::Render(CPoint&, CRect&, type, n). using ParticleUpdateFn = unsigned char (*)(void*); @@ -510,7 +520,7 @@ static constexpr auto kObjectRenderDetours = // holds only those commands, with our black emissive target bound. The vertex // data and textures of the first flush are still in place: the engine only // overwrites them when new draws are queued. Render thread only. -static void detour_draw_flush() { +static void flush_with_emissive_replay() { const auto original = g_drawFlushHook.original(); if (!g_ctx || !g_drawQueueCount || !features::world_post_wants_emissive()) { original(); @@ -527,6 +537,41 @@ static void detour_draw_flush() { features::world_post_end_emissive(static_cast(collected)); } +static void detour_draw_flush() { + flush_with_emissive_replay(); + // The engine uploads into, and binds, whatever name its texture table + // holds: give it its own atlas textures back before the next upload. + for (auto& swapped : g_swappedAtlases) { + if (swapped.name) *swapped.name = swapped.original; + swapped = {}; + } +} + +// Sprite smoothing. A flush starts by uploading the CPU-composited sprite +// atlas, then draws from it. Right after that upload the atlas is upscaled +// (FSR1) into our own texture, whose name replaces the atlas's in the engine's +// texture table until the flush is over. The upload has just marked the +// texture as needing a rebind, so the first draw picks ours up. +static void detour_texture_upload(int x, int y, int width, int rows, void* pixels, bool second) { + g_textureUploadHook.original()(x, y, width, rows, pixels, second); + if (!g_ctx || !g_drawQueueCount || second || x != 0 || y != 0 || + !features::world_post_wants_atlas_upscale()) { + return; + } + const auto& layout = g_ctx->manifest->spriteAtlas; + const auto count = reinterpret_cast(g_drawQueueCount); + const int slot = game::atlas_slot_for_upload(layout, count, pixels); + if (slot < 0 || static_cast(slot) >= g_swappedAtlases.size()) return; + auto& swapped = g_swappedAtlases[static_cast(slot)]; + const auto atlas = game::atlas_info(layout, count, slot); + if (!atlas.textureName || swapped.name || atlas.width != width) return; + const unsigned upscaled = features::world_post_upscale_atlas(slot, *atlas.textureName, + atlas.width, atlas.height, rows); + if (!upscaled) return; + swapped = {atlas.textureName, *atlas.textureName}; + *atlas.textureName = upscaled; +} + // World post passes (soft fog of war, bloom). The engine queues every draw // and submits the queue in DrawFlush_GL at the end of the frame, so the world // only exists in the framebuffer once we flush, and the fog only lands in our @@ -781,8 +826,16 @@ bool install_all(AppContext& ctx) { reinterpret_cast(&detour_draw_flush)); g_drawFlushHook.enable(); LOG_INFO("DrawFlush_GL hook installed (bloom from the engine's additive draws)"); + if (ctx.addrs.TextureUpload) { + g_textureUploadHook.create(reinterpret_cast(ctx.addrs.TextureUpload), + reinterpret_cast(&detour_texture_upload)); + g_textureUploadHook.enable(); + LOG_INFO("TexSubImage_GL hook installed (sprite upscaling={})", + ctx.cfg.spriteUpscale); + } } } catch (...) { + (void)g_textureUploadHook.remove(); g_drawQueueCount = nullptr; g_drawQueueCommands = nullptr; (void)g_drawFlushHook.remove(); @@ -833,6 +886,7 @@ void uninstall_all() noexcept { } catch (...) { } + (void)g_textureUploadHook.remove(); (void)g_drawFlushHook.remove(); g_drawQueueCount = nullptr; g_drawQueueCommands = nullptr; @@ -869,6 +923,7 @@ void prepare_for_shutdown() noexcept { // state are torn down. MinHook itself stays initialized until // uninstall_all(), after every MinHook-backed subsystem has removed its // hooks. + (void)g_textureUploadHook.disable(); (void)g_drawFlushHook.disable(); (void)g_renderFogHook.disable(); (void)g_vidCellFrameSizeHook.disable(); diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index a7aab18..8610cb2 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1276,6 +1276,8 @@ void test_config_world_post() { expect_eq(defaults.softFogDrift, 10.0f, "Soft fog drift default"); expect_eq(defaults.softFogSmoothing, 0.25f, "Soft fog smoothing default"); expect_eq(defaults.heatShimmer, 1.5f, "Heat shimmer default"); + expect_true(!defaults.spriteUpscale, "Sprite upscaling defaults off"); + expect_eq(defaults.spriteSharpness, 0.5f, "Sprite sharpness default"); expect_eq(defaults.bloomStrength, 0.35f, "Bloom strength default"); const auto tempPath = @@ -1290,6 +1292,8 @@ void test_config_world_post() { out << "SoftFogDrift = 20\n"; out << "SoftFogSmoothing = 0.5\n"; out << "HeatShimmer = 4\n"; + out << "SpriteUpscale = true\n"; + out << "SpriteSharpness = 0.8\n"; out << "BloomStrength = 0.5\n"; } iee::core::EngineConfig cfg{}; @@ -1300,6 +1304,8 @@ void test_config_world_post() { expect_eq(cfg.softFogDrift, 20.0f, "Soft fog drift should parse"); expect_eq(cfg.softFogSmoothing, 0.5f, "Soft fog smoothing should parse"); expect_eq(cfg.heatShimmer, 4.0f, "Heat shimmer should parse"); + expect_true(cfg.spriteUpscale, "Sprite upscaling should parse"); + expect_eq(cfg.spriteSharpness, 0.8f, "Sprite sharpness should parse"); expect_eq(cfg.bloomStrength, 0.5f, "Bloom strength should parse"); { @@ -1310,6 +1316,7 @@ void test_config_world_post() { out << "SoftFogDrift = -4\n"; out << "SoftFogSmoothing = 99\n"; out << "HeatShimmer = 500\n"; + out << "SpriteSharpness = 9\n"; out << "BloomStrength = -3\n"; } cfg = {}; @@ -1319,6 +1326,7 @@ void test_config_world_post() { expect_eq(cfg.softFogDrift, 0.0f, "Soft fog drift is clamped to 0"); expect_eq(cfg.softFogSmoothing, 2.0f, "Soft fog smoothing is clamped to its maximum"); expect_eq(cfg.heatShimmer, 12.0f, "Heat shimmer is clamped to its maximum"); + expect_eq(cfg.spriteSharpness, 1.0f, "Sprite sharpness is clamped to 1"); expect_eq(cfg.bloomStrength, 0.0f, "Bloom strength is clamped to 0"); { @@ -1465,6 +1473,74 @@ void test_tick_clock_and_particle_backstep() { "A phase past the end draws the logic position"); } +void test_sprite_atlas_lookup() { + using namespace iee::game; + const auto found = find_manifest("BGEE 2.7.3.x"); + expect_true(found.has_value(), "The 2.7.3 manifest should be registered"); + if (!found) return; + expect_true(found->get().spriteAtlas.valid(), "2.7.3 should describe the sprite atlas"); + expect_true(!found->get().patterns.textureUpload.empty(), "2.7.3 should carry the upload pattern"); + expect_eq(found->get().referenceRvas.textureUpload, std::uintptr_t{0x42D190}, + "TexSubImage_GL reference RVA"); + expect_true(!current_manifest().spriteAtlas.valid(), "2.6.6 has no sprite atlas layout"); + + // A small stand-in for the engine's GL state block, same shape, smaller gaps. + SpriteAtlasLayout layout{}; + layout.userStateBeforeCount = 0x64; + layout.texturesBeforeCount = 0x800; + layout.textureEntrySize = 0x28; + layout.atlasAfterCount = 0x80; + layout.atlasStride = 0x30; + layout.atlasCount = 2; + layout.widthOffset = 0x00; + layout.heightOffset = 0x04; + layout.texelsOffset = 0x20; + layout.textureIndexOffset = 0x28; + layout.textureShift = 21; + layout.textureMask = 0x1FF; + expect_true(layout.valid(), "The stand-in layout is complete"); + + std::vector block(0x1000, 0); + const auto count = reinterpret_cast(block.data()) + 0x800; + const auto put32 = [&](std::size_t offset, std::uint32_t value) { + std::memcpy(block.data() + offset, &value, sizeof(value)); + }; + const auto put64 = [&](std::size_t offset, std::uint64_t value) { + std::memcpy(block.data() + offset, &value, sizeof(value)); + }; + int texelsA = 0; + int texelsB = 0; + // Atlas 0: 1024x1024, texture index 3. Atlas 1: 1024x512, texture index 4. + put32(0x880 + 0x00, 1024); + put32(0x880 + 0x04, 1024); + put64(0x880 + 0x20, reinterpret_cast(&texelsA)); + put32(0x880 + 0x28, 3); + put32(0x8B0 + 0x00, 1024); + put32(0x8B0 + 0x04, 512); + put64(0x8B0 + 0x20, reinterpret_cast(&texelsB)); + put32(0x8B0 + 0x28, 4); + put32(3 * 0x28, 77); // GL name of texture 3 + put32(4 * 0x28, 88); + + put32(0x800 - 0x64, 4u << 21); // the engine has texture 4 selected + expect_eq(atlas_slot_for_upload(layout, count, &texelsB), 1, + "An upload of an atlas's own buffer to its own texture is recognised"); + expect_eq(atlas_slot_for_upload(layout, count, &texelsA), -1, + "The other atlas's buffer with the wrong texture selected is not"); + int other = 0; + expect_eq(atlas_slot_for_upload(layout, count, &other), -1, "Any other upload is ignored"); + expect_eq(atlas_slot_for_upload(layout, count, nullptr), -1, "A null buffer is ignored"); + expect_eq(atlas_slot_for_upload(layout, 0, &texelsB), -1, "An unresolved state block is ignored"); + + const auto atlas = atlas_info(layout, count, 1); + expect_eq(atlas.width, 1024, "The atlas width is read from its descriptor"); + expect_eq(atlas.height, 512, "The atlas height is read from its descriptor"); + expect_true(atlas.textureName != nullptr && *atlas.textureName == 88u, + "The atlas's GL texture name is found in the texture table"); + expect_true(atlas_info(layout, count, 2).textureName == nullptr, "An out-of-range slot has none"); + expect_true(atlas_info(layout, count, -1).textureName == nullptr, "A negative slot has none"); +} + void test_world_post_plan() { using namespace iee::game; @@ -2337,6 +2413,7 @@ int main() { test_world_post_plan(); test_tick_clock_and_particle_backstep(); test_draw_queue(); + test_sprite_atlas_lookup(); test_build_area_effect_points(); test_config_detection_section(); test_config_world_post(); diff --git a/tools/InfinityEngine-Enhancer.sample.ini b/tools/InfinityEngine-Enhancer.sample.ini index f1c84f0..f10baa3 100644 --- a/tools/InfinityEngine-Enhancer.sample.ini +++ b/tools/InfinityEngine-Enhancer.sample.ini @@ -38,6 +38,13 @@ LightSpill = 1.5 ; Heat haze above fires and other warm light sources: how far (world pixels) ; the air ripples; 0 = off. Needs Bloom. HeatShimmer = 1.5 +; Smoother creature and effect sprites: an edge-adaptive 2x upscale (FSR1) of +; the engine's sprite sheet before it is drawn in the world view. It rounds off +; stair-stepped outlines; it cannot add detail. SpriteSharpness (0..1) is the +; strength of FSR1's sharpening pass. BGEE 2.7.3 only. With +; EnableDebugHotkeys, F7 toggles it. +SpriteUpscale = false +SpriteSharpness = 0.5 [Detection] ; Collect the current area's authored ambient animations from engine memory From 0ee147b5e90d63994d9f77ba63029f02a4876c52 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 8 Oct 2026 00:22:16 +0200 Subject: [PATCH 48/57] World pass gate: tolerate repeated frame boundaries (bloom and sprite upscaling never engaged) --- src/iee/features/world_post.cpp | 31 ++++++++++++++++++------------- src/iee/game/world_post_plan.h | 27 +++++++++++++++++++++++++++ tests/iee_tests.cpp | 29 +++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 13 deletions(-) diff --git a/src/iee/features/world_post.cpp b/src/iee/features/world_post.cpp index 601ec0f..df0d720 100644 --- a/src/iee/features/world_post.cpp +++ b/src/iee/features/world_post.cpp @@ -140,8 +140,11 @@ std::atomic g_dropFogHistory{false}; // frame boundary to the fog; additive draws queued later belong to the UI. unsigned g_frame = 1; unsigned g_emissiveFrame = 0; -bool g_worldPassOpen = false; -bool g_fogSeenThisFrame = false; +game::WorldPassGate g_worldPass; +// Field counters for the per-area log line: frame boundaries seen and sprite +// atlases upscaled since the previous area frame. +int g_frameTicksSinceArea = 0; +int g_atlasUpscalesSinceArea = 0; int g_emissiveCommands = 0; int g_savedDrawFramebuffer = 0; int g_savedReadFramebuffer = 0; @@ -859,14 +862,12 @@ void world_post_poll_hotkeys() noexcept { void world_post_on_frame() noexcept { ++g_frame; - // The world pass is only open on frames that follow a frame which drew an - // area: in menus and full-screen panels every draw is UI. - g_worldPassOpen = g_fogSeenThisFrame; - g_fogSeenThisFrame = false; + g_worldPass.on_frame(); + ++g_frameTicksSinceArea; } bool world_post_wants_atlas_upscale() noexcept { - return g_worldPassOpen && g_settings.spriteUpscale && !g_failed; + return g_worldPass.open() && g_settings.spriteUpscale && !g_failed; } unsigned world_post_upscale_atlas(int slot, unsigned sourceTexture, int width, int height, @@ -909,6 +910,7 @@ unsigned world_post_upscale_atlas(int slot, unsigned sourceTexture, int width, i fn.glBindTexture(gl::TEXTURE_2D, atlas.upscaled.texture); fn.glDrawArrays(gl::TRIANGLES, 0, 3); result = atlas.sharpened.texture; + ++g_atlasUpscalesSinceArea; if (!g_resources.atlasDrawnOnce) { g_resources.atlasDrawnOnce = true; if (!gl::check_error("sprite atlas upscale")) { @@ -932,7 +934,7 @@ unsigned world_post_upscale_atlas(int slot, unsigned sourceTexture, int width, i } bool world_post_wants_emissive() noexcept { - return g_worldPassOpen && g_settings.bloom && !g_failed; + return g_worldPass.open() && g_settings.bloom && !g_failed; } bool world_post_begin_emissive() noexcept { @@ -980,8 +982,7 @@ void world_post_end_emissive(int commands) noexcept { bool world_post_before_fog(const WorldView& view) noexcept { g_view = view; const auto& fn = gl::get_gl_functions(); - g_worldPassOpen = false; - g_fogSeenThisFrame = true; + g_worldPass.on_area_drawn(); if (!fn.postProcessAvailable) { fail("the GL context lacks framebuffer or vertex-array support"); return false; @@ -1020,11 +1021,13 @@ bool world_post_before_fog(const WorldView& view) noexcept { try { LOG_INFO( "World post: softFog={} (radius {} world px, blur levels={} offset={:.2f}), " - "bloom={} (strength {}, light spill {}, {} additive draws this frame), viewport " - "{}x{}, {:.3f} px per world px, engine framebuffer draw={} read={}", + "bloom={} (strength {}, light spill {}, {} additive draws this frame), sprite " + "upscale={} ({} atlas passes, {} frame boundaries since the last area frame), " + "viewport {}x{}, {:.3f} px per world px, engine framebuffer draw={} read={}", g_settings.softFog, g_settings.fogRadius, plan.levels, plan.offset, g_settings.bloom, g_settings.bloomStrength, g_settings.lightSpill, - g_emissiveFrame == g_frame ? g_emissiveCommands : 0, viewport[2], viewport[3], + g_emissiveFrame == g_frame ? g_emissiveCommands : 0, g_settings.spriteUpscale, + g_atlasUpscalesSinceArea, g_frameTicksSinceArea, viewport[2], viewport[3], g_pixelsPerWorldPixel, g_engineDrawFramebuffer, g_engineReadFramebuffer); } catch (...) { } @@ -1032,6 +1035,8 @@ bool world_post_before_fog(const WorldView& view) noexcept { } bind_engine_framebuffer(); } + g_frameTicksSinceArea = 0; + g_atlasUpscalesSinceArea = 0; // The guards have restored the engine's state; hand it our target for the // fog draw only. Framebuffer bindings are not part of the engine's cache. if (capturing) fn.glBindFramebuffer(gl::FRAMEBUFFER, g_resources.fogCapture.framebuffer); diff --git a/src/iee/game/world_post_plan.h b/src/iee/game/world_post_plan.h index 9638c22..b642cc7 100644 --- a/src/iee/game/world_post_plan.h +++ b/src/iee/game/world_post_plan.h @@ -25,6 +25,33 @@ int available_blur_levels(Extent base) noexcept; // The plan whose blur reaches about `radiusPixels` on an image of `base` size. BlurPlan blur_plan_for_radius(float radiusPixels, Extent base) noexcept; int bloom_levels(Extent base) noexcept; +// Tells world draws from UI draws by where a frame is. Within a frame that +// shows an area, everything queued up to the fog of war is world and everything +// after it is UI; a frame that shows no area (menus, full-screen panels) is +// all UI. The frame boundary may be reported more than once per presented +// frame, so "an area was drawn recently" is counted in boundaries, not exact. +class WorldPassGate { + public: + void on_frame() noexcept { + if (sinceArea_ < kNever) ++sinceArea_; + closedForFrame_ = false; + } + // The fog of war is about to be drawn: the world part of this frame is over. + void on_area_drawn() noexcept { + sinceArea_ = 0; + closedForFrame_ = true; + } + [[nodiscard]] bool open() const noexcept { + return !closedForFrame_ && sinceArea_ <= kTolerance; + } + + private: + static constexpr int kTolerance = 4; + static constexpr int kNever = 1000; + int sinceArea_{kNever}; + bool closedForFrame_{}; +}; + // How far to move the shown image towards the new one after `stepSeconds`, // for an exponential approach with time constant `smoothingSeconds`. 1 means // "show the new image": no smoothing, or a history too old to trust. diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 8610cb2..4e89355 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1541,6 +1541,34 @@ void test_sprite_atlas_lookup() { expect_true(atlas_info(layout, count, -1).textureName == nullptr, "A negative slot has none"); } +void test_world_pass_gate() { + using namespace iee::game; + WorldPassGate gate; + expect_true(!gate.open(), "Nothing is world before an area has been drawn"); + gate.on_frame(); + expect_true(!gate.open(), "A frame boundary alone does not open the world pass"); + + gate.on_area_drawn(); + expect_true(!gate.open(), "After the area is drawn the rest of the frame is UI"); + gate.on_frame(); + expect_true(gate.open(), "The frame after an area frame starts in the world pass"); + gate.on_area_drawn(); + expect_true(!gate.open(), "It closes again once that frame's area is drawn"); + + // The frame boundary can be reported more than once per presented frame. + gate.on_frame(); + gate.on_frame(); + expect_true(gate.open(), "A doubled frame boundary still opens the world pass"); + gate.on_area_drawn(); + gate.on_frame(); + gate.on_frame(); + gate.on_frame(); + expect_true(gate.open(), "A few boundaries without an area are tolerated"); + + for (int i = 0; i < 8; ++i) gate.on_frame(); + expect_true(!gate.open(), "With no area drawn for several frames, everything is UI"); +} + void test_world_post_plan() { using namespace iee::game; @@ -2411,6 +2439,7 @@ int main() { test_sprite_motion_tracker(); test_animation_interpolation(); test_world_post_plan(); + test_world_pass_gate(); test_tick_clock_and_particle_backstep(); test_draw_queue(); test_sprite_atlas_lookup(); From bc09968cf9a7000691c2eddb4c82d2a5b943531e Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 8 Oct 2026 00:52:05 +0200 Subject: [PATCH 49/57] Sprite upscaling: field log of the first texture uploads and the atlas lookup inputs --- src/iee/hooks.cpp | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index 15c0a1c..510ed88 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -554,12 +554,46 @@ static void detour_draw_flush() { // texture as needing a rebind, so the first draw picks ours up. static void detour_texture_upload(int x, int y, int width, int rows, void* pixels, bool second) { g_textureUploadHook.original()(x, y, width, rows, pixels, second); - if (!g_ctx || !g_drawQueueCount || second || x != 0 || y != 0 || - !features::world_post_wants_atlas_upscale()) { - return; - } + if (!g_ctx || !g_drawQueueCount) return; const auto& layout = g_ctx->manifest->spriteAtlas; const auto count = reinterpret_cast(g_drawQueueCount); + // Field log: the first uploads seen while sprite upscaling wants to run, + // with everything the atlas lookup compares, so a mismatch is visible. + static int reached = 0; + if (reached < 4) { + ++reached; + try { + LOG_INFO("Texture upload hook reached (#{}, width={}, rows={}, wants upscale={})", reached, + width, rows, features::world_post_wants_atlas_upscale()); + } catch (...) { + } + } + static int logged = 0; + if (logged < 12 && features::world_post_wants_atlas_upscale()) { + ++logged; + try { + const auto word = [&](std::ptrdiff_t offset) { + std::uint32_t value = 0; + std::memcpy(&value, reinterpret_cast(count + offset), sizeof(value)); + return value; + }; + const auto pointer = [&](std::ptrdiff_t offset) { + std::uintptr_t value = 0; + std::memcpy(&value, reinterpret_cast(count + offset), sizeof(value)); + return value; + }; + LOG_INFO( + "Texture upload #{}: x={} y={} width={} rows={} second={} pixels=0x{:X} | selected " + "texture={} | atlas0: {}x{} buffer=0x{:X} texture={} | atlas1: {}x{} buffer=0x{:X} " + "texture={} | slot={}", + logged, x, y, width, rows, second, reinterpret_cast(pixels), + (word(-0x64) >> 21) & 0x1FF, word(0x80), word(0x84), pointer(0xA0), word(0xA8), + word(0xB0), word(0xB4), pointer(0xD0), word(0xD8), + game::atlas_slot_for_upload(layout, count, pixels)); + } catch (...) { + } + } + if (second || x != 0 || y != 0 || !features::world_post_wants_atlas_upscale()) return; const int slot = game::atlas_slot_for_upload(layout, count, pixels); if (slot < 0 || static_cast(slot) >= g_swappedAtlases.size()) return; auto& swapped = g_swappedAtlases[static_cast(slot)]; From 9c25aba92038d46f4e9018e4df151b2c3a090281 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 8 Oct 2026 01:00:45 +0200 Subject: [PATCH 50/57] Sprite upscaling: log flush and upload counters and the atlas's pending rows --- src/iee/hooks.cpp | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index 510ed88..d809a96 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -537,7 +537,35 @@ static void flush_with_emissive_replay() { features::world_post_end_emissive(static_cast(collected)); } +// Field counters while the sprite path is being verified in game. +static std::atomic g_flushCalls{0}; +static std::atomic g_uploadCalls{0}; + +static void log_flush_diagnostics() { + // The first flushes that have draws queued: how many rows each sprite atlas + // has pending (what the engine is about to upload), and the hook counters. + static int logged = 0; + if (logged >= 6 || !g_drawQueueCount || *g_drawQueueCount <= 0) return; + ++logged; + const auto count = reinterpret_cast(g_drawQueueCount); + const auto word = [&](std::ptrdiff_t offset) { + std::uint32_t value = 0; + std::memcpy(&value, reinterpret_cast(count + offset), sizeof(value)); + return value; + }; + LOG_INFO( + "Flush #{} ({} flush calls, {} upload calls so far): {} commands | atlas0 pending " + "y={} h={} texture={} | atlas1 pending y={} h={} texture={} | wants upscale={}", + logged, g_flushCalls.load(), g_uploadCalls.load(), *g_drawQueueCount, word(0x94), word(0x98), + word(0xA8), word(0xC4), word(0xC8), word(0xD8), features::world_post_wants_atlas_upscale()); +} + static void detour_draw_flush() { + ++g_flushCalls; + try { + log_flush_diagnostics(); + } catch (...) { + } flush_with_emissive_replay(); // The engine uploads into, and binds, whatever name its texture table // holds: give it its own atlas textures back before the next upload. @@ -553,6 +581,7 @@ static void detour_draw_flush() { // texture table until the flush is over. The upload has just marked the // texture as needing a rebind, so the first draw picks ours up. static void detour_texture_upload(int x, int y, int width, int rows, void* pixels, bool second) { + ++g_uploadCalls; g_textureUploadHook.original()(x, y, width, rows, pixels, second); if (!g_ctx || !g_drawQueueCount) return; const auto& layout = g_ctx->manifest->spriteAtlas; @@ -864,8 +893,8 @@ bool install_all(AppContext& ctx) { g_textureUploadHook.create(reinterpret_cast(ctx.addrs.TextureUpload), reinterpret_cast(&detour_texture_upload)); g_textureUploadHook.enable(); - LOG_INFO("TexSubImage_GL hook installed (sprite upscaling={})", - ctx.cfg.spriteUpscale); + LOG_INFO("TexSubImage_GL hook installed at 0x{:X} (sprite upscaling={})", + ctx.addrs.TextureUpload, ctx.cfg.spriteUpscale); } } } catch (...) { From e03e17554b7b9dfda8378d029577071622bd244d Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 8 Oct 2026 01:05:18 +0200 Subject: [PATCH 51/57] Manifest: TexSubImage_GL pattern was in the particle slots (sprite upscaling hooked the wrong functions); pin pattern slots at compile time --- src/iee/game/build_manifest.cpp | 10 +++++ src/iee/game/build_manifest.h | 9 +++-- src/iee/hooks.cpp | 65 --------------------------------- tests/iee_tests.cpp | 12 ++++++ 4 files changed, 28 insertions(+), 68 deletions(-) diff --git a/src/iee/game/build_manifest.cpp b/src/iee/game/build_manifest.cpp index 9b44fc9..a6dc695 100644 --- a/src/iee/game/build_manifest.cpp +++ b/src/iee/game/build_manifest.cpp @@ -296,6 +296,16 @@ static_assert([] { }(), "2.7.3 smoothed object render pattern format is invalid"); static_assert(validate_pattern_format(kKnownBuilds[1].patterns.textureUpload), "2.7.3 texture upload pattern format is invalid"); +// Positional initialisation guard: each slot must hold its own function's bytes. +static_assert(kKnownBuilds[1].patterns.renderFog.starts_with("48 89 5C 24 10 4C 89 44 24 18") && + kKnownBuilds[1].patterns.drawFlush.starts_with("4C 8B DC 55 41 56 41 57") && + kKnownBuilds[1].patterns.particleUpdate.starts_with( + "48 89 5C 24 18 57 48 83 EC 20 8B 51 10") && + kKnownBuilds[1].patterns.particleRender.starts_with( + "48 89 5C 24 18 48 89 54 24 10 55 56 57") && + kKnownBuilds[1].patterns.textureUpload.starts_with( + "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18"), + "a 2.7.3 hook pattern is in the wrong PatternSet slot"); static_assert(validate_pattern_format(kKnownBuilds[1].patterns.particleUpdate) && validate_pattern_format(kKnownBuilds[1].patterns.particleRender), "2.7.3 particle pattern format is invalid"); diff --git a/src/iee/game/build_manifest.h b/src/iee/game/build_manifest.h index 758a6a3..9eae67e 100644 --- a/src/iee/game/build_manifest.h +++ b/src/iee/game/build_manifest.h @@ -28,6 +28,9 @@ struct BranchInstructionDesc { } }; +// Initialised positionally in build_manifest.cpp, in this order: add new +// members at the end, and pin each pattern's first bytes there with a +// static_assert so a value in the wrong slot cannot compile. struct PatternSet { std::string_view loadArea{}; std::string_view renderTexture{}; @@ -58,14 +61,14 @@ struct PatternSet { // fog of war, bloom); either empty or non-unique leaves both off. std::string_view renderFog{}; std::string_view drawFlush{}; - // Optional: TexSubImage_GL, the upload of the streaming sprite atlas at the - // start of a flush. Hooked to upscale the atlas before it is drawn from. - std::string_view textureUpload{}; // Optional pair: CParticle::AsynchronousUpdate and CParticle::Render (rain, // snow, sparkles). The update marks the logic tick; the render is wrapped // to draw each particle part of the way through its step. std::string_view particleUpdate{}; std::string_view particleRender{}; + // Optional: TexSubImage_GL, the upload of the streaming sprite atlas at the + // start of a flush. Hooked to upscale the atlas before it is drawn from. + std::string_view textureUpload{}; }; struct ReferenceRvas { diff --git a/src/iee/hooks.cpp b/src/iee/hooks.cpp index d809a96..30175ab 100644 --- a/src/iee/hooks.cpp +++ b/src/iee/hooks.cpp @@ -537,35 +537,7 @@ static void flush_with_emissive_replay() { features::world_post_end_emissive(static_cast(collected)); } -// Field counters while the sprite path is being verified in game. -static std::atomic g_flushCalls{0}; -static std::atomic g_uploadCalls{0}; - -static void log_flush_diagnostics() { - // The first flushes that have draws queued: how many rows each sprite atlas - // has pending (what the engine is about to upload), and the hook counters. - static int logged = 0; - if (logged >= 6 || !g_drawQueueCount || *g_drawQueueCount <= 0) return; - ++logged; - const auto count = reinterpret_cast(g_drawQueueCount); - const auto word = [&](std::ptrdiff_t offset) { - std::uint32_t value = 0; - std::memcpy(&value, reinterpret_cast(count + offset), sizeof(value)); - return value; - }; - LOG_INFO( - "Flush #{} ({} flush calls, {} upload calls so far): {} commands | atlas0 pending " - "y={} h={} texture={} | atlas1 pending y={} h={} texture={} | wants upscale={}", - logged, g_flushCalls.load(), g_uploadCalls.load(), *g_drawQueueCount, word(0x94), word(0x98), - word(0xA8), word(0xC4), word(0xC8), word(0xD8), features::world_post_wants_atlas_upscale()); -} - static void detour_draw_flush() { - ++g_flushCalls; - try { - log_flush_diagnostics(); - } catch (...) { - } flush_with_emissive_replay(); // The engine uploads into, and binds, whatever name its texture table // holds: give it its own atlas textures back before the next upload. @@ -581,47 +553,10 @@ static void detour_draw_flush() { // texture table until the flush is over. The upload has just marked the // texture as needing a rebind, so the first draw picks ours up. static void detour_texture_upload(int x, int y, int width, int rows, void* pixels, bool second) { - ++g_uploadCalls; g_textureUploadHook.original()(x, y, width, rows, pixels, second); if (!g_ctx || !g_drawQueueCount) return; const auto& layout = g_ctx->manifest->spriteAtlas; const auto count = reinterpret_cast(g_drawQueueCount); - // Field log: the first uploads seen while sprite upscaling wants to run, - // with everything the atlas lookup compares, so a mismatch is visible. - static int reached = 0; - if (reached < 4) { - ++reached; - try { - LOG_INFO("Texture upload hook reached (#{}, width={}, rows={}, wants upscale={})", reached, - width, rows, features::world_post_wants_atlas_upscale()); - } catch (...) { - } - } - static int logged = 0; - if (logged < 12 && features::world_post_wants_atlas_upscale()) { - ++logged; - try { - const auto word = [&](std::ptrdiff_t offset) { - std::uint32_t value = 0; - std::memcpy(&value, reinterpret_cast(count + offset), sizeof(value)); - return value; - }; - const auto pointer = [&](std::ptrdiff_t offset) { - std::uintptr_t value = 0; - std::memcpy(&value, reinterpret_cast(count + offset), sizeof(value)); - return value; - }; - LOG_INFO( - "Texture upload #{}: x={} y={} width={} rows={} second={} pixels=0x{:X} | selected " - "texture={} | atlas0: {}x{} buffer=0x{:X} texture={} | atlas1: {}x{} buffer=0x{:X} " - "texture={} | slot={}", - logged, x, y, width, rows, second, reinterpret_cast(pixels), - (word(-0x64) >> 21) & 0x1FF, word(0x80), word(0x84), pointer(0xA0), word(0xA8), - word(0xB0), word(0xB4), pointer(0xD0), word(0xD8), - game::atlas_slot_for_upload(layout, count, pixels)); - } catch (...) { - } - } if (second || x != 0 || y != 0 || !features::world_post_wants_atlas_upscale()) return; const int slot = game::atlas_slot_for_upload(layout, count, pixels); if (slot < 0 || static_cast(slot) >= g_swappedAtlases.size()) return; diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 4e89355..79f2434 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1359,6 +1359,18 @@ void test_manifest_world_post_targets() { "The 2.6.6 manifest has no world post targets"); expect_true(!manifest.patterns.particleUpdate.empty() && !manifest.patterns.particleRender.empty(), "2.7.3 should carry the particle update and render patterns"); + // The manifest is initialised positionally: a pattern in the wrong slot + // hooks the wrong function. Pin each pattern to its function's first bytes. + expect_true(manifest.patterns.renderFog.starts_with("48 89 5C 24 10 4C 89 44 24 18"), + "The RenderFog slot holds CInfinity::RenderFog's bytes"); + expect_true(manifest.patterns.drawFlush.starts_with("4C 8B DC 55 41 56 41 57 49 8D 6B D8"), + "The DrawFlush slot holds DrawFlush_GL's bytes"); + expect_true(manifest.patterns.particleUpdate.starts_with("48 89 5C 24 18 57 48 83 EC 20 8B 51 10"), + "The particle update slot holds CParticle::AsynchronousUpdate's bytes"); + expect_true(manifest.patterns.particleRender.starts_with("48 89 5C 24 18 48 89 54 24 10 55 56 57"), + "The particle render slot holds CParticle::Render's bytes"); + expect_true(manifest.patterns.textureUpload.starts_with("48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18"), + "The texture upload slot holds TexSubImage_GL's bytes"); expect_eq(manifest.referenceRvas.particleUpdate, std::uintptr_t{0x423C30}, "CParticle::AsynchronousUpdate reference RVA"); expect_eq(manifest.referenceRvas.particleRender, std::uintptr_t{0x425BE0}, From 20a93a0652113e26a9ca99867b40c3a2d231cde1 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 8 Oct 2026 01:14:31 +0200 Subject: [PATCH 52/57] Sprite smoothing: selectable filters (Catmull-Rom, EASU, and 4x combinations), cycled with F7 --- .../2026-10-07-soft-fog-and-bloom-design.md | 22 +++ src/iee/core/config.cpp | 9 ++ src/iee/core/config.h | 5 +- src/iee/features/world_post.cpp | 131 +++++++++++++----- src/iee/features/world_post.h | 8 +- src/iee/game/world_post_plan.h | 26 ++++ tests/iee_tests.cpp | 23 +++ tools/InfinityEngine-Enhancer.sample.ini | 13 +- 8 files changed, 192 insertions(+), 45 deletions(-) diff --git a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md index 719ff9b..101f40b 100644 --- a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md +++ b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md @@ -409,3 +409,25 @@ edges; the dark fringe from blending non-premultiplied alpha is unchanged (fixing it needs the blend mode swapped for alpha-blended sprites only, since additive sprites share the atlas); BAM v2 (PVRZ) sprites bypass the atlas and are not filtered. + +## 17. Revision 2026-10-08 (later): selectable sprite filters + +In-game verdict on section 16's FSR1 2x (once it actually ran; a manifest +slot mix-up had put its hook on the wrong function for three builds): "looks +like shit". The owner prefers Catmull-Rom and asked for Catmull-Rom followed +by FSR's EASU. + +The sprite pass is now a plan of one or two 2x stages over the atlas +(`game::sprite_filter_plan`), each texel-to-texel, followed by RCAS when +`SpriteSharpness > 0`: + +| `SpriteFilter` | Stages | Scale | +|---|---|---| +| 1 | EASU | 2x | +| 2 | Catmull-Rom | 2x | +| 3 (default) | Catmull-Rom, EASU | 4x | +| 4 | EASU, EASU | 4x | + +F7 cycles off and the four filters at runtime so the owner can judge them in +game instead of from offline sheets, which did not predict their in-game +opinion. A 4x atlas copy is 4096x4096 RGBA8 per stage. diff --git a/src/iee/core/config.cpp b/src/iee/core/config.cpp index b04496d..d02beab 100644 --- a/src/iee/core/config.cpp +++ b/src/iee/core/config.cpp @@ -71,6 +71,7 @@ static void normalize(EngineConfig& cfg) noexcept { cfg.lightSpill = std::clamp(cfg.lightSpill, 0.0f, 8.0f); cfg.heatShimmer = std::clamp(cfg.heatShimmer, 0.0f, 12.0f); cfg.spriteSharpness = std::clamp(cfg.spriteSharpness, 0.0f, 1.0f); + cfg.spriteFilter = std::clamp(cfg.spriteFilter, 1, 4); cfg.bloomStrength = std::clamp(cfg.bloomStrength, 0.0f, 2.0f); } @@ -130,6 +131,13 @@ static void apply_kv(EngineConfig& cfg, const std::string& section, const std::s assign_bool(cfg.spriteUpscale); else if (iequals(key, "SpriteSharpness")) assign_float(cfg.spriteSharpness); + else if (iequals(key, "SpriteFilter")) { + float filter = static_cast(cfg.spriteFilter); + assign_float(filter); + if (std::isfinite(filter)) { + cfg.spriteFilter = static_cast(std::lround(std::clamp(filter, 0.0f, 100.0f))); + } + } else if (iequals(key, "BloomStrength")) assign_float(cfg.bloomStrength); return; @@ -238,6 +246,7 @@ bool ConfigManager::save(const std::filesystem::path& path, const EngineConfig& f << "HeatShimmer = " << cfg.heatShimmer << "\n"; write_bool(f, "SpriteUpscale", cfg.spriteUpscale); f << "SpriteSharpness = " << cfg.spriteSharpness << "\n"; + f << "SpriteFilter = " << cfg.spriteFilter << "\n"; f << "BloomStrength = " << cfg.bloomStrength << "\n"; write_section(f, "Detection"); diff --git a/src/iee/core/config.h b/src/iee/core/config.h index 6171358..5a9941a 100644 --- a/src/iee/core/config.h +++ b/src/iee/core/config.h @@ -21,7 +21,10 @@ struct EngineConfig { float heatShimmer = 1.5f; // Edge-adaptive 2x upscale (FSR1) of the sprite atlas before it is drawn. bool spriteUpscale = false; - float spriteSharpness = 0.5f; // 0 = no sharpening pass effect, 1 = strongest + float spriteSharpness = 0.5f; // 0 = no sharpening pass, 1 = strongest + // 1 = FSR1 EASU 2x, 2 = Catmull-Rom 2x, 3 = Catmull-Rom 2x then EASU 2x, + // 4 = EASU 2x twice. + int spriteFilter = 3; bool dumpEngineShaders = false; bool enableDebugHotkeys = false; diff --git a/src/iee/features/world_post.cpp b/src/iee/features/world_post.cpp index df0d720..1f13c12 100644 --- a/src/iee/features/world_post.cpp +++ b/src/iee/features/world_post.cpp @@ -47,10 +47,10 @@ struct SharpenProgram { int sharpness{-1}; }; -// One sprite atlas's upscaled copies: the edge-adaptive 2x image and the -// sharpened result the engine then draws from. +// One sprite atlas's upscaled copies: up to two 2x stages, and the sharpened +// result when sharpening is on. The engine draws from the last one written. struct AtlasTargets { - Target upscaled{}; + std::array stages{}; Target sharpened{}; }; @@ -90,7 +90,7 @@ struct Settings { float fogDrift{}; float fogSmoothing{}; float heatShimmer{}; - bool spriteUpscale{}; + int spriteFilter{}; // 0 = off; see game::sprite_filter_plan float spriteSharpness{}; }; @@ -105,6 +105,7 @@ struct Resources { FogTemporalProgram fogTemporal{}; ShimmerProgram shimmer{}; unsigned spriteUpscale{}; + unsigned spriteCatmullRom{}; SharpenProgram spriteSharpen{}; std::array atlases{}; bool atlasDrawnOnce{}; @@ -353,6 +354,36 @@ void main() { } )glsl"; +// Catmull-Rom bicubic at exactly 2x, texel to texel like the pass above. It +// does not follow edges, but it keeps the sprite's own shading crisp, which +// makes it a good first stage in front of the edge-adaptive one. +constexpr const char* kSpriteCatmullRomSource = R"glsl(#version 330 +uniform sampler2D uTexture; +out vec4 fragColor; +vec4 weights(float t) { + float t2 = t * t; + float t3 = t2 * t; + return vec4(-0.5 * t3 + t2 - 0.5 * t, 1.5 * t3 - 2.5 * t2 + 1.0, + -1.5 * t3 + 2.0 * t2 + 0.5 * t, 0.5 * t3 - 0.5 * t2); +} +void main() { + ivec2 size = textureSize(uTexture, 0); + vec2 pp = gl_FragCoord.xy * 0.5 - 0.5; + vec2 fp = floor(pp); + vec4 wx = weights(pp.x - fp.x); + vec4 wy = weights(pp.y - fp.y); + ivec2 ip = ivec2(fp); + vec4 sum = vec4(0.0); + for (int j = 0; j < 4; ++j) { + for (int i = 0; i < 4; ++i) { + sum += texelFetch(uTexture, clamp(ip + ivec2(i - 1, j - 1), ivec2(0), size - 1), 0) * + (wx[i] * wy[j]); + } + } + fragColor = clamp(sum, 0.0, 1.0); +} +)glsl"; + // FSR1 RCAS (robust contrast-adaptive sharpening), same source: sharpens as // far as it can without pushing any channel outside its neighbourhood's range. constexpr const char* kSpriteSharpenSource = R"glsl(#version 330 @@ -659,8 +690,10 @@ bool create_shared_resources() noexcept { } resources.shimmer.id = build_program(kShimmerSource, kValueNoiseSource); resources.spriteUpscale = build_program(kSpriteUpscaleSource); + resources.spriteCatmullRom = build_program(kSpriteCatmullRomSource); resources.spriteSharpen.id = build_program(kSpriteSharpenSource); - if (!resources.shimmer.id || !resources.spriteUpscale || !resources.spriteSharpen.id) { + if (!resources.shimmer.id || !resources.spriteUpscale || !resources.spriteCatmullRom || + !resources.spriteSharpen.id) { return false; } resources.spriteSharpen.sharpness = @@ -827,13 +860,13 @@ void draw_bloom() noexcept { void world_post_configure(const core::EngineConfig& cfg) noexcept { g_settings = {cfg.softFogOfWar, cfg.bloom, cfg.softFogRadius, cfg.bloomStrength, cfg.lightSpill, cfg.softFogDrift, cfg.softFogSmoothing, cfg.heatShimmer, - cfg.spriteUpscale, cfg.spriteSharpness}; + cfg.spriteUpscale ? cfg.spriteFilter : 0, cfg.spriteSharpness}; } bool world_post_active() noexcept { // Sprite upscaling needs the world flushed before the UI is queued, which // is what the fog detour does when this is true. - return !g_failed && (g_settings.softFog || g_settings.bloom || g_settings.spriteUpscale); + return !g_failed && (g_settings.softFog || g_settings.bloom || g_settings.spriteFilter != 0); } void world_post_poll_hotkeys() noexcept { @@ -852,10 +885,13 @@ void world_post_poll_hotkeys() noexcept { if (!fogPressed && !bloomPressed && !spritePressed) return; if (fogPressed) g_settings.softFog = !g_settings.softFog; if (bloomPressed) g_settings.bloom = !g_settings.bloom; - if (spritePressed) g_settings.spriteUpscale = !g_settings.spriteUpscale; + if (spritePressed) { + g_settings.spriteFilter = (g_settings.spriteFilter + 1) % (game::kSpriteFilterCount + 1); + } try { - LOG_INFO("Hotkey: soft fog of war={}, bloom={}, sprite upscale={}", g_settings.softFog, - g_settings.bloom, g_settings.spriteUpscale); + LOG_INFO("Hotkey: soft fog of war={}, bloom={}, sprite filter={} ({})", g_settings.softFog, + g_settings.bloom, g_settings.spriteFilter, + game::sprite_filter_plan(g_settings.spriteFilter).name); } catch (...) { } } @@ -867,14 +903,15 @@ void world_post_on_frame() noexcept { } bool world_post_wants_atlas_upscale() noexcept { - return g_worldPass.open() && g_settings.spriteUpscale && !g_failed; + return g_worldPass.open() && g_settings.spriteFilter != 0 && !g_failed; } unsigned world_post_upscale_atlas(int slot, unsigned sourceTexture, int width, int height, int rows) noexcept { const auto& fn = gl::get_gl_functions(); - if (!fn.postProcessAvailable || slot < 0 || slot >= 2 || !sourceTexture || width <= 0 || - height <= 0 || width > 4096 || height > 4096 || rows <= 0) { + const auto plan = game::sprite_filter_plan(g_settings.spriteFilter); + if (!fn.postProcessAvailable || plan.count <= 0 || slot < 0 || slot >= 2 || !sourceTexture || + width <= 0 || height <= 0 || width > 2048 || height > 2048 || rows <= 0) { return 0; } int drawFramebuffer = 0; @@ -885,31 +922,52 @@ unsigned world_post_upscale_atlas(int slot, unsigned sourceTexture, int width, i { core::GlStateGuard textures({0}); core::GlPassGuard pass; - const game::Extent doubled{width * 2, height * 2}; bool ready = ensure_shared_resources(); if (ready) { auto& atlas = g_resources.atlases[static_cast(slot)]; - if (!(atlas.sharpened.extent == doubled)) { + // Every stage is created at its final size for this filter, once. + const auto ensure_target = [&](Target& target, game::Extent extent) { + if (target.extent == extent) return true; gl::discard_errors(); - ready = create_target(atlas.upscaled, doubled, gl::RGBA8, gl::UNSIGNED_BYTE) && - create_target(atlas.sharpened, doubled, gl::RGBA8, gl::UNSIGNED_BYTE) && - gl::check_error("sprite atlas targets"); + return create_target(target, extent, gl::RGBA8, gl::UNSIGNED_BYTE) && + gl::check_error("sprite atlas target"); + }; + set_pass_state(); + unsigned input = sourceTexture; + game::Extent size{width, height}; + int outputRows = rows; + for (int stage = 0; stage < plan.count && ready; ++stage) { + size = {size.width * 2, size.height * 2}; + // Only the rows the engine just filled, plus margin for the kernels. + outputRows = outputRows * 2 + 4 < size.height ? outputRows * 2 + 4 : size.height; + auto& target = atlas.stages[static_cast(stage)]; + ready = ensure_target(target, size); + if (!ready) break; + fn.glActiveTexture(gl::TEXTURE0); + fn.glBindFramebuffer(gl::FRAMEBUFFER, target.framebuffer); + fn.glViewport(0, 0, size.width, outputRows); + fn.glUseProgram(plan.stages[stage] == game::SpriteStage::Easu + ? g_resources.spriteUpscale + : g_resources.spriteCatmullRom); + fn.glBindTexture(gl::TEXTURE_2D, input); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); + input = target.texture; + } + if (ready && g_settings.spriteSharpness > 0.0f) { + ready = ensure_target(atlas.sharpened, size); + if (ready) { + fn.glActiveTexture(gl::TEXTURE0); + fn.glBindFramebuffer(gl::FRAMEBUFFER, atlas.sharpened.framebuffer); + fn.glViewport(0, 0, size.width, outputRows); + fn.glUseProgram(g_resources.spriteSharpen.id); + fn.glUniform1f(g_resources.spriteSharpen.sharpness, g_settings.spriteSharpness); + fn.glBindTexture(gl::TEXTURE_2D, input); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); + input = atlas.sharpened.texture; + } } if (ready) { - // Only the rows the engine just filled; two rows of margin for the kernel. - const int outputRows = rows * 2 + 4 < doubled.height ? rows * 2 + 4 : doubled.height; - set_pass_state(); - fn.glViewport(0, 0, doubled.width, outputRows); - fn.glBindFramebuffer(gl::FRAMEBUFFER, atlas.upscaled.framebuffer); - fn.glUseProgram(g_resources.spriteUpscale); - fn.glBindTexture(gl::TEXTURE_2D, sourceTexture); - fn.glDrawArrays(gl::TRIANGLES, 0, 3); - fn.glBindFramebuffer(gl::FRAMEBUFFER, atlas.sharpened.framebuffer); - fn.glUseProgram(g_resources.spriteSharpen.id); - fn.glUniform1f(g_resources.spriteSharpen.sharpness, g_settings.spriteSharpness); - fn.glBindTexture(gl::TEXTURE_2D, atlas.upscaled.texture); - fn.glDrawArrays(gl::TRIANGLES, 0, 3); - result = atlas.sharpened.texture; + result = input; ++g_atlasUpscalesSinceArea; if (!g_resources.atlasDrawnOnce) { g_resources.atlasDrawnOnce = true; @@ -918,8 +976,8 @@ unsigned world_post_upscale_atlas(int slot, unsigned sourceTexture, int width, i ready = false; } else { try { - LOG_INFO("World post: first sprite atlas upscaled ({}x{} -> {}x{}, {} rows)", width, - height, doubled.width, doubled.height, rows); + LOG_INFO("World post: first sprite atlas upscaled ({}: {}x{} -> {}x{}, {} rows)", + plan.name, width, height, size.width, size.height, rows); } catch (...) { } } @@ -1022,11 +1080,12 @@ bool world_post_before_fog(const WorldView& view) noexcept { LOG_INFO( "World post: softFog={} (radius {} world px, blur levels={} offset={:.2f}), " "bloom={} (strength {}, light spill {}, {} additive draws this frame), sprite " - "upscale={} ({} atlas passes, {} frame boundaries since the last area frame), " + "filter={} ({} atlas passes, {} frame boundaries since the last area frame), " "viewport {}x{}, {:.3f} px per world px, engine framebuffer draw={} read={}", g_settings.softFog, g_settings.fogRadius, plan.levels, plan.offset, g_settings.bloom, g_settings.bloomStrength, g_settings.lightSpill, - g_emissiveFrame == g_frame ? g_emissiveCommands : 0, g_settings.spriteUpscale, + g_emissiveFrame == g_frame ? g_emissiveCommands : 0, + game::sprite_filter_plan(g_settings.spriteFilter).name, g_atlasUpscalesSinceArea, g_frameTicksSinceArea, viewport[2], viewport[3], g_pixelsPerWorldPixel, g_engineDrawFramebuffer, g_engineReadFramebuffer); } catch (...) { diff --git a/src/iee/features/world_post.h b/src/iee/features/world_post.h index e17b5a4..d30a94f 100644 --- a/src/iee/features/world_post.h +++ b/src/iee/features/world_post.h @@ -28,7 +28,8 @@ struct WorldView { void world_post_configure(const core::EngineConfig& cfg) noexcept; // True when at least one effect is switched on and no pass has failed. bool world_post_active() noexcept; -// Debug A/B: F7 toggles sprite upscaling, F8 soft fog of war, F9 bloom. Call once per +// Debug A/B: F7 cycles the sprite filter (off and each filter), F8 toggles +// soft fog of war, F9 bloom. Call once per // frame, only when debug hotkeys are enabled. void world_post_poll_hotkeys() noexcept; // Frame boundary: the next world pass starts. @@ -48,8 +49,9 @@ bool world_post_begin_emissive() noexcept; void world_post_end_emissive(int commands) noexcept; // Sprite smoothing. The engine composites sprites on the CPU into a streaming // atlas and uploads it at the start of a flush. Right after that upload, in -// the world pass, the atlas is upscaled 2x with FSR1 (edge-adaptive upsample, -// then sharpen); the caller makes the engine draw from the returned texture +// the world pass, the atlas is upscaled 2x or 4x by the configured filter +// (game::sprite_filter_plan) and optionally sharpened; the caller makes the +// engine draw from the returned texture // for the rest of that flush and puts the engine's own back afterwards. // Normalised texture coordinates are unchanged by the larger texture. bool world_post_wants_atlas_upscale() noexcept; diff --git a/src/iee/game/world_post_plan.h b/src/iee/game/world_post_plan.h index b642cc7..0a0b2c1 100644 --- a/src/iee/game/world_post_plan.h +++ b/src/iee/game/world_post_plan.h @@ -52,6 +52,32 @@ class WorldPassGate { bool closedForFrame_{}; }; +// Sprite smoothing filters: one or two 2x upscale passes over the sprite atlas. +enum class SpriteStage { Easu, CatmullRom }; +inline constexpr int kSpriteFilterCount = 4; +struct SpriteFilterPlan { + int count{}; + SpriteStage stages[2]{}; + const char* name{"off"}; + [[nodiscard]] constexpr int scale() const noexcept { return 1 << count; } +}; +// 1 = FSR1 EASU 2x, 2 = Catmull-Rom 2x, 3 = Catmull-Rom 2x then EASU 2x, +// 4 = EASU 2x twice. Anything else is off. +constexpr SpriteFilterPlan sprite_filter_plan(int filter) noexcept { + switch (filter) { + case 1: + return {1, {SpriteStage::Easu, SpriteStage::Easu}, "FSR1 EASU 2x"}; + case 2: + return {1, {SpriteStage::CatmullRom, SpriteStage::CatmullRom}, "Catmull-Rom 2x"}; + case 3: + return {2, {SpriteStage::CatmullRom, SpriteStage::Easu}, "Catmull-Rom 2x then EASU 2x"}; + case 4: + return {2, {SpriteStage::Easu, SpriteStage::Easu}, "EASU 2x twice"}; + default: + return {}; + } +} + // How far to move the shown image towards the new one after `stepSeconds`, // for an exponential approach with time constant `smoothingSeconds`. 1 means // "show the new image": no smoothing, or a history too old to trust. diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 79f2434..9b2a3b8 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1278,6 +1278,7 @@ void test_config_world_post() { expect_eq(defaults.heatShimmer, 1.5f, "Heat shimmer default"); expect_true(!defaults.spriteUpscale, "Sprite upscaling defaults off"); expect_eq(defaults.spriteSharpness, 0.5f, "Sprite sharpness default"); + expect_eq(defaults.spriteFilter, 3, "Sprite filter defaults to Catmull-Rom then EASU"); expect_eq(defaults.bloomStrength, 0.35f, "Bloom strength default"); const auto tempPath = @@ -1294,6 +1295,7 @@ void test_config_world_post() { out << "HeatShimmer = 4\n"; out << "SpriteUpscale = true\n"; out << "SpriteSharpness = 0.8\n"; + out << "SpriteFilter = 2\n"; out << "BloomStrength = 0.5\n"; } iee::core::EngineConfig cfg{}; @@ -1306,6 +1308,7 @@ void test_config_world_post() { expect_eq(cfg.heatShimmer, 4.0f, "Heat shimmer should parse"); expect_true(cfg.spriteUpscale, "Sprite upscaling should parse"); expect_eq(cfg.spriteSharpness, 0.8f, "Sprite sharpness should parse"); + expect_eq(cfg.spriteFilter, 2, "Sprite filter should parse"); expect_eq(cfg.bloomStrength, 0.5f, "Bloom strength should parse"); { @@ -1317,6 +1320,7 @@ void test_config_world_post() { out << "SoftFogSmoothing = 99\n"; out << "HeatShimmer = 500\n"; out << "SpriteSharpness = 9\n"; + out << "SpriteFilter = 40\n"; out << "BloomStrength = -3\n"; } cfg = {}; @@ -1327,6 +1331,7 @@ void test_config_world_post() { expect_eq(cfg.softFogSmoothing, 2.0f, "Soft fog smoothing is clamped to its maximum"); expect_eq(cfg.heatShimmer, 12.0f, "Heat shimmer is clamped to its maximum"); expect_eq(cfg.spriteSharpness, 1.0f, "Sprite sharpness is clamped to 1"); + expect_eq(cfg.spriteFilter, 4, "Sprite filter is clamped to the last mode"); expect_eq(cfg.bloomStrength, 0.0f, "Bloom strength is clamped to 0"); { @@ -1617,6 +1622,24 @@ void test_world_post_plan() { expect_eq(bloom_levels({960, 540}), 5, "Bloom uses at most five levels"); expect_eq(bloom_levels({40, 40}), 2, "Bloom levels shrink with the image"); + expect_eq(sprite_filter_plan(0).count, 0, "Filter 0 is off"); + expect_eq(sprite_filter_plan(1).count, 1, "Filter 1 is one EASU pass"); + expect_true(sprite_filter_plan(1).stages[0] == SpriteStage::Easu, "Filter 1 uses EASU"); + expect_true(sprite_filter_plan(2).count == 1 && + sprite_filter_plan(2).stages[0] == SpriteStage::CatmullRom, + "Filter 2 is one Catmull-Rom pass"); + expect_true(sprite_filter_plan(3).count == 2 && + sprite_filter_plan(3).stages[0] == SpriteStage::CatmullRom && + sprite_filter_plan(3).stages[1] == SpriteStage::Easu, + "Filter 3 is Catmull-Rom then EASU"); + expect_true(sprite_filter_plan(4).count == 2 && sprite_filter_plan(4).stages[0] == SpriteStage::Easu && + sprite_filter_plan(4).stages[1] == SpriteStage::Easu, + "Filter 4 is EASU twice"); + expect_eq(sprite_filter_plan(99).count, 0, "An unknown filter is off"); + expect_eq(sprite_filter_plan(-1).count, 0, "A negative filter is off"); + expect_eq(sprite_filter_plan(3).scale(), 4, "Two passes make 4x"); + expect_eq(sprite_filter_plan(2).scale(), 2, "One pass makes 2x"); + expect_eq(temporal_blend(0.1f, 0.0f), 1.0f, "No smoothing time means the new image is shown at once"); expect_eq(temporal_blend(0.0f, 0.25f), 0.0f, "No elapsed time keeps the shown image"); expect_true(std::abs(temporal_blend(0.25f, 0.25f) - 0.6321f) < 0.001f, diff --git a/tools/InfinityEngine-Enhancer.sample.ini b/tools/InfinityEngine-Enhancer.sample.ini index f10baa3..616a48c 100644 --- a/tools/InfinityEngine-Enhancer.sample.ini +++ b/tools/InfinityEngine-Enhancer.sample.ini @@ -38,12 +38,15 @@ LightSpill = 1.5 ; Heat haze above fires and other warm light sources: how far (world pixels) ; the air ripples; 0 = off. Needs Bloom. HeatShimmer = 1.5 -; Smoother creature and effect sprites: an edge-adaptive 2x upscale (FSR1) of -; the engine's sprite sheet before it is drawn in the world view. It rounds off -; stair-stepped outlines; it cannot add detail. SpriteSharpness (0..1) is the -; strength of FSR1's sharpening pass. BGEE 2.7.3 only. With -; EnableDebugHotkeys, F7 toggles it. +; Smoother creature and effect sprites: the engine's sprite sheet is upscaled +; before it is drawn in the world view. It rounds off stair-stepped outlines; +; it cannot add detail. SpriteFilter: 1 = FSR1 EASU 2x, 2 = Catmull-Rom 2x, +; 3 = Catmull-Rom 2x then EASU 2x (4x), 4 = EASU 2x twice (4x). +; SpriteSharpness (0..1) is the strength of FSR1's sharpening pass, run last; +; 0 skips it. BGEE 2.7.3 only. With EnableDebugHotkeys, F7 cycles off and the +; four filters. SpriteUpscale = false +SpriteFilter = 3 SpriteSharpness = 0.5 [Detection] From e2c7fb92ab967a8ba45a8cf3224d7631ce923a8e Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 8 Oct 2026 01:21:56 +0200 Subject: [PATCH 53/57] Revert "Sprite smoothing: selectable filters (Catmull-Rom, EASU, and 4x combinations), cycled with F7" This reverts commit 20a93a0652113e26a9ca99867b40c3a2d231cde1. --- .../2026-10-07-soft-fog-and-bloom-design.md | 22 --- src/iee/core/config.cpp | 9 -- src/iee/core/config.h | 5 +- src/iee/features/world_post.cpp | 131 +++++------------- src/iee/features/world_post.h | 8 +- src/iee/game/world_post_plan.h | 26 ---- tests/iee_tests.cpp | 23 --- tools/InfinityEngine-Enhancer.sample.ini | 13 +- 8 files changed, 45 insertions(+), 192 deletions(-) diff --git a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md index 101f40b..719ff9b 100644 --- a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md +++ b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md @@ -409,25 +409,3 @@ edges; the dark fringe from blending non-premultiplied alpha is unchanged (fixing it needs the blend mode swapped for alpha-blended sprites only, since additive sprites share the atlas); BAM v2 (PVRZ) sprites bypass the atlas and are not filtered. - -## 17. Revision 2026-10-08 (later): selectable sprite filters - -In-game verdict on section 16's FSR1 2x (once it actually ran; a manifest -slot mix-up had put its hook on the wrong function for three builds): "looks -like shit". The owner prefers Catmull-Rom and asked for Catmull-Rom followed -by FSR's EASU. - -The sprite pass is now a plan of one or two 2x stages over the atlas -(`game::sprite_filter_plan`), each texel-to-texel, followed by RCAS when -`SpriteSharpness > 0`: - -| `SpriteFilter` | Stages | Scale | -|---|---|---| -| 1 | EASU | 2x | -| 2 | Catmull-Rom | 2x | -| 3 (default) | Catmull-Rom, EASU | 4x | -| 4 | EASU, EASU | 4x | - -F7 cycles off and the four filters at runtime so the owner can judge them in -game instead of from offline sheets, which did not predict their in-game -opinion. A 4x atlas copy is 4096x4096 RGBA8 per stage. diff --git a/src/iee/core/config.cpp b/src/iee/core/config.cpp index d02beab..b04496d 100644 --- a/src/iee/core/config.cpp +++ b/src/iee/core/config.cpp @@ -71,7 +71,6 @@ static void normalize(EngineConfig& cfg) noexcept { cfg.lightSpill = std::clamp(cfg.lightSpill, 0.0f, 8.0f); cfg.heatShimmer = std::clamp(cfg.heatShimmer, 0.0f, 12.0f); cfg.spriteSharpness = std::clamp(cfg.spriteSharpness, 0.0f, 1.0f); - cfg.spriteFilter = std::clamp(cfg.spriteFilter, 1, 4); cfg.bloomStrength = std::clamp(cfg.bloomStrength, 0.0f, 2.0f); } @@ -131,13 +130,6 @@ static void apply_kv(EngineConfig& cfg, const std::string& section, const std::s assign_bool(cfg.spriteUpscale); else if (iequals(key, "SpriteSharpness")) assign_float(cfg.spriteSharpness); - else if (iequals(key, "SpriteFilter")) { - float filter = static_cast(cfg.spriteFilter); - assign_float(filter); - if (std::isfinite(filter)) { - cfg.spriteFilter = static_cast(std::lround(std::clamp(filter, 0.0f, 100.0f))); - } - } else if (iequals(key, "BloomStrength")) assign_float(cfg.bloomStrength); return; @@ -246,7 +238,6 @@ bool ConfigManager::save(const std::filesystem::path& path, const EngineConfig& f << "HeatShimmer = " << cfg.heatShimmer << "\n"; write_bool(f, "SpriteUpscale", cfg.spriteUpscale); f << "SpriteSharpness = " << cfg.spriteSharpness << "\n"; - f << "SpriteFilter = " << cfg.spriteFilter << "\n"; f << "BloomStrength = " << cfg.bloomStrength << "\n"; write_section(f, "Detection"); diff --git a/src/iee/core/config.h b/src/iee/core/config.h index 5a9941a..6171358 100644 --- a/src/iee/core/config.h +++ b/src/iee/core/config.h @@ -21,10 +21,7 @@ struct EngineConfig { float heatShimmer = 1.5f; // Edge-adaptive 2x upscale (FSR1) of the sprite atlas before it is drawn. bool spriteUpscale = false; - float spriteSharpness = 0.5f; // 0 = no sharpening pass, 1 = strongest - // 1 = FSR1 EASU 2x, 2 = Catmull-Rom 2x, 3 = Catmull-Rom 2x then EASU 2x, - // 4 = EASU 2x twice. - int spriteFilter = 3; + float spriteSharpness = 0.5f; // 0 = no sharpening pass effect, 1 = strongest bool dumpEngineShaders = false; bool enableDebugHotkeys = false; diff --git a/src/iee/features/world_post.cpp b/src/iee/features/world_post.cpp index 1f13c12..df0d720 100644 --- a/src/iee/features/world_post.cpp +++ b/src/iee/features/world_post.cpp @@ -47,10 +47,10 @@ struct SharpenProgram { int sharpness{-1}; }; -// One sprite atlas's upscaled copies: up to two 2x stages, and the sharpened -// result when sharpening is on. The engine draws from the last one written. +// One sprite atlas's upscaled copies: the edge-adaptive 2x image and the +// sharpened result the engine then draws from. struct AtlasTargets { - std::array stages{}; + Target upscaled{}; Target sharpened{}; }; @@ -90,7 +90,7 @@ struct Settings { float fogDrift{}; float fogSmoothing{}; float heatShimmer{}; - int spriteFilter{}; // 0 = off; see game::sprite_filter_plan + bool spriteUpscale{}; float spriteSharpness{}; }; @@ -105,7 +105,6 @@ struct Resources { FogTemporalProgram fogTemporal{}; ShimmerProgram shimmer{}; unsigned spriteUpscale{}; - unsigned spriteCatmullRom{}; SharpenProgram spriteSharpen{}; std::array atlases{}; bool atlasDrawnOnce{}; @@ -354,36 +353,6 @@ void main() { } )glsl"; -// Catmull-Rom bicubic at exactly 2x, texel to texel like the pass above. It -// does not follow edges, but it keeps the sprite's own shading crisp, which -// makes it a good first stage in front of the edge-adaptive one. -constexpr const char* kSpriteCatmullRomSource = R"glsl(#version 330 -uniform sampler2D uTexture; -out vec4 fragColor; -vec4 weights(float t) { - float t2 = t * t; - float t3 = t2 * t; - return vec4(-0.5 * t3 + t2 - 0.5 * t, 1.5 * t3 - 2.5 * t2 + 1.0, - -1.5 * t3 + 2.0 * t2 + 0.5 * t, 0.5 * t3 - 0.5 * t2); -} -void main() { - ivec2 size = textureSize(uTexture, 0); - vec2 pp = gl_FragCoord.xy * 0.5 - 0.5; - vec2 fp = floor(pp); - vec4 wx = weights(pp.x - fp.x); - vec4 wy = weights(pp.y - fp.y); - ivec2 ip = ivec2(fp); - vec4 sum = vec4(0.0); - for (int j = 0; j < 4; ++j) { - for (int i = 0; i < 4; ++i) { - sum += texelFetch(uTexture, clamp(ip + ivec2(i - 1, j - 1), ivec2(0), size - 1), 0) * - (wx[i] * wy[j]); - } - } - fragColor = clamp(sum, 0.0, 1.0); -} -)glsl"; - // FSR1 RCAS (robust contrast-adaptive sharpening), same source: sharpens as // far as it can without pushing any channel outside its neighbourhood's range. constexpr const char* kSpriteSharpenSource = R"glsl(#version 330 @@ -690,10 +659,8 @@ bool create_shared_resources() noexcept { } resources.shimmer.id = build_program(kShimmerSource, kValueNoiseSource); resources.spriteUpscale = build_program(kSpriteUpscaleSource); - resources.spriteCatmullRom = build_program(kSpriteCatmullRomSource); resources.spriteSharpen.id = build_program(kSpriteSharpenSource); - if (!resources.shimmer.id || !resources.spriteUpscale || !resources.spriteCatmullRom || - !resources.spriteSharpen.id) { + if (!resources.shimmer.id || !resources.spriteUpscale || !resources.spriteSharpen.id) { return false; } resources.spriteSharpen.sharpness = @@ -860,13 +827,13 @@ void draw_bloom() noexcept { void world_post_configure(const core::EngineConfig& cfg) noexcept { g_settings = {cfg.softFogOfWar, cfg.bloom, cfg.softFogRadius, cfg.bloomStrength, cfg.lightSpill, cfg.softFogDrift, cfg.softFogSmoothing, cfg.heatShimmer, - cfg.spriteUpscale ? cfg.spriteFilter : 0, cfg.spriteSharpness}; + cfg.spriteUpscale, cfg.spriteSharpness}; } bool world_post_active() noexcept { // Sprite upscaling needs the world flushed before the UI is queued, which // is what the fog detour does when this is true. - return !g_failed && (g_settings.softFog || g_settings.bloom || g_settings.spriteFilter != 0); + return !g_failed && (g_settings.softFog || g_settings.bloom || g_settings.spriteUpscale); } void world_post_poll_hotkeys() noexcept { @@ -885,13 +852,10 @@ void world_post_poll_hotkeys() noexcept { if (!fogPressed && !bloomPressed && !spritePressed) return; if (fogPressed) g_settings.softFog = !g_settings.softFog; if (bloomPressed) g_settings.bloom = !g_settings.bloom; - if (spritePressed) { - g_settings.spriteFilter = (g_settings.spriteFilter + 1) % (game::kSpriteFilterCount + 1); - } + if (spritePressed) g_settings.spriteUpscale = !g_settings.spriteUpscale; try { - LOG_INFO("Hotkey: soft fog of war={}, bloom={}, sprite filter={} ({})", g_settings.softFog, - g_settings.bloom, g_settings.spriteFilter, - game::sprite_filter_plan(g_settings.spriteFilter).name); + LOG_INFO("Hotkey: soft fog of war={}, bloom={}, sprite upscale={}", g_settings.softFog, + g_settings.bloom, g_settings.spriteUpscale); } catch (...) { } } @@ -903,15 +867,14 @@ void world_post_on_frame() noexcept { } bool world_post_wants_atlas_upscale() noexcept { - return g_worldPass.open() && g_settings.spriteFilter != 0 && !g_failed; + return g_worldPass.open() && g_settings.spriteUpscale && !g_failed; } unsigned world_post_upscale_atlas(int slot, unsigned sourceTexture, int width, int height, int rows) noexcept { const auto& fn = gl::get_gl_functions(); - const auto plan = game::sprite_filter_plan(g_settings.spriteFilter); - if (!fn.postProcessAvailable || plan.count <= 0 || slot < 0 || slot >= 2 || !sourceTexture || - width <= 0 || height <= 0 || width > 2048 || height > 2048 || rows <= 0) { + if (!fn.postProcessAvailable || slot < 0 || slot >= 2 || !sourceTexture || width <= 0 || + height <= 0 || width > 4096 || height > 4096 || rows <= 0) { return 0; } int drawFramebuffer = 0; @@ -922,52 +885,31 @@ unsigned world_post_upscale_atlas(int slot, unsigned sourceTexture, int width, i { core::GlStateGuard textures({0}); core::GlPassGuard pass; + const game::Extent doubled{width * 2, height * 2}; bool ready = ensure_shared_resources(); if (ready) { auto& atlas = g_resources.atlases[static_cast(slot)]; - // Every stage is created at its final size for this filter, once. - const auto ensure_target = [&](Target& target, game::Extent extent) { - if (target.extent == extent) return true; + if (!(atlas.sharpened.extent == doubled)) { gl::discard_errors(); - return create_target(target, extent, gl::RGBA8, gl::UNSIGNED_BYTE) && - gl::check_error("sprite atlas target"); - }; - set_pass_state(); - unsigned input = sourceTexture; - game::Extent size{width, height}; - int outputRows = rows; - for (int stage = 0; stage < plan.count && ready; ++stage) { - size = {size.width * 2, size.height * 2}; - // Only the rows the engine just filled, plus margin for the kernels. - outputRows = outputRows * 2 + 4 < size.height ? outputRows * 2 + 4 : size.height; - auto& target = atlas.stages[static_cast(stage)]; - ready = ensure_target(target, size); - if (!ready) break; - fn.glActiveTexture(gl::TEXTURE0); - fn.glBindFramebuffer(gl::FRAMEBUFFER, target.framebuffer); - fn.glViewport(0, 0, size.width, outputRows); - fn.glUseProgram(plan.stages[stage] == game::SpriteStage::Easu - ? g_resources.spriteUpscale - : g_resources.spriteCatmullRom); - fn.glBindTexture(gl::TEXTURE_2D, input); - fn.glDrawArrays(gl::TRIANGLES, 0, 3); - input = target.texture; - } - if (ready && g_settings.spriteSharpness > 0.0f) { - ready = ensure_target(atlas.sharpened, size); - if (ready) { - fn.glActiveTexture(gl::TEXTURE0); - fn.glBindFramebuffer(gl::FRAMEBUFFER, atlas.sharpened.framebuffer); - fn.glViewport(0, 0, size.width, outputRows); - fn.glUseProgram(g_resources.spriteSharpen.id); - fn.glUniform1f(g_resources.spriteSharpen.sharpness, g_settings.spriteSharpness); - fn.glBindTexture(gl::TEXTURE_2D, input); - fn.glDrawArrays(gl::TRIANGLES, 0, 3); - input = atlas.sharpened.texture; - } + ready = create_target(atlas.upscaled, doubled, gl::RGBA8, gl::UNSIGNED_BYTE) && + create_target(atlas.sharpened, doubled, gl::RGBA8, gl::UNSIGNED_BYTE) && + gl::check_error("sprite atlas targets"); } if (ready) { - result = input; + // Only the rows the engine just filled; two rows of margin for the kernel. + const int outputRows = rows * 2 + 4 < doubled.height ? rows * 2 + 4 : doubled.height; + set_pass_state(); + fn.glViewport(0, 0, doubled.width, outputRows); + fn.glBindFramebuffer(gl::FRAMEBUFFER, atlas.upscaled.framebuffer); + fn.glUseProgram(g_resources.spriteUpscale); + fn.glBindTexture(gl::TEXTURE_2D, sourceTexture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); + fn.glBindFramebuffer(gl::FRAMEBUFFER, atlas.sharpened.framebuffer); + fn.glUseProgram(g_resources.spriteSharpen.id); + fn.glUniform1f(g_resources.spriteSharpen.sharpness, g_settings.spriteSharpness); + fn.glBindTexture(gl::TEXTURE_2D, atlas.upscaled.texture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); + result = atlas.sharpened.texture; ++g_atlasUpscalesSinceArea; if (!g_resources.atlasDrawnOnce) { g_resources.atlasDrawnOnce = true; @@ -976,8 +918,8 @@ unsigned world_post_upscale_atlas(int slot, unsigned sourceTexture, int width, i ready = false; } else { try { - LOG_INFO("World post: first sprite atlas upscaled ({}: {}x{} -> {}x{}, {} rows)", - plan.name, width, height, size.width, size.height, rows); + LOG_INFO("World post: first sprite atlas upscaled ({}x{} -> {}x{}, {} rows)", width, + height, doubled.width, doubled.height, rows); } catch (...) { } } @@ -1080,12 +1022,11 @@ bool world_post_before_fog(const WorldView& view) noexcept { LOG_INFO( "World post: softFog={} (radius {} world px, blur levels={} offset={:.2f}), " "bloom={} (strength {}, light spill {}, {} additive draws this frame), sprite " - "filter={} ({} atlas passes, {} frame boundaries since the last area frame), " + "upscale={} ({} atlas passes, {} frame boundaries since the last area frame), " "viewport {}x{}, {:.3f} px per world px, engine framebuffer draw={} read={}", g_settings.softFog, g_settings.fogRadius, plan.levels, plan.offset, g_settings.bloom, g_settings.bloomStrength, g_settings.lightSpill, - g_emissiveFrame == g_frame ? g_emissiveCommands : 0, - game::sprite_filter_plan(g_settings.spriteFilter).name, + g_emissiveFrame == g_frame ? g_emissiveCommands : 0, g_settings.spriteUpscale, g_atlasUpscalesSinceArea, g_frameTicksSinceArea, viewport[2], viewport[3], g_pixelsPerWorldPixel, g_engineDrawFramebuffer, g_engineReadFramebuffer); } catch (...) { diff --git a/src/iee/features/world_post.h b/src/iee/features/world_post.h index d30a94f..e17b5a4 100644 --- a/src/iee/features/world_post.h +++ b/src/iee/features/world_post.h @@ -28,8 +28,7 @@ struct WorldView { void world_post_configure(const core::EngineConfig& cfg) noexcept; // True when at least one effect is switched on and no pass has failed. bool world_post_active() noexcept; -// Debug A/B: F7 cycles the sprite filter (off and each filter), F8 toggles -// soft fog of war, F9 bloom. Call once per +// Debug A/B: F7 toggles sprite upscaling, F8 soft fog of war, F9 bloom. Call once per // frame, only when debug hotkeys are enabled. void world_post_poll_hotkeys() noexcept; // Frame boundary: the next world pass starts. @@ -49,9 +48,8 @@ bool world_post_begin_emissive() noexcept; void world_post_end_emissive(int commands) noexcept; // Sprite smoothing. The engine composites sprites on the CPU into a streaming // atlas and uploads it at the start of a flush. Right after that upload, in -// the world pass, the atlas is upscaled 2x or 4x by the configured filter -// (game::sprite_filter_plan) and optionally sharpened; the caller makes the -// engine draw from the returned texture +// the world pass, the atlas is upscaled 2x with FSR1 (edge-adaptive upsample, +// then sharpen); the caller makes the engine draw from the returned texture // for the rest of that flush and puts the engine's own back afterwards. // Normalised texture coordinates are unchanged by the larger texture. bool world_post_wants_atlas_upscale() noexcept; diff --git a/src/iee/game/world_post_plan.h b/src/iee/game/world_post_plan.h index 0a0b2c1..b642cc7 100644 --- a/src/iee/game/world_post_plan.h +++ b/src/iee/game/world_post_plan.h @@ -52,32 +52,6 @@ class WorldPassGate { bool closedForFrame_{}; }; -// Sprite smoothing filters: one or two 2x upscale passes over the sprite atlas. -enum class SpriteStage { Easu, CatmullRom }; -inline constexpr int kSpriteFilterCount = 4; -struct SpriteFilterPlan { - int count{}; - SpriteStage stages[2]{}; - const char* name{"off"}; - [[nodiscard]] constexpr int scale() const noexcept { return 1 << count; } -}; -// 1 = FSR1 EASU 2x, 2 = Catmull-Rom 2x, 3 = Catmull-Rom 2x then EASU 2x, -// 4 = EASU 2x twice. Anything else is off. -constexpr SpriteFilterPlan sprite_filter_plan(int filter) noexcept { - switch (filter) { - case 1: - return {1, {SpriteStage::Easu, SpriteStage::Easu}, "FSR1 EASU 2x"}; - case 2: - return {1, {SpriteStage::CatmullRom, SpriteStage::CatmullRom}, "Catmull-Rom 2x"}; - case 3: - return {2, {SpriteStage::CatmullRom, SpriteStage::Easu}, "Catmull-Rom 2x then EASU 2x"}; - case 4: - return {2, {SpriteStage::Easu, SpriteStage::Easu}, "EASU 2x twice"}; - default: - return {}; - } -} - // How far to move the shown image towards the new one after `stepSeconds`, // for an exponential approach with time constant `smoothingSeconds`. 1 means // "show the new image": no smoothing, or a history too old to trust. diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 9b2a3b8..79f2434 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1278,7 +1278,6 @@ void test_config_world_post() { expect_eq(defaults.heatShimmer, 1.5f, "Heat shimmer default"); expect_true(!defaults.spriteUpscale, "Sprite upscaling defaults off"); expect_eq(defaults.spriteSharpness, 0.5f, "Sprite sharpness default"); - expect_eq(defaults.spriteFilter, 3, "Sprite filter defaults to Catmull-Rom then EASU"); expect_eq(defaults.bloomStrength, 0.35f, "Bloom strength default"); const auto tempPath = @@ -1295,7 +1294,6 @@ void test_config_world_post() { out << "HeatShimmer = 4\n"; out << "SpriteUpscale = true\n"; out << "SpriteSharpness = 0.8\n"; - out << "SpriteFilter = 2\n"; out << "BloomStrength = 0.5\n"; } iee::core::EngineConfig cfg{}; @@ -1308,7 +1306,6 @@ void test_config_world_post() { expect_eq(cfg.heatShimmer, 4.0f, "Heat shimmer should parse"); expect_true(cfg.spriteUpscale, "Sprite upscaling should parse"); expect_eq(cfg.spriteSharpness, 0.8f, "Sprite sharpness should parse"); - expect_eq(cfg.spriteFilter, 2, "Sprite filter should parse"); expect_eq(cfg.bloomStrength, 0.5f, "Bloom strength should parse"); { @@ -1320,7 +1317,6 @@ void test_config_world_post() { out << "SoftFogSmoothing = 99\n"; out << "HeatShimmer = 500\n"; out << "SpriteSharpness = 9\n"; - out << "SpriteFilter = 40\n"; out << "BloomStrength = -3\n"; } cfg = {}; @@ -1331,7 +1327,6 @@ void test_config_world_post() { expect_eq(cfg.softFogSmoothing, 2.0f, "Soft fog smoothing is clamped to its maximum"); expect_eq(cfg.heatShimmer, 12.0f, "Heat shimmer is clamped to its maximum"); expect_eq(cfg.spriteSharpness, 1.0f, "Sprite sharpness is clamped to 1"); - expect_eq(cfg.spriteFilter, 4, "Sprite filter is clamped to the last mode"); expect_eq(cfg.bloomStrength, 0.0f, "Bloom strength is clamped to 0"); { @@ -1622,24 +1617,6 @@ void test_world_post_plan() { expect_eq(bloom_levels({960, 540}), 5, "Bloom uses at most five levels"); expect_eq(bloom_levels({40, 40}), 2, "Bloom levels shrink with the image"); - expect_eq(sprite_filter_plan(0).count, 0, "Filter 0 is off"); - expect_eq(sprite_filter_plan(1).count, 1, "Filter 1 is one EASU pass"); - expect_true(sprite_filter_plan(1).stages[0] == SpriteStage::Easu, "Filter 1 uses EASU"); - expect_true(sprite_filter_plan(2).count == 1 && - sprite_filter_plan(2).stages[0] == SpriteStage::CatmullRom, - "Filter 2 is one Catmull-Rom pass"); - expect_true(sprite_filter_plan(3).count == 2 && - sprite_filter_plan(3).stages[0] == SpriteStage::CatmullRom && - sprite_filter_plan(3).stages[1] == SpriteStage::Easu, - "Filter 3 is Catmull-Rom then EASU"); - expect_true(sprite_filter_plan(4).count == 2 && sprite_filter_plan(4).stages[0] == SpriteStage::Easu && - sprite_filter_plan(4).stages[1] == SpriteStage::Easu, - "Filter 4 is EASU twice"); - expect_eq(sprite_filter_plan(99).count, 0, "An unknown filter is off"); - expect_eq(sprite_filter_plan(-1).count, 0, "A negative filter is off"); - expect_eq(sprite_filter_plan(3).scale(), 4, "Two passes make 4x"); - expect_eq(sprite_filter_plan(2).scale(), 2, "One pass makes 2x"); - expect_eq(temporal_blend(0.1f, 0.0f), 1.0f, "No smoothing time means the new image is shown at once"); expect_eq(temporal_blend(0.0f, 0.25f), 0.0f, "No elapsed time keeps the shown image"); expect_true(std::abs(temporal_blend(0.25f, 0.25f) - 0.6321f) < 0.001f, diff --git a/tools/InfinityEngine-Enhancer.sample.ini b/tools/InfinityEngine-Enhancer.sample.ini index 616a48c..f10baa3 100644 --- a/tools/InfinityEngine-Enhancer.sample.ini +++ b/tools/InfinityEngine-Enhancer.sample.ini @@ -38,15 +38,12 @@ LightSpill = 1.5 ; Heat haze above fires and other warm light sources: how far (world pixels) ; the air ripples; 0 = off. Needs Bloom. HeatShimmer = 1.5 -; Smoother creature and effect sprites: the engine's sprite sheet is upscaled -; before it is drawn in the world view. It rounds off stair-stepped outlines; -; it cannot add detail. SpriteFilter: 1 = FSR1 EASU 2x, 2 = Catmull-Rom 2x, -; 3 = Catmull-Rom 2x then EASU 2x (4x), 4 = EASU 2x twice (4x). -; SpriteSharpness (0..1) is the strength of FSR1's sharpening pass, run last; -; 0 skips it. BGEE 2.7.3 only. With EnableDebugHotkeys, F7 cycles off and the -; four filters. +; Smoother creature and effect sprites: an edge-adaptive 2x upscale (FSR1) of +; the engine's sprite sheet before it is drawn in the world view. It rounds off +; stair-stepped outlines; it cannot add detail. SpriteSharpness (0..1) is the +; strength of FSR1's sharpening pass. BGEE 2.7.3 only. With +; EnableDebugHotkeys, F7 toggles it. SpriteUpscale = false -SpriteFilter = 3 SpriteSharpness = 0.5 [Detection] From 2b61ad45803053a55412e9f67eb8cb596de86b01 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 8 Oct 2026 01:22:24 +0200 Subject: [PATCH 54/57] docs: record the sprite filter comparison and the owner's choice of FSR1 --- .../specs/2026-10-07-soft-fog-and-bloom-design.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md index 719ff9b..80f291d 100644 --- a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md +++ b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md @@ -409,3 +409,15 @@ edges; the dark fringe from blending non-premultiplied alpha is unchanged (fixing it needs the blend mode swapped for alpha-blended sprites only, since additive sprites share the atlas); BAM v2 (PVRZ) sprites bypass the atlas and are not filtered. + +## 17. Revision 2026-10-08 (later): other sprite filters tried and removed + +After FSR1 2x first ran in game the owner disliked it and asked for +Catmull-Rom followed by EASU. A build with four filters cycled by F7 (EASU 2x, +Catmull-Rom 2x, Catmull-Rom then EASU at 4x, EASU twice at 4x) let them compare +in game. Verdict: FSR1 2x (EASU then RCAS) is the one they prefer; the other +three are "pure shite". That build was reverted, so the sprite pass is section +16's again: one EASU stage and RCAS, toggled with F7. + +Lesson for this owner: offline comparison sheets did not predict their in-game +opinion in either direction. Give them a runtime toggle and let them judge. From a16cab93b9efd50957d1f72d954b581d88599a69 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 8 Oct 2026 19:58:16 +0200 Subject: [PATCH 55/57] Sprite smoothing: keep the atlas corner untextured draws sample (selection circles vanished) --- .../2026-10-07-soft-fog-and-bloom-design.md | 22 ++++++++++ src/iee/features/world_post.cpp | 40 ++++++++++++------- src/iee/game/world_post_plan.cpp | 9 +++++ src/iee/game/world_post_plan.h | 14 +++++++ tests/iee_tests.cpp | 5 +++ 5 files changed, 75 insertions(+), 15 deletions(-) diff --git a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md index 80f291d..1e7780a 100644 --- a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md +++ b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md @@ -421,3 +421,25 @@ three are "pure shite". That build was reverted, so the sprite pass is section Lesson for this owner: offline comparison sheets did not predict their in-game opinion in either direction. Give them a runtime toggle and let them judge. + +## 18. Revision 2026-10-08 (later): selection circles vanishing with sprite smoothing + +Owner report: with `SpriteUpscale` on, selection circles sometimes disappear. + +Cause (2.7.3, decompiled): the engine has no untextured draw. +`DrawDisable_GL(DRAW_TEXTURE_2D)` (`0x42B050`) selects atlas 0's texture +(`fx[0]`) and sets the texture coordinate to the atlas's size, i.e. (1, 1). +`DrawInit_GL` uploads a 2x2 white block once at `(texW-2, texH-2)` of that +atlas, so selection circles (`CVidMode::DrawEllipseArc`), health bars and every +other plain-coloured shape are white texels times the vertex colour. The 2x +copy only redrew the rows the engine had just uploaded, so its last corner was +never filled and those shapes came out transparent in every flush that swapped +atlas 0. + +Fix: after the uploaded rows, the same two passes redraw the corner of atlas +0's copy (`game::untextured_corner`). The log now names each atlas slot the +first time it is upscaled. + +Rule for any later change to the atlas swap: whatever replaces atlas 0 must +keep its last corner white. Derived from the decompile; not yet confirmed in +game. diff --git a/src/iee/features/world_post.cpp b/src/iee/features/world_post.cpp index df0d720..4832828 100644 --- a/src/iee/features/world_post.cpp +++ b/src/iee/features/world_post.cpp @@ -107,7 +107,7 @@ struct Resources { unsigned spriteUpscale{}; SharpenProgram spriteSharpen{}; std::array atlases{}; - bool atlasDrawnOnce{}; + std::array atlasDrawnOnce{}; BlurProgram blurDown{}; BlurProgram blurUp{}; Target fogCapture{}; @@ -896,30 +896,40 @@ unsigned world_post_upscale_atlas(int slot, unsigned sourceTexture, int width, i gl::check_error("sprite atlas targets"); } if (ready) { + // Both shaders address texels by gl_FragCoord, so a viewport selects + // which part of the copy is redrawn. + const auto filter = [&](game::Region region) { + fn.glViewport(region.x, region.y, region.width, region.height); + fn.glBindFramebuffer(gl::FRAMEBUFFER, atlas.upscaled.framebuffer); + fn.glUseProgram(g_resources.spriteUpscale); + fn.glBindTexture(gl::TEXTURE_2D, sourceTexture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); + fn.glBindFramebuffer(gl::FRAMEBUFFER, atlas.sharpened.framebuffer); + fn.glUseProgram(g_resources.spriteSharpen.id); + fn.glUniform1f(g_resources.spriteSharpen.sharpness, g_settings.spriteSharpness); + fn.glBindTexture(gl::TEXTURE_2D, atlas.upscaled.texture); + fn.glDrawArrays(gl::TRIANGLES, 0, 3); + }; // Only the rows the engine just filled; two rows of margin for the kernel. const int outputRows = rows * 2 + 4 < doubled.height ? rows * 2 + 4 : doubled.height; set_pass_state(); - fn.glViewport(0, 0, doubled.width, outputRows); - fn.glBindFramebuffer(gl::FRAMEBUFFER, atlas.upscaled.framebuffer); - fn.glUseProgram(g_resources.spriteUpscale); - fn.glBindTexture(gl::TEXTURE_2D, sourceTexture); - fn.glDrawArrays(gl::TRIANGLES, 0, 3); - fn.glBindFramebuffer(gl::FRAMEBUFFER, atlas.sharpened.framebuffer); - fn.glUseProgram(g_resources.spriteSharpen.id); - fn.glUniform1f(g_resources.spriteSharpen.sharpness, g_settings.spriteSharpness); - fn.glBindTexture(gl::TEXTURE_2D, atlas.upscaled.texture); - fn.glDrawArrays(gl::TRIANGLES, 0, 3); + filter({0, 0, doubled.width, outputRows}); + // Untextured shapes drawn in this flush sample the last corner of + // atlas 0, which those rows rarely reach: without it they come out + // transparent. + if (slot == 0 && outputRows < doubled.height) filter(game::untextured_corner(doubled)); result = atlas.sharpened.texture; ++g_atlasUpscalesSinceArea; - if (!g_resources.atlasDrawnOnce) { - g_resources.atlasDrawnOnce = true; + auto& drawnOnce = g_resources.atlasDrawnOnce[static_cast(slot)]; + if (!drawnOnce) { + drawnOnce = true; if (!gl::check_error("sprite atlas upscale")) { result = 0; ready = false; } else { try { - LOG_INFO("World post: first sprite atlas upscaled ({}x{} -> {}x{}, {} rows)", width, - height, doubled.width, doubled.height, rows); + LOG_INFO("World post: sprite atlas {} first upscaled ({}x{} -> {}x{}, {} rows)", slot, + width, height, doubled.width, doubled.height, rows); } catch (...) { } } diff --git a/src/iee/game/world_post_plan.cpp b/src/iee/game/world_post_plan.cpp index 3c2ae2e..42d827f 100644 --- a/src/iee/game/world_post_plan.cpp +++ b/src/iee/game/world_post_plan.cpp @@ -13,6 +13,15 @@ Extent half_extent(Extent extent) noexcept { return {std::max(1, extent.width / 2), std::max(1, extent.height / 2)}; } +Region untextured_corner(Extent upscaled) noexcept { + // The 2x2 block doubled, plus two source texels of kernel reach. + constexpr int kSide = 8; + const int width = std::clamp(upscaled.width, 0, kSide); + const int height = std::clamp(upscaled.height, 0, kSide); + return {std::max(0, upscaled.width) - width, std::max(0, upscaled.height) - height, width, + height}; +} + int available_blur_levels(Extent base) noexcept { int levels = 0; Extent extent = base; diff --git a/src/iee/game/world_post_plan.h b/src/iee/game/world_post_plan.h index b642cc7..b2efaba 100644 --- a/src/iee/game/world_post_plan.h +++ b/src/iee/game/world_post_plan.h @@ -10,6 +10,14 @@ struct Extent { friend bool operator==(const Extent&, const Extent&) = default; }; +struct Region { + int x{}; + int y{}; + int width{}; + int height{}; + friend bool operator==(const Region&, const Region&) = default; +}; + inline constexpr int kMaxBlurLevels = 6; // A dual-filter blur: `levels` successive half-size images below the source, @@ -25,6 +33,12 @@ int available_blur_levels(Extent base) noexcept; // The plan whose blur reaches about `radiusPixels` on an image of `base` size. BlurPlan blur_plan_for_radius(float radiusPixels, Extent base) noexcept; int bloom_levels(Extent base) noexcept; +// The engine has no untextured draw: selection circles, health bars and other +// plain-coloured shapes sample a 2x2 white block it keeps in the last rows and +// columns of sprite atlas 0 (written once in DrawInit_GL, addressed at +// coordinate (1, 1)). This is that block, with room for the filter kernels, +// in a 2x copy of the atlas. +Region untextured_corner(Extent upscaled) noexcept; // Tells world draws from UI draws by where a frame is. Within a frame that // shows an area, everything queued up to the fog of war is world and everything // after it is UI; a frame that shows no area (menus, full-screen panels) is diff --git a/tests/iee_tests.cpp b/tests/iee_tests.cpp index 79f2434..7be94b2 100644 --- a/tests/iee_tests.cpp +++ b/tests/iee_tests.cpp @@ -1593,6 +1593,11 @@ void test_world_post_plan() { expect_eq(available_blur_levels({10, 10}), 0, "A tiny image supports no blur levels"); expect_eq(available_blur_levels({0, 0}), 0, "A degenerate image supports no blur levels"); + expect_true(untextured_corner({2048, 2048}) == Region{2040, 2040, 8, 8}, + "The untextured-draw texels sit in the last corner of the atlas copy"); + expect_true(untextured_corner({4, 2}) == Region{0, 0, 4, 2}, + "The corner region never leaves a small atlas"); + const auto none = blur_plan_for_radius(0.0f, {1920, 1080}); expect_eq(none.levels, 0, "Radius 0 means no blur"); expect_eq(blur_plan_for_radius(std::numeric_limits::quiet_NaN(), {1920, 1080}).levels, 0, From 41b0b5805fff88584e9d79c5e9af29c83e0865e3 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 8 Oct 2026 20:07:52 +0200 Subject: [PATCH 56/57] docs: selection circle fix confirmed in game --- docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md index 1e7780a..11b7895 100644 --- a/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md +++ b/docs/superpowers/specs/2026-10-07-soft-fog-and-bloom-design.md @@ -441,5 +441,4 @@ Fix: after the uploaded rows, the same two passes redraw the corner of atlas first time it is upscaled. Rule for any later change to the atlas swap: whatever replaces atlas 0 must -keep its last corner white. Derived from the decompile; not yet confirmed in -game. +keep its last corner white. Owner confirmed the fix in game the same day. From 99da4357470718c771e5374ab8b40f30c49c0572 Mon Sep 17 00:00:00 2001 From: Gabriel El Faouzi Date: Thu, 8 Oct 2026 20:20:08 +0200 Subject: [PATCH 57/57] docs: shader effect replacement design, blood first --- ...6-10-08-effect-replacement-blood-design.md | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 docs/superpowers/specs/2026-10-08-effect-replacement-blood-design.md diff --git a/docs/superpowers/specs/2026-10-08-effect-replacement-blood-design.md b/docs/superpowers/specs/2026-10-08-effect-replacement-blood-design.md new file mode 100644 index 0000000..fcccfd8 --- /dev/null +++ b/docs/superpowers/specs/2026-10-08-effect-replacement-blood-design.md @@ -0,0 +1,255 @@ +# Shader effect replacement, with blood as the first effect + +Date: 2026-10-08 +Status: Design, awaiting owner review. Nothing here is built. +Target: BGEE 2.7.3 (x64, Proton), EEex-loaded DLL. Other builds leave it off. + +Roadmap item C2 of +[2026-10-03-graphics-uplift-roadmap-design.md](2026-10-03-graphics-uplift-roadmap-design.md). +The standing constraints of that document apply (manifest-only addresses, +fail closed, INI gate plus runtime toggle, coexist with EEex's detours). + +## 1. Goal + +A mechanism that lets a GLSL shader repaint a chosen engine effect, and one +effect that proves it: the blood splash on a hit. + +Owner intent (2026-10-08): the remaining effect work is "replace effects with +GLSL"; start with blood, then add effects one at a time, most common first. +Anything not replaced must keep drawing exactly as today. + +Success for this spec: + +- Hit blood is drawn as smooth, glossy liquid at screen resolution instead of + magnified sprite pixels, in the creature's own blood colour, darkened by the + area light like everything else, hidden behind walls like today. +- A debug hotkey switches between the engine's blood and ours while playing. +- Adding the next effect needs a table entry and a shader, not new plumbing. +- The log lists which effects were replaced and which other effect art was + seen at the same hook, so the next effects are chosen from play, not guessed. + +## 2. Evidence (2.7.3, decompiled 2026-10-08) + +### 2.1 "Blood" is two systems + +`CGameSprite::AddBlood` (`0x347BC0`) branches on the blood type: + +| Kind | Path | Drawn by | +|---|---|---| +| Hit blood (`GUSH_LOW/MEDIUM/HIGH`, `PULSATING_ARTERY`) | `StartSpriteEffect(0, type, 0)` (`0x3794D0`) loads `BLOODS`, `BLOODM`, `BLOODL` or `BLOODCR` into the creature's `m_spriteSplashVidCell` (`CGameSprite + 0x3DE0`) and recolours its palette with the creature's blood colour (`GetColorBlood` / `m_bloodColor`) | `CGameSprite::RenderSpriteEffect` (`0x370E20`), called from `CGameSprite::Render` | +| Exploding death (`EXPLODING_DEATH`) | A `CBlood` object (`0x220F00`) holding `CParticle` dots and blobs, kept in `m_lstBlood` | `CBlood::Render` (`0x224F20`) through `CParticle::Render`, before or after the body depending on direction | + +Hit blood is the common case and the trial. The particle system is a later +second user of the same shader (section 9). + +### 2.2 The hit-blood draw + +`RenderSpriteEffect` draws the splash cell while its sequence has not ended: +`FXPrep` / `FXLock` / `FXRender` composite the frame on the CPU into the sprite +atlas, `FXRenderClippingPolys` cuts it against the wall polygons, and +`FXUnlock` / `FXBltFrom` queue one quad. The position is the creature's +`m_pos` plus its height offset; `MIRROR_FX` flips it for mirrored animations. + +The same function and the same cell also draw the fire, shock and acid impact +art (`SPFIRIMP`, `SPSHKIMP`, `SPBURN`, `SPSPARKS`, or a VVC named by the +effect). Whether a call is blood is decided by the splash cell's resref, never +by the call. + +The four BAMs are small and short: 6 to 13 frames, 5 to about 50 pixels on a +side (`BLOODS` 8 frames up to 17x16, `BLOODM` 13 up to 23x43, `BLOODL` 13 up +to 49x52, `BLOODCR` 6 up to 47x45). At the owner's zoom (about 7.5 screen +pixels per world pixel) one splash covers up to roughly 370 screen pixels. + +### 2.3 The draw queue merges draws + +`DrawEnd_GL` (`0x42B220`) appends a command only when the state word differs +from the previous command's; otherwise it adds the new vertices to the +previous command's `primCount`. One command can therefore hold several +sprites. A draw is identified by its vertex range, not by a command index: +`gl.user.primStart` (`0x2F73F70`, `0x60` bytes before `gl.n`) is the vertex +cursor, and it advances by exactly the vertices a draw adds. + +`DrawFlush_GL` resets the cursor to 0. The mod already calls the engine's +flush more than once per frame with a rewritten queue (the bloom's emissive +replay), and vertex data and atlas textures stay valid across those calls +until new draws are queued. + +## 3. Approach: the engine's own draw becomes the guide + +The engine keeps drawing the effect. The mod records which vertices that draw +added, and when the queue is flushed it sends just those vertices to an +offscreen "guide" image instead of the screen, then runs the effect's shader +over that part of the screen with the guide as input. + +Per replaced draw, inside one flush: + +1. Flush everything queued before it to the screen (the engine's flush, on a + shortened queue). +2. Clear the guide image in the effect's screen rectangle, bind it, and let + the engine's flush draw only the effect's vertices into it. +3. Bind the screen again and draw the effect shader over that rectangle, + reading the guide. +4. Continue with the rest of the queue. + +Why this shape: + +- **Everything the engine decides stays decided by the engine**: position, + mirroring, the creature's blood colour, the area light tint, wall clipping, + draw order against other creatures, the gore option, and movement smoothing + (the hook runs inside the already-smoothed creature render). None of it has + to be re-derived or kept in step. +- **Logic is untouched.** The engine's draw still runs, so frame counters, + durations and the cells `InterpolateAnimations` counts for a creature are + exactly as today. +- **Off means off.** With the feature disabled or failed, no range is + recorded and the flush is the engine's own. + +What it costs: the shader can only paint where the engine's art is, plus a +small margin. It gives a better-looking version of the same splash, not a +different, larger or longer one. + +### 3.1 Alternatives considered + +| Alternative | Why not now | +|---|---| +| **Free-form draw**: skip the engine's draw, flush, and draw our own quad with our own simulation (droplets with their own motion, any size, any lifetime) | Flashier, but it loses wall clipping (blood shows through walls), and position, mirroring, colour, light tint and the gore option all have to be rebuilt by hand. Skipping the engine's draw also has to be proven not to change effect lifetime. Worth revisiting per effect if the guided result is too tame; the table (section 4.2) leaves room for it | +| **Paint in the tile shader**, as fire and smoke do today | Sits under every creature, is multiplied by the tile light, and costs a per-pixel loop over all points. Those limits are why item C wanted to leave `fpSEAM` | +| **Batch all effects before the fog** | One pass instead of one per effect, but every effect is drawn over creatures standing in front of it | +| **Replace the BAM art** with better pre-rendered frames | Still sprite pixels magnified 7x; the owner rejected every sprite upscaler | + +## 4. Components + +New code lives in its own files; `world_post.cpp` is already 1,100 lines. + +### 4.1 `game/effect_ranges.*` (pure, unit-tested) + +Tracks vertex ranges and plans a flush. + +- `EffectRangeRecorder`: `begin(effect, cursor)` / `end(cursor)` around a + hooked draw; holds up to 16 ranges per flush. `on_flush()` returns the ranges + and clears them. A range still open at a flush (the engine flushes mid-frame + when its atlas fills) is closed at the current cursor and reopened at 0. + An empty range (the engine drew nothing) is dropped. +- `plan_flush(commands, ranges)`: turns the queue into an ordered list of + segments, each a run of commands tagged "screen" or "effect N". A command + whose vertices straddle a range boundary is split into copies with adjusted + `primStart` / `primCount`. Clear commands (`primCount == -1`) stay in screen + segments. Anything implausible (overlapping ranges, counts out of bounds) + yields a single screen segment: the engine's own frame. + +### 4.2 `game/effect_table.*` (pure, unit-tested) + +Maps an effect resref to how it is replaced: a shader kind and its +parameters. Entries for this spec: `BLOODS`, `BLOODM`, `BLOODL`, `BLOODCR` to +the `Liquid` kind. The table is compiled in; no file format until a second +effect kind shows what one would need. + +### 4.3 `features/world_effects.*` (GL) + +Owns the guide image (screen-sized RGBA8, recreated on resize) and the effect +programs. Three calls used by the flush detour: clear the guide in a +rectangle and bind it; bind the screen back; draw an effect over a rectangle. +Uses `GlPassGuard` / `GlStateGuard` as the world post passes do, and restores +the framebuffer bindings it found. + +The small GL helpers both features need (`Target`, `create_target`, +`build_program`, the full-screen vertex array) move from `world_post.cpp` into +a shared `features/gl_pass_util.*`. No behaviour change; nothing else in +`world_post.cpp` is restructured. + +### 4.4 Hook and flush changes (`hooks.cpp`, `build_manifest`) + +- New manifest pattern for `CGameSprite::RenderSpriteEffect`, with a + `static_assert` pinning it to the function's first bytes and a log line for + the resolved address (the 2026-10-08 lesson). New layout value: the vertex + cursor's distance from `gl.n`. +- The detour reads the splash cell's resref. If the world pass is open, the + feature is on and the table has the resref, it records the cursor, calls the + engine, and records the cursor again. Otherwise it only calls the engine. + The effect's screen rectangle comes from the creature position and the + cell's current frame box, padded by the shader's reach. +- `detour_draw_flush` takes the recorded ranges. With none, it behaves as + today. With some, it runs the segments of `plan_flush` through the engine's + flush one after another (the first carries the atlas upload), switching + target around effect segments, then does the emissive replay and the atlas + restore as today. + +## 5. The liquid shader + +Input: the guide image, which holds the engine's splash as premultiplied +colour (drawn with `SRC_ALPHA, ONE_MINUS_SRC_ALPHA` into a cleared target, so +its alpha is the sprite's alpha squared; the shader takes the square root). + +1. **Shape.** Sample the guide's alpha around the pixel with a small kernel + whose radius is a fraction of a world pixel times the zoom, and threshold + the result with a soft edge. Blurring then thresholding rounds the + magnified pixel blocks into smooth blobs and joins neighbouring droplets. +2. **Colour.** The blurred colour divided by the blurred alpha: the + creature's blood colour with the engine's light tint already in it. +3. **Depth and gloss.** The gradient of the blurred alpha stands in for a + surface normal: a darker rim, a slightly lighter centre and a small + highlight, so it reads as wet rather than flat. +4. **Output.** Normal alpha blending onto the screen. Blood is not additive + and does not feed the bloom. + +Shaders are validated offline with `glslangValidator`, like the others. + +## 6. Configuration + +```ini +[Effects] +; Draw hit blood as smooth, glossy liquid instead of sprite pixels. +ShaderBlood = false +; How wet it looks: 0 = flat colour, 1 = strong rim and highlight. +BloodGloss = 0.6 +``` + +Default off in the sample; on, with a visibly strong gloss, in the owner's +INI. With `EnableDebugHotkeys`, one key toggles it; F7 to F10 are taken, so +the key is chosen after checking the game's own key bindings. + +## 7. Failure handling and logging + +- Any GL error creating the guide or a program, or an invalid manifest: + the feature turns itself off with one log line and the engine draws as + before. +- More than 16 replaced draws in one flush: the extra ones are drawn by the + engine as today. +- Logged once per resref: "replaced" or the reason it was not. Logged once + per other resref seen in the splash cell: this is the list of impact art + the hook could replace next. +- The per-area `World post:` line gains the number of replaced draws and + the number of extra flushes that frame, so the cost is visible. + +## 8. Testing and validation + +- Unit tests: the range recorder (nesting refused, flush while open, empty + ranges, the cap) and `plan_flush` (no ranges, a range inside one merged + command, a range across several commands, a range at either end, clear + commands, implausible input); the table lookup. +- Host tests and the mingw compile check locally, then CI. +- In game, by the owner: hit something, toggle the key, compare. Checks worth + making: blood colour on a non-red creature, a hit behind a wall, a hit on a + mirrored creature, a crowded fight (cost), and that fire and shock impacts + look unchanged. + +## 9. Known limits + +- The splash keeps the original's shape, size, duration and frame steps. This + is a better-looking version of the same effect. +- Each replaced draw adds two calls to the engine's flush and two small + passes. Fine for a handful per frame; the log shows the count. +- Overlapping semi-transparent pixels within one splash accumulate in the + guide differently from on screen; with single-layer splash frames this does + not arise. +- 2.7.3 only. + +## 10. Out of scope, in likely order + +1. Exploding-death blood: bracket `CBlood::Render` the same way so its dots + and blobs feed the same liquid shader and merge into streams. +2. The other impacts drawn by the same hook (fire, shock, acid). +3. Spell effects: `CVEFVidCell::Render` is the next hook site. +4. Blood that stays on the ground for a while. That is a new effect, not a + replacement, and needs its own place in the draw order. +5. A free-form effect kind (section 3.1), if a specific effect needs it.