Removes the 16:9 pillarboxing (black bars on the sides) during cutscenes on ultrawide / super-ultrawide monitors, without editing the executable via an hex editor.
It does the same thing as the manual HxD fix — it just does it in memory, at runtime, every launch. No file edits, so it should survive game updates.
The Glacier engine stores the cutscene aspect ratio as a raw 32-bit float.
For 16:9 that value is 1.777778, which in memory is the bytes 39 8E E3 3F.
On startup this ASI scans the game's loaded executable image, finds every copy
of that constant, and overwrites it with the float for your aspect ratio
(Width / Height). Identical result to the hex edit, computed automatically.
The scan runs on a short retry schedule (up to 20 attempts, 500 ms apart) so it still lands if the engine is slow to map the constant — it succeeds as soon as the value appears and stops, so there's no fixed startup delay to wait out.
- An ASI loader. The standard one is Ultimate ASI Loader (ThirteenAG, on GitHub). Download the x64 build.
- Rename the loader's
dllto a name the game already loads, and drop it in the game'sRetailfolder (next to007FirstLight.exe). Good choices for a Glacier/Bink title:bink2w64.dll(Glacier uses Bink for video — natural fit), ordinput8.dll, orversion.dll, orwinhttp.dll. Any one of these works; pick whichever the loader's docs recommend if one doesn't take.
Place these next to 007FirstLight.exe (…\Steam\steamapps\common\007 First
Light\Retail):
Retail\
├─ 007FirstLight.exe
├─ <asi-loader>.dll (e.g. bink2w64.dll / dinput8.dll)
├─ 007FirstLightUltrawide.asi ← this patch
└─ 007FirstLightUltrawide.ini ← your settings
Some loader configs look for .asi files in a scripts\ or plugins\
subfolder instead — if so, put the .asi (and .ini) there.
Out of the box it's zero-config — AspectRatio = auto follows the game's live
render resolution, so the ratio is always right even if you change resolution or
resize the window. You only touch the ini to force a fixed ratio or override:
[General]
Enabled = 1 ; 0/false/no/off = ASI loads but does nothing
[Aspect]
AspectRatio = auto ; (default) follow the game's live resolution automatically
;AspectRatio = 2.388889 ; or force an exact ratio (overrides everything)
;Width = 3440 ; or an explicit resolution override (needs both)
;Height = 1440
[Advanced]
SearchAspect = 0 ; 0 = search for the built-in 16:9 value (normal)
[Video]
KbdToggleKey = VK_F10 ; keyboard key — flip ultrawide <-> 16:9 for FMV
GamepadToggleKey = XINPUT_GAMEPAD_BACK ; Xbox-style pad button (XInput) — does the same
DualSenseToggleButton = CREATE ; DualSense (PS5) button, read natively over HIDAuto mode reads the game window's current size every poll (through Windows —
no game-memory addresses) and recomputes the ratio, so resizes and resolution
changes are tracked live, and it keeps working across game updates with zero
maintenance. If the window can't be read yet it falls back to the monitor's size;
if neither is available it fails safe (renders the game unmodified). To force a
fixed ratio instead, set AspectRatio to a number, or set both Width and
Height.
Enabled = 0 is a quick way to turn the fix off without removing any files
(handy for A/B testing or if a future update breaks it).
[Advanced] SearchAspect controls the value the patch searches for in memory
(as opposed to [Aspect], which is the value it writes). Leave it at 0 for
normal use — that means "look for the game's stock 16:9 constant". You'd only
ever set it if a game update changed that stored value (see Troubleshooting).
Normal use: leave it at 0. Zero means "use the built-in 16:9 value (1.777778 → 39 8E E3 3F)." That's the value the game ships with, so you never touch this for a working patch.
When you'd change it: only if a game update relocates and changes the locked ratio so the scan reports Occurrences patched: 0. At that point the game is no longer storing 16:9, so searching for 16:9 finds nothing. You point SearchAspect at the new locked ratio instead — and because it's in the ini, you do it without recompiling.
SearchAspect |
float | search bytes |
|---|---|---|
0 (default) |
1.777778 (16:9) | 39 8E E3 3F |
1.85 |
1.850000 | CD CC EC 3F |
2.0 |
2.000000 | 00 00 00 40 |
2.39 |
2.390000 | C3 F5 18 40 |
Launch the game and play a cutscene — it should fill the screen. A log file
007FirstLightUltrawide.log is written next to the .asi; open it to confirm.
A good run looks like:
Replace bytes: 8E E3 18 40 (2.388889)
Patched occurrence #1 at 00007FF6...
Patched occurrence #2 at 00007FF6...
Finished. Occurrences patched: 2
You may also see one or more No matches yet (attempt N/20); retrying... lines
before the patch succeeds — that's normal when the engine hasn't finished
mapping the constant yet, and the retry loop simply waits for it.
The main patch fixes real-time, engine-rendered cinematics — the engine just
renders more width. Some scenes, though, are pre-rendered video files baked
at 16:9 (in this game they're packed inside the Glacier .rpkg archives and
played by CRI Movie — CRI Movie/PCx64 Ver.4.13.18, statically linked into
007FirstLight.exe, so there is no separate video-codec DLL). A 16:9 video has
no pixels for the ultrawide sides, so nothing can fill them with correct image —
the only clean option is to show the video centered at 16:9 while it plays,
then return to ultrawide.
Reliably auto-detecting when one of these is playing turned out to need a disassembler — the CRI Movie player is statically linked with no usable hook point, and the memory flags an earlier version polled were brittle and broke on game updates. So instead of guessing, this patch gives you a manual hotkey.
Press F10 (keyboard), your Xbox-style pad's View/Back button, or your
DualSense's Create button (the defaults) once when an FMV starts — the aspect flips
to the engine default 16:9 so the video is centered with side bars instead of stretched.
Press it again when the video ends to snap back to ultrawide. The switch is instant:
the engine re-reads the aspect every frame, so flipping the patched value takes effect
immediately (the same reason live AspectRatio = auto resolution tracking works). Launch
state is ultrawide.
[Video]
KbdToggleKey = VK_F10 ; keyboard key (read via GetAsyncKeyState)
GamepadToggleKey = XINPUT_GAMEPAD_BACK ; Xbox-style pad button (read via XInput)
DualSenseToggleButton = CREATE ; DualSense (PS5) button (read natively over HID)
PollIntervalMs = 50 ; how often the toggles + live resolution are polledAll three inputs work at once — any one toggles. The keyboard key is a Windows
virtual-key name: e.g. VK_F10, VK_PAUSE, VK_HOME (the VK_ prefix is optional;
a numeric code like 0x79 / 121 also works) — see Microsoft's
virtual-key codes.
The Xbox-style button is named by its XInput identifier: XINPUT_GAMEPAD_BACK
(the default — the "View"/Back button), XINPUT_GAMEPAD_START, XINPUT_GAMEPAD_A,
XINPUT_GAMEPAD_LEFT_SHOULDER, … (the XINPUT_GAMEPAD_ prefix is optional, so BACK
works too; the two analog triggers are XINPUT_GAMEPAD_LEFT_TRIGGER / _RIGHT_TRIGGER).
The DualSense button is named the PlayStation way: CREATE (the default), OPTIONS,
PS, CROSS, CIRCLE, SQUARE, TRIANGLE, L1/R1/L2/R2, L3/R3, TOUCHPAD,
MUTE (SHARE is an alias for CREATE). Names are case-insensitive, and 0 disables
that input.
The toggle only fires while the game window is focused, so the same key/button pressed in another app after alt-tabbing (or while the Steam overlay is up) won't flip the aspect underneath the game.
A DualSense is read directly over HID (USB or Bluetooth), so its Create button
works even when the pad is connected natively — where XInput, an Xbox-only API, can't
see it. The DualSense and DualSense Edge are both recognized, and the pad is detected on
hot-plug. An Xbox-style pad — or a DualSense routed through Steam Input /
DS4Windows, which makes it look like one — is read through XInput instead, and Steam
Input maps the DualSense's Create button to XINPUT_GAMEPAD_BACK. So Create is the
default toggle whether the pad is native or routed through Steam. The log records every
toggle (Hotkey: aspect toggled -> …), each apply (Aspect applied -> …), the configured
inputs, and when a DualSense connects, so you can confirm it's working.
Why not automatic? Found two memory addresses to detect playback, but those addresses move on game updates and the signal was ambiguous. A frame-accurate automatic detector means locating the CRI player's functions (
criMvPly_Start/_Stop/_GetStatus) in Ghidra/IDA and hooking them with MinHook — which is a much larger job. The manual toggle is deterministic, needs no addresses, and can't break on an update. That reverse-engineering design and insane ramblings are preserved indocs/cri-movie-detection.md, which I may work on in time.
Occurrences patched: 0— the scan locates the value wherever an update moves it, so this almost always means the value itself changed (a different baked constant, or stored/computed differently). Re-derive the 16:9 search bytes in HxD, convert them to a decimal aspect (or just use the new ratio), and set[Advanced] SearchAspectin the ini — no recompile needed. Timing is rarely the cause now: the patch re-scans automatically (20 attempts, 500 ms apart, ~10 s total), so it still lands even if the engine is slow to map the constant.- Log file never appears — the ASI isn't being loaded. Check that the loader proxy dll name is one the game actually imports, and that you used the x64 loader. Some titles need the proxy in the same folder as the exe.
- Cutscenes stretch instead of crop — that's the engine's choice for how it fits the wider ratio; this fix changes the ratio it targets, not the fit mode.
- Want to disable it — set
Enabled = 0in the ini, or just remove the.asi(or the loader dll). Nothing on disk in the game files was changed.
Requires CMake (3.15+) and a C++ compiler — either Visual Studio Build Tools
(MSVC) or MinGW/GCC. The CMakeLists.txt works with both, and it generates
007FirstLightUltrawide.ini automatically next to the built .asi (the
template lives inside CMakeLists.txt, so there's no separate file to copy).
The project targets 64-bit only.
Do not pass a toolchain file (those are for Linux cross-compiling).
cmake -B build
cmake --build build --config ReleaseOutput: build\Release\007FirstLightUltrawide.asi (with the .ini beside it).
MSVC is multi-config, so the --config Release is what selects the optimized
build — there's no -DCMAKE_BUILD_TYPE step.
Pick a non-Visual-Studio generator so CMake uses your MinGW gcc. No toolchain
file is needed — your shell's environment already provides the right compiler.
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release # or -G "MinGW Makefiles"
cmake --build buildOutput: build/007FirstLightUltrawide.asi (with the .ini beside it).
This is the only situation where the toolchain file applies:
cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-mingw-x64.cmake
cmake --build buildThe build accepts the semantic version as a cache variable; it is compiled into
the .asi (file-version resource + the .log header). Omit it and you get
0.0.0 / dev:
cmake -B build -DPROJECT_VERSION=1.4.0Pushing to main runs .github/workflows/release.yml, which builds an MSVC
x64 Release and publishes a GitHub Release. The version is derived
automatically from the commit history using
Conventional Commits:
| Commit prefix | Example | Bump |
|---|---|---|
fix: |
fix: handle 0-width ini value |
patch — 1.2.3 → 1.2.4 |
feat: |
feat: ini-configurable retry budget |
minor — 1.2.3 → 1.3.0 |
feat!: / BREAKING CHANGE: |
feat!: drop 32-bit support |
major — 1.2.3 → 2.0.0 |
Commits that aren't fix/feat/breaking (e.g. docs:, chore:) build but
publish no release. Each release attaches
007FirstLightUltrawide-vX.Y.Z.zip (the .asi + generated .ini), and that
version is the one embedded in the binary.
Seeding or forcing a version. Commit-based bumping needs a prior version tag to count from, so the first release (and any time you want a specific version) is cut by pushing a tag:
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0The workflow builds that exact version and publishes it. After a version tag
exists, pushes to main take over and auto-increment from it.
Release notes are generated automatically by
git-cliff (cliff.toml), which groups the Conventional
Commits in the release into sections — Features, Bug Fixes, Documentation,
CI/CD, and so on — so every release ships a detailed, categorized changelog.
For releases to publish, the repo's Settings → Actions → General → Workflow permissions must be set to Read and write (lets the default
GITHUB_TOKENcreate tags and releases).
Two layers keep the code consistent:
-
Locally — pre-commit. Fast format + hygiene checks, plus a commit-message check that enforces the Conventional Commits the release workflow depends on. One-time setup:
pip install pre-commit pre-commit install # installs the pre-commit and commit-msg hooks pre-commit run --all-files # optional: lint the whole tree now
Hooks:
clang-format(shares.clang-format), whitespace/EOF/line-ending fixers, andconventional-pre-commiton the commit message. clang-tidy is not run here (it needs a build database) — CI handles it. -
In CI — cpp-linter.
.github/workflows/cpp-linter.ymlrunsclang-formatandclang-tidy(using.clang-format/.clang-tidy) on every PR and push tomain, annotating findings inline. It configures an MSVC build first so clang-tidy can resolvewindows.h.
Style/check config lives in .clang-format and .clang-tidy at the repo root;
both the local hooks and CI read the same files, so results match.
- This patches every occurrence of the
39 8E E3 3Fbyte pattern in the main module. If the engine happens to use that same float for something unrelated, it would be changed too.