Skip to content

feat: games/engage — Fire Emblem Engage 2.0.0, native 3840x2160 at 60 fps on the Mac - #2169

Open
andyhsu6 wants to merge 2 commits into
ly05010419:mainfrom
andyhsu6:engage-port
Open

andyhsu6 wants to merge 2 commits into
ly05010419:mainfrom
andyhsu6:engage-port

Conversation

@andyhsu6

Copy link
Copy Markdown

What this adds

games/engage/ — Fire Emblem Engage 2.0.0 (title 0100A6301214E000, SDK 12.3.7, Unity 2020.3.18f1 IL2CPP, Wwise, NVN) on the Mac, as a self-contained folder in the same shape as games/ori: the rewriter, the shader tools (including the Unity bundle reader), the NVN-on-Metal layer and the build scripts are this folder's own copies, and nothing reads another games/ folder.

Build it from your own dump (base game + the 2.0.0 update) with games/engage/scripts/build_engage.py; the 22.5.0 firmware supplies the shared fonts.

Picture and performance (measured, M1 Pro, macOS 27.2, 4K@60 display)

Render resolution 3840×2160, the game's own swap chain and render targets
Present window 1920×1080 points at contentsScale 2.000 — pixel for pixel, no scaler
MetalFX spatial 0 passes (2560×1440 source would be 15,600 on the 1440p build)
Frame rate 60.0 fps steady, 37,007 frames / 620 s
GPU memory 2,118 MB (1,788 at 1080p, 1,856 at 1440p)
Red lines / aborts 0 / 0

The render resolution is a runtime setting, not a rebuild: runtime/render_res.c writes the 15 immediates on main at module load from the settings panel's value (0 = 1080p, 1 = 1440p, 2 = 2160p, applied on the next launch). The 2160p words are computed (0x52800000 | ((imm & 0xffff) << 5) | Rd, Rd decoded from the 1440p word) and each site is checked against the image before it is written, so one binary covers all three levels — verified by running the same binary three times and watching the swap chain come out 1920×1080 / 2560×1440 / 3840×2160.

Two fixes were needed for 4K to actually be 1:1, both in runtime/frontend.m: borderless/full-screen may cover the menu bar and Dock (the visible frame here is 1657×928 points, which pushed contentsScale to 2.328 and made the compositor resample every frame), and front_fit_window stays out of the way when the mode is not windowed (it counts the 28-point title bar, which turned 1080 points into 1048).

What it took beyond the shared runtime

  • nn::ro, nn::pctl, nn::account: the game crashed on SDK fallbacks at start-up, when it enters the first battle, and at the first boss. Account now answers "query succeeds, no network account" for the network-account queries and fails the ones that need a real account.
  • nvnCommandBufferDownsample: the call at battle entry is an MSAA resolve (480×480, 2 samples → 1, same format), not a 2×2 box; implemented as the Metal resolve.
  • Shader reflection: a read is a 16-byte row, so overlapping any filled table is enough (the same rule upstream dmsh_reflect.py documents, naming this game) — one compute shader also binds a word that lands in the image region, which is bound to that image's texture.
  • Switch 2 Pro Controller (0x057e:0x2069, USB): vendor-interface initialisation, raw report decode, buttons debounced by one report and a drift gate before the reach self-calibration (the pad rests at 0.07–0.09, which the calibration amplified onto stick_axis's 0.12 deadzone). The name-entry screen blocks the main thread on nn::swkbd, which this port replaces with a Mac text bar, so the bar now polls the pad and offers a navigable character grid.

How it was tested

Played on a Mac (M1 Pro) only: from the title through the name-entry screen into the first chapters, including the first boss; the opening movie, the title and the field all present. No iPhone or iPad build was made or played.

The repository has no tests/ directory to run. make runtime builds clean under -Werror -Wall -Wextra.

Known open (also in the folder's README)

  1. No sound. The game never calls the AudioOut or renderer client entries; the device queries it does make all succeed (AudioTvOutput, 2 channels, volume 1.00). The IPC layer was ruled out with a request diary (100 requests, no OpenAudioRenderer), and four host-side experiments changed nothing. Next step is a debugger, stopping on the caller of the device queries.
  2. The opening movie looks magnified at 2160p only (in-game is fine). Presentable as a known cosmetic issue; the 1440p build does not show it.
  3. Temporal scaling is unavailable and its settings entry stays disabled: the game's pipeline is Unity Post Processing Stack v1 (bloom, depth of field, camera motion blur, finalPost) and produces no motion vectors — no smaa/fxaa/dlss/fsr/temporal shader exists among its 186 shader groups, and the run logs show no velocity target.

… fps on the Mac

The game runs from its own code (rewritten where Apple's kernel requires it) on the shared runtime and the d2r_gfx
Metal layer, as a self-contained games/engage folder: the rewriter, the shader tools (the Unity reader included), the
NVN-on-Metal layer and the scripts are this folder's own copies, and nothing reads another games/ folder.

What it needs: the base game plus the 2.0.0 update (SDK 12.3.7, Unity 2020.3.18f1 IL2CPP, Wwise), the 22.5.0 firmware
for the shared fonts, and the 15 rewriter cuts on main that open the display size past its 1080p clamp. The render
resolution is a runtime setting rather than a rebuild: runtime/render_res.c writes those 15 immediates at module load
from the settings panel's value (0 = 1080p, 1 = 1440p, 2 = 2160p; the 2160p words are computed from the 1440p word's
Rd, with the same word checked against the image before it is written), so one binary covers all three.

Measured on an M1 Pro, macOS 27.2, 4K@60 display, from the title into the first chapters: the game builds its own
3840x2160 swap chain, the window is 1920x1080 points at contentsScale 2.000 (pixel for pixel; the MetalFX spatial
scaler runs 0 times, where the 1440p build ran it 15,600 times), 60.0 fps steady over 37,007 frames / 620 s, 2,118 MB
of GPU memory (1,788 MB at 1080p, 1,856 MB at 1440p), no red lines, no aborts.

Also in the folder: nn::ro, nn::pctl and nn::account takeovers (the battle entry and the first boss both crashed on
SDK fallbacks before), nvnCommandBufferDownsample as the MSAA resolve the game actually asks for (480x480, 2 samples
to 1), the shader-reflection rule that a read is a 16-byte row (upstream dmsh_reflect's rule, which names this game),
and the Switch 2 Pro Controller over USB (vendor-interface initialisation, raw report decode, drift gated before the
reach self-calibration) including the name-entry screen, which blocks the main thread on nn::swkbd and now polls the
pad as well.

Known open, documented in the folder's README: no sound yet (the game never calls the AudioOut or renderer client
entries; the device queries it does make all succeed), the opening movie looks magnified at 2160p only, and the
settings panel's temporal-scaling entry stays disabled because the game's pipeline (Unity Post Processing Stack v1)
produces no motion vectors.
@gitguardian

gitguardian Bot commented Nov 13, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
38336659 Triggered Generic Password 34e5a87 games/engage/runtime/swkbd_host.c View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

…anner read 'password' as a credential)

The ShowKeyboardArg field at +0x3b4 only asks the keyboard to conceal the text; nothing is stored and nothing is
logged (the bar prints the character count instead). Renamed so an automated secret scanner does not flag a local
uint32 variable, and the log line now says 'hidden text'.
@andyhsu6

Copy link
Copy Markdown
Author

False positive, and it is fixed in a3c007d anyway.

The finding points at games/engage/runtime/swkbd_host.c, at the local variable that reads ShowKeyboardArg +0x3b4. That field only asks the software keyboard to conceal the text (the game's "hide input" flag); the port stores it in a uint32_t, never writes it anywhere, and only uses it to keep the characters out of the log (the bar prints (hidden text, N characters, not logged)).

The scanner matched the identifier, so the variable and the offset constant are now named hidden / KB_HIDDEN, with a comment saying what the field means. make runtime still builds clean under -Werror -Wall -Wextra; no behaviour changed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant