zyser007/tloverlay

C#

0

41 commits

updated Sep 14, 2026

See the code

README

TLOverlay

Real-time English -> Thai translation overlay for games running in Windowed Fullscreen (borderless) on Windows 10 / 11.

โปรแกรม overlay แปลอังกฤษเป็นไทยแบบเรียลไทม์ สำหรับเกมที่รันแบบ Windowed Fullscreen บน Windows 10/11 — ไม่ inject DLL เข้าเกม แปลได้ทั้งแบบออฟไลน์ 100% ด้วยโมเดลในเครื่อง หรือผ่าน Google / OpenAI สำหรับเครื่องสเปกต่ำที่รันโมเดลเองไม่ไหว

Download

Latest release — download TLOverlay-<version>-win-x64.zip, unzip it somewhere writable, run TLOverlay.exe.

Self-contained: no .NET runtime to install. Windows SmartScreen warns on first run because the executable is unsigned - "More info" then "Run anyway". Do not run it from inside the zip; the app says so if you try, because everything it downloads would be deleted with the archive's temp folder.

After that it keeps itself up to date - see Updates.

How it works

Windows.Graphics.Capture   capture the game's window only, never the whole screen
        v
ChangeDetector             skip frames that did not change (>90% of them)
        v
Windows.Media.Ocr          offline English OCR, ships with Windows
        v
TextAssembler              rebuild wrapped lines into whole sentences
        v
CachingTranslator          SQLite + LRU; repeat dialogue is instant
        v
llama.cpp on 127.0.0.1     local GGUF model, no network egress
        v
Overlay window             layered, click-through, excluded from capture

Three design decisions carry most of the weight:

  • No injection, no D3D hooking. The overlay is an ordinary topmost window and capture goes through the public WGC API. That keeps it out of anti-cheat's way and off the list of things that crash games.
  • The overlay is excluded from capture via SetWindowDisplayAffinity (WDA_EXCLUDEFROMCAPTURE). Without it, OCR reads back its own Thai output and the pipeline feeds on itself.
  • Text must hold still before we translate it. Games reveal dialogue one character at a time; translating on first difference would fire a dozen times per line and show half-sentences.

Requirements

To run

  • Windows 10 build 19041 (2004) or newer, or Windows 11
  • The game must run Windowed Fullscreen / Borderless. True exclusive fullscreen cannot be captured.
  • English OCR language pack (present by default on virtually all installs)
  • For the local engine, roughly 3 GB of free RAM. For Google or OpenAI, a connection instead - see Translation engines.
  • Nothing else. Release builds are self-contained, so no .NET runtime install.

To build

  • .NET 8 SDK
  • Windows. This does not cross-compile: WPF and the WinRT projections have no Linux or macOS build, so dotnet build fails outside Windows no matter the runtime identifier.

Using it

  1. dotnet run --project src/TLOverlay.App
  2. First run opens the setup screen. Download the model, or point it at one you already have. This happens once.
  3. Pick the game window from the list. The panel warns you if the window still has a border, which usually means the game is in exclusive fullscreen.
  4. Ctrl+Alt+R, drag a box over the dialogue area, press Enter. The editor opens showing the area already set, so redrawing replaces it deliberately and Escape keeps what was there. One area per game, saved and reloaded automatically.
  5. Ctrl+Alt+T to start translating.
  6. To move the Thai out of the way, switch the mouse mode to interactive (Ctrl+Alt+C), drag the panel where you want it and drag its bottom-right corner - the ridged grip - to resize. The position is saved per game. Switch back to click-through to play.
HotkeyAction
Ctrl+Alt+TStart / stop translating
Ctrl+Alt+RDraw the capture region
Ctrl+Alt+STranslate now (the current mode decides how much)
Ctrl+Alt+FTranslate the whole screen
Ctrl+Alt+HShow / hide the translated text
Ctrl+Alt+GShow / hide the translation area
Ctrl+Alt+CSwitch between click-through and interactive

These are the defaults; all six can be rebound under ตั้งค่า. A binding needs at least one of Ctrl, Alt or Shift - a global hotkey with no modifier is swallowed system-wide, which would take that key away from the game. Changes are saved as the differences from the defaults, so an action added in a later version arrives with a working key rather than none. คืนค่าพื้นฐาน puts everything in that window back.

Three translating modes

ModeWhat it does
อัตโนมัติWatches the capture region and translates each line as it settles. The default.
เฉพาะพื้นที่ที่เลือกSame region, but only when you press Ctrl+Alt+S.
ทั้งหน้าจอReads every line of text anywhere on the game window and draws the Thai over the original, in place. Ctrl+Alt+F.

On-demand suits a game that redraws its dialogue box constantly, or a session where you only want a line here and there. It ignores the "same text as last time" guard, so pressing twice on one line really does translate it again.

Full screen is for menus, item tables and quest logs - text scattered in places a single box cannot cover. One press reads the whole window, translates every line in one request, and covers each original with an opaque Thai label. The labels stay until the next press; Ctrl+Alt+H hides and shows them. The opacity of those labels is a slider, saved per game.

Text size is a slider too, next to it, and it applies to both presentations. For the subtitle panel it is the size outright; for full-screen labels it scales the size each label takes from its own OCR box, so a menu entry and a line of dialogue stay proportioned to each other. Turned up far enough, labels grow past the English they cover and may overlap the lines around them - which is the right trade when the alternative is text too small to read.

It only works on demand, and that is structural rather than a preference: a whole game screen always has something moving on it - a clock, a health bar, an idle animation - so change detection over the window would fire continuously and translate everything several times a second.

Two things to know before leaning on it. OCR reads a full screen less well than a tight crop, because the region path upscales a small box before reading it and a whole window cannot be upscaled - large dialogue text comes back well, small HUD text less so. And on a metered engine every press translates the whole HUD, while a local model on a CPU will take a long time over forty lines; the mode hint says so for whichever engine is selected. Lines already translated once are served from cache, so pressing again after the dialogue advances usually costs only the lines that changed.

Translation engines

Three, chosen in Setup. The pipeline in front of them - capture, change detection, OCR, sentence assembly, glossary, cache - is the same either way; only the last step moves.

EngineNeedsPrivacyCost
Local model (default)~3 GB free RAM, a 0.8-2.4 GB downloadNothing leaves the machineFree
Google TranslateNothing, or a Cloud Translation keyGame text goes to GoogleFree, or per character
OpenAI-compatibleAn API keyGame text goes to the providerPer line

The local model is the default and the only fully offline option. It is also the one that does not fit everywhere: a machine with 8 GB of RAM and a game already running has no 3 GB to spare, and on a slow CPU a line can take longer than the dialogue stays on screen. That is what the other two are for.

Google works with no account at all. Without a key it uses the endpoint the Google Translate web page itself calls - undocumented, rate-limited per IP address, and something Google can change or withdraw without notice. Setup says so where the choice is made rather than letting it be discovered mid-game. With a Cloud Translation API key it uses the documented, billed v2 API instead.

OpenAI-compatible gives the most natural Thai of the three for game dialogue, because it gets the same prompt and the same few-shot examples the local model does - switching between them changes the speed and the bill, not the voice. The endpoint is configurable, so the same setting points at OpenRouter, Groq, or a model running on another PC in the house.

API keys are encrypted with DPAPI under the current Windows account before they are written to settings.json, so a settings file that ends up in a bug report or a backup does not hand over someone's billing credentials. Setup has a test button that translates one sentence and shows the result - a wrong key or model name otherwise surfaces much later as an overlay that silently never says anything.

The cache is keyed on the engine's identity, so switching engines never serves one engine's lines from another's.

Models

These are for the local engine only - on Google or OpenAI there is nothing to download.

The translation model and llama-server.exe are not in this repository, and you do not need a terminal to get them. Run the app: if either is missing it opens a setup screen that downloads both, with a progress bar, a resume if the connection drops, and a Browse button for files you already have. Reachable later from the control panel as ตั้งค่าโมเดล, which is also how you switch model or move the work between CPU and GPU.

If you would rather script it, tools/fetch-models.ps1 does the same job, but it needs PowerShell 7 (pwsh), which Windows does not ship by default.

Both rows have a ลบ button. The model is the largest thing this app puts on a disk by two orders of magnitude, and a player who has moved to a cloud engine - or who just needs the space back for a game - should not have to go hunting through %LocalAppData% for it. Deleting the model takes any half-finished download with it; deleting the server takes the whole runtime folder, since the executable does not run without the DLLs beside it. A server pointed somewhere else by hand loses only its executable - that folder is the player's.

Where they are installed

The server and the model together are several gigabytes, so the setup screen lets you choose the folder they go in - useful when the system drive is the full one. It shows the free space on the chosen drive and warns before a download that would not fit.

Changing the location when something is already installed offers to move it. The move copies everything across first and only deletes the original once every file has arrived, so an interrupted move costs you the copy, never the install. Settings, profiles, logs and the translation cache are small and stay under %LocalAppData%\TLOverlay regardless; only runtime\ and models\ move.

See NOTICE.md for model licensing - it matters, and not every model here may be used commercially. The setup screen shows each model's licence next to it in the dropdown.

Build

dotnet build tloverlay.sln -c Release
dotnet test tests/TLOverlay.Core.Tests/TLOverlay.Core.Tests.csproj
dotnet run --project src/TLOverlay.App

The projects compile against net8.0-windows10.0.22621.0 but declare SupportedOSPlatformVersion 10.0.19041.0. The newer projection is only needed at compile time, for GraphicsCaptureSession.IsBorderRequired; that property is probed with ApiInformation before use, so the app still runs on Windows 10 2004 - it just keeps the yellow capture border there.

Release

Producing a build

dotnet publish src/TLOverlay.App/TLOverlay.App.csproj `
  --configuration Release `
  --runtime win-x64 `
  --self-contained true `
  -p:PublishSingleFile=true `
  -p:IncludeNativeLibrariesForSelfExtract=true `
  -p:DebugType=None `
  --output artifacts/TLOverlay-win-x64

Three of those flags are load-bearing, and one option must stay off:

  • --self-contained true - the audience is players, not developers. Asking them to install the .NET Desktop Runtime before they can read their game is a step most will not get past. The cost is size: the release zip is around 75 MB. Single-file packing means it really is one TLOverlay.exe, nothing beside it.
  • -p:IncludeNativeLibrariesForSelfExtract=true - Microsoft.Data.Sqlite needs the native e_sqlite3.dll. Without this it never leaves the bundle and the translation cache throws on the first lookup, at runtime, on the user's machine.
  • -p:EnableCompressionInSingleFile=true - halves the executable, from about 183 MB to about 75 MB. That is the file the in-app updater downloads, so without it every update costs the user two and a half times the data of the zip. The price is a few hundred milliseconds of decompression at startup, once per launch, which nobody waiting for a game to load will notice.
  • --runtime win-x64 - required for a self-contained build, and the only architecture the capture interop targets.
  • Never -p:PublishTrimmed=true. WPF does not support trimming. The build succeeds and the executable crashes on startup, which is the worst possible place to find out.

What ships, and what does not

The zip contains TLOverlay.exe, NOTICE.md and README.md.

It does not contain models/ or runtime/. Those are several gigabytes, and their licences differ per model - some are non-commercial. The app fetches them itself on first run, which is the whole reason the setup screen exists.

Cutting a release

  1. Bump <Version> in Directory.Build.props.
  2. Commit that on the default branch.
  3. git tag v0.2.0 && git push origin v0.2.0

The tag is what triggers .github/workflows/release.yml: it runs the tests, publishes, zips, and creates the GitHub Release with generated notes. Nothing else publishes a release, so a tag always corresponds to something that shipped.

A release carries three assets, and the updater needs all three shapes to be right:

AssetFor
TLOverlay-<version>-win-x64.zippeople downloading by hand
TLOverlay.exethe in-app updater - one file, no unpacking
SHA256SUMS.txtwhat the updater checks the download against

A release missing the executable or the checksums is simply not offered as an update. That is deliberate: the binary is unsigned, so those hashes are the only thing standing between the updater and whatever arrived over the wire.

CI runs the same publish command on every push and uploads the result as a build artifact. A publish that has stopped working is therefore caught on the commit that broke it, not while cutting a tag.

Note on signing

The executable is unsigned, so Windows SmartScreen shows a warning the first time a user runs it ("More info" then "Run anyway"). Removing that needs a code signing certificate, which this project does not have. Worth saying plainly in release notes rather than leaving people to wonder.

Status

End to end and working: capture, OCR, translation, overlay, per-game profiles, glossary, caching, hotkeys and the region editor. The control panel reports average OCR and translation time plus the frame skip ratio, which is the number that tells you whether change detection is doing its job - below roughly 80% during play means a region is picking up animated scenery.

Not built yet: the NLLB ONNX backend as a lighter alternative to the local LLM, and automatic region detection.

Updates

The app checks GitHub for a newer release once a day, on startup, and says so with a banner on the control panel. Nothing is downloaded until you press the button. Three settings, under การอัพเดทโปรแกรม:

SettingWhat it does
แจ้งเตือน (default)Checks and tells you. You decide when.
ดาวน์โหลดอัตโนมัติChecks, downloads and restarts into it.
ไม่ต้องตรวจสอบNever contacts GitHub.

Notify is the default because this program runs beside a game: a background download of 70 MB is not something to start on someone's connection mid-session. For the same reason it is a banner rather than a dialog, and installing stops a running translation session first - replacing the executable underneath a live capture session and a model server held in a job object is how you end up with an orphaned llama-server sitting on two gigabytes.

How the swap works. Windows will not let a running executable be overwritten, but it will let one be renamed. So there is no separate updater program to install, keep in sync, or fail to clean up: the running exe becomes TLOverlay.exe.old, the new one takes its name, the app restarts and deletes the leftover. If the update dies between those two renames - the one moment the folder can hold a .old and no program - the next startup puts the old one back.

What is checked before anything is replaced, in order: the download starts with MZ, its SHA-256 matches SHA256SUMS.txt from the release, and the new build runs --version and answers with the version it claims. Only then is anything renamed. That last check is not paranoia - a publish can be broken in ways that only appear at startup, and finding out here means you keep a working program instead of one that no longer opens.

Installed somewhere unwritable (Program Files, read-only media), the app says so and sends you to the release page rather than asking for administrator rights an overlay has no business holding.

Memory

Two processes, and they are worth understanding separately.

The app should sit at roughly 150-250 MB and stay there. It is flat by construction: a 1080p frame is 8 MB and several are pulled every second, so every frame buffer is rented from a pool and returned rather than allocated. Frames go through one WinRT buffer that is reused until the capture size changes - a fresh one per frame is native memory the GC cannot see, so nothing about allocating it creates any pressure to collect the wrapper that owns it. Getting that wrong once took the app past 10 GB inside two minutes.

Polling backs off on its own. Eight frames a second is right while dialogue is moving and pure waste in a menu, so after a quiet stretch the interval doubles up to half a second, and returns to full rate on the first change. The control panel shows the current interval next to the memory figures.

The model server needs roughly the model's file size plus a few hundred megabytes. That is the number that decides which model a machine can run, so Setup shows it beside each model along with the machine's own RAM, and says so plainly when the two do not leave room for a game. On 8 GB, use Gemma 3 1B.

If the app's figure climbs steadily during a session, that is a bug - the readout is on the panel so it can be reported with a number.

Known limits

  • Exclusive fullscreen cannot be captured. Borderless only.
  • Windows 10 builds before Windows 11 show a yellow capture border around the game that the OS will not let us turn off.
  • Text baked into textures with heavily stylised fonts may not read.
  • A local LLM competes with the game for VRAM. gpuLayers defaults to 0 (CPU) in %AppData%\TLOverlay\settings.json for that reason; raise it if you have headroom and want sub-second translations.

zyser007/tloverlay

C#

0

41 commits

updated Sep 14, 2026

See the code

README

TLOverlay

Real-time English -> Thai translation overlay for games running in Windowed Fullscreen (borderless) on Windows 10 / 11.

โปรแกรม overlay แปลอังกฤษเป็นไทยแบบเรียลไทม์ สำหรับเกมที่รันแบบ Windowed Fullscreen บน Windows 10/11 — ไม่ inject DLL เข้าเกม แปลได้ทั้งแบบออฟไลน์ 100% ด้วยโมเดลในเครื่อง หรือผ่าน Google / OpenAI สำหรับเครื่องสเปกต่ำที่รันโมเดลเองไม่ไหว

Download

Latest release — download TLOverlay-<version>-win-x64.zip, unzip it somewhere writable, run TLOverlay.exe.

Self-contained: no .NET runtime to install. Windows SmartScreen warns on first run because the executable is unsigned - "More info" then "Run anyway". Do not run it from inside the zip; the app says so if you try, because everything it downloads would be deleted with the archive's temp folder.

After that it keeps itself up to date - see Updates.

How it works

Windows.Graphics.Capture   capture the game's window only, never the whole screen
        v
ChangeDetector             skip frames that did not change (>90% of them)
        v
Windows.Media.Ocr          offline English OCR, ships with Windows
        v
TextAssembler              rebuild wrapped lines into whole sentences
        v
CachingTranslator          SQLite + LRU; repeat dialogue is instant
        v
llama.cpp on 127.0.0.1     local GGUF model, no network egress
        v
Overlay window             layered, click-through, excluded from capture

Three design decisions carry most of the weight:

  • No injection, no D3D hooking. The overlay is an ordinary topmost window and capture goes through the public WGC API. That keeps it out of anti-cheat's way and off the list of things that crash games.
  • The overlay is excluded from capture via SetWindowDisplayAffinity (WDA_EXCLUDEFROMCAPTURE). Without it, OCR reads back its own Thai output and the pipeline feeds on itself.
  • Text must hold still before we translate it. Games reveal dialogue one character at a time; translating on first difference would fire a dozen times per line and show half-sentences.

Requirements

To run

  • Windows 10 build 19041 (2004) or newer, or Windows 11
  • The game must run Windowed Fullscreen / Borderless. True exclusive fullscreen cannot be captured.
  • English OCR language pack (present by default on virtually all installs)
  • For the local engine, roughly 3 GB of free RAM. For Google or OpenAI, a connection instead - see Translation engines.
  • Nothing else. Release builds are self-contained, so no .NET runtime install.

To build

  • .NET 8 SDK
  • Windows. This does not cross-compile: WPF and the WinRT projections have no Linux or macOS build, so dotnet build fails outside Windows no matter the runtime identifier.

Using it

  1. dotnet run --project src/TLOverlay.App
  2. First run opens the setup screen. Download the model, or point it at one you already have. This happens once.
  3. Pick the game window from the list. The panel warns you if the window still has a border, which usually means the game is in exclusive fullscreen.
  4. Ctrl+Alt+R, drag a box over the dialogue area, press Enter. The editor opens showing the area already set, so redrawing replaces it deliberately and Escape keeps what was there. One area per game, saved and reloaded automatically.
  5. Ctrl+Alt+T to start translating.
  6. To move the Thai out of the way, switch the mouse mode to interactive (Ctrl+Alt+C), drag the panel where you want it and drag its bottom-right corner - the ridged grip - to resize. The position is saved per game. Switch back to click-through to play.
HotkeyAction
Ctrl+Alt+TStart / stop translating
Ctrl+Alt+RDraw the capture region
Ctrl+Alt+STranslate now (the current mode decides how much)
Ctrl+Alt+FTranslate the whole screen
Ctrl+Alt+HShow / hide the translated text
Ctrl+Alt+GShow / hide the translation area
Ctrl+Alt+CSwitch between click-through and interactive

These are the defaults; all six can be rebound under ตั้งค่า. A binding needs at least one of Ctrl, Alt or Shift - a global hotkey with no modifier is swallowed system-wide, which would take that key away from the game. Changes are saved as the differences from the defaults, so an action added in a later version arrives with a working key rather than none. คืนค่าพื้นฐาน puts everything in that window back.

Three translating modes

ModeWhat it does
อัตโนมัติWatches the capture region and translates each line as it settles. The default.
เฉพาะพื้นที่ที่เลือกSame region, but only when you press Ctrl+Alt+S.
ทั้งหน้าจอReads every line of text anywhere on the game window and draws the Thai over the original, in place. Ctrl+Alt+F.

On-demand suits a game that redraws its dialogue box constantly, or a session where you only want a line here and there. It ignores the "same text as last time" guard, so pressing twice on one line really does translate it again.

Full screen is for menus, item tables and quest logs - text scattered in places a single box cannot cover. One press reads the whole window, translates every line in one request, and covers each original with an opaque Thai label. The labels stay until the next press; Ctrl+Alt+H hides and shows them. The opacity of those labels is a slider, saved per game.

Text size is a slider too, next to it, and it applies to both presentations. For the subtitle panel it is the size outright; for full-screen labels it scales the size each label takes from its own OCR box, so a menu entry and a line of dialogue stay proportioned to each other. Turned up far enough, labels grow past the English they cover and may overlap the lines around them - which is the right trade when the alternative is text too small to read.

It only works on demand, and that is structural rather than a preference: a whole game screen always has something moving on it - a clock, a health bar, an idle animation - so change detection over the window would fire continuously and translate everything several times a second.

Two things to know before leaning on it. OCR reads a full screen less well than a tight crop, because the region path upscales a small box before reading it and a whole window cannot be upscaled - large dialogue text comes back well, small HUD text less so. And on a metered engine every press translates the whole HUD, while a local model on a CPU will take a long time over forty lines; the mode hint says so for whichever engine is selected. Lines already translated once are served from cache, so pressing again after the dialogue advances usually costs only the lines that changed.

Translation engines

Three, chosen in Setup. The pipeline in front of them - capture, change detection, OCR, sentence assembly, glossary, cache - is the same either way; only the last step moves.

EngineNeedsPrivacyCost
Local model (default)~3 GB free RAM, a 0.8-2.4 GB downloadNothing leaves the machineFree
Google TranslateNothing, or a Cloud Translation keyGame text goes to GoogleFree, or per character
OpenAI-compatibleAn API keyGame text goes to the providerPer line

The local model is the default and the only fully offline option. It is also the one that does not fit everywhere: a machine with 8 GB of RAM and a game already running has no 3 GB to spare, and on a slow CPU a line can take longer than the dialogue stays on screen. That is what the other two are for.

Google works with no account at all. Without a key it uses the endpoint the Google Translate web page itself calls - undocumented, rate-limited per IP address, and something Google can change or withdraw without notice. Setup says so where the choice is made rather than letting it be discovered mid-game. With a Cloud Translation API key it uses the documented, billed v2 API instead.

OpenAI-compatible gives the most natural Thai of the three for game dialogue, because it gets the same prompt and the same few-shot examples the local model does - switching between them changes the speed and the bill, not the voice. The endpoint is configurable, so the same setting points at OpenRouter, Groq, or a model running on another PC in the house.

API keys are encrypted with DPAPI under the current Windows account before they are written to settings.json, so a settings file that ends up in a bug report or a backup does not hand over someone's billing credentials. Setup has a test button that translates one sentence and shows the result - a wrong key or model name otherwise surfaces much later as an overlay that silently never says anything.

The cache is keyed on the engine's identity, so switching engines never serves one engine's lines from another's.

Models

These are for the local engine only - on Google or OpenAI there is nothing to download.

The translation model and llama-server.exe are not in this repository, and you do not need a terminal to get them. Run the app: if either is missing it opens a setup screen that downloads both, with a progress bar, a resume if the connection drops, and a Browse button for files you already have. Reachable later from the control panel as ตั้งค่าโมเดล, which is also how you switch model or move the work between CPU and GPU.

If you would rather script it, tools/fetch-models.ps1 does the same job, but it needs PowerShell 7 (pwsh), which Windows does not ship by default.

Both rows have a ลบ button. The model is the largest thing this app puts on a disk by two orders of magnitude, and a player who has moved to a cloud engine - or who just needs the space back for a game - should not have to go hunting through %LocalAppData% for it. Deleting the model takes any half-finished download with it; deleting the server takes the whole runtime folder, since the executable does not run without the DLLs beside it. A server pointed somewhere else by hand loses only its executable - that folder is the player's.

Where they are installed

The server and the model together are several gigabytes, so the setup screen lets you choose the folder they go in - useful when the system drive is the full one. It shows the free space on the chosen drive and warns before a download that would not fit.

Changing the location when something is already installed offers to move it. The move copies everything across first and only deletes the original once every file has arrived, so an interrupted move costs you the copy, never the install. Settings, profiles, logs and the translation cache are small and stay under %LocalAppData%\TLOverlay regardless; only runtime\ and models\ move.

See NOTICE.md for model licensing - it matters, and not every model here may be used commercially. The setup screen shows each model's licence next to it in the dropdown.

Build

dotnet build tloverlay.sln -c Release
dotnet test tests/TLOverlay.Core.Tests/TLOverlay.Core.Tests.csproj
dotnet run --project src/TLOverlay.App

The projects compile against net8.0-windows10.0.22621.0 but declare SupportedOSPlatformVersion 10.0.19041.0. The newer projection is only needed at compile time, for GraphicsCaptureSession.IsBorderRequired; that property is probed with ApiInformation before use, so the app still runs on Windows 10 2004 - it just keeps the yellow capture border there.

Release

Producing a build

dotnet publish src/TLOverlay.App/TLOverlay.App.csproj `
  --configuration Release `
  --runtime win-x64 `
  --self-contained true `
  -p:PublishSingleFile=true `
  -p:IncludeNativeLibrariesForSelfExtract=true `
  -p:DebugType=None `
  --output artifacts/TLOverlay-win-x64

Three of those flags are load-bearing, and one option must stay off:

  • --self-contained true - the audience is players, not developers. Asking them to install the .NET Desktop Runtime before they can read their game is a step most will not get past. The cost is size: the release zip is around 75 MB. Single-file packing means it really is one TLOverlay.exe, nothing beside it.
  • -p:IncludeNativeLibrariesForSelfExtract=true - Microsoft.Data.Sqlite needs the native e_sqlite3.dll. Without this it never leaves the bundle and the translation cache throws on the first lookup, at runtime, on the user's machine.
  • -p:EnableCompressionInSingleFile=true - halves the executable, from about 183 MB to about 75 MB. That is the file the in-app updater downloads, so without it every update costs the user two and a half times the data of the zip. The price is a few hundred milliseconds of decompression at startup, once per launch, which nobody waiting for a game to load will notice.
  • --runtime win-x64 - required for a self-contained build, and the only architecture the capture interop targets.
  • Never -p:PublishTrimmed=true. WPF does not support trimming. The build succeeds and the executable crashes on startup, which is the worst possible place to find out.

What ships, and what does not

The zip contains TLOverlay.exe, NOTICE.md and README.md.

It does not contain models/ or runtime/. Those are several gigabytes, and their licences differ per model - some are non-commercial. The app fetches them itself on first run, which is the whole reason the setup screen exists.

Cutting a release

  1. Bump <Version> in Directory.Build.props.
  2. Commit that on the default branch.
  3. git tag v0.2.0 && git push origin v0.2.0

The tag is what triggers .github/workflows/release.yml: it runs the tests, publishes, zips, and creates the GitHub Release with generated notes. Nothing else publishes a release, so a tag always corresponds to something that shipped.

A release carries three assets, and the updater needs all three shapes to be right:

AssetFor
TLOverlay-<version>-win-x64.zippeople downloading by hand
TLOverlay.exethe in-app updater - one file, no unpacking
SHA256SUMS.txtwhat the updater checks the download against

A release missing the executable or the checksums is simply not offered as an update. That is deliberate: the binary is unsigned, so those hashes are the only thing standing between the updater and whatever arrived over the wire.

CI runs the same publish command on every push and uploads the result as a build artifact. A publish that has stopped working is therefore caught on the commit that broke it, not while cutting a tag.

Note on signing

The executable is unsigned, so Windows SmartScreen shows a warning the first time a user runs it ("More info" then "Run anyway"). Removing that needs a code signing certificate, which this project does not have. Worth saying plainly in release notes rather than leaving people to wonder.

Status

End to end and working: capture, OCR, translation, overlay, per-game profiles, glossary, caching, hotkeys and the region editor. The control panel reports average OCR and translation time plus the frame skip ratio, which is the number that tells you whether change detection is doing its job - below roughly 80% during play means a region is picking up animated scenery.

Not built yet: the NLLB ONNX backend as a lighter alternative to the local LLM, and automatic region detection.

Updates

The app checks GitHub for a newer release once a day, on startup, and says so with a banner on the control panel. Nothing is downloaded until you press the button. Three settings, under การอัพเดทโปรแกรม:

SettingWhat it does
แจ้งเตือน (default)Checks and tells you. You decide when.
ดาวน์โหลดอัตโนมัติChecks, downloads and restarts into it.
ไม่ต้องตรวจสอบNever contacts GitHub.

Notify is the default because this program runs beside a game: a background download of 70 MB is not something to start on someone's connection mid-session. For the same reason it is a banner rather than a dialog, and installing stops a running translation session first - replacing the executable underneath a live capture session and a model server held in a job object is how you end up with an orphaned llama-server sitting on two gigabytes.

How the swap works. Windows will not let a running executable be overwritten, but it will let one be renamed. So there is no separate updater program to install, keep in sync, or fail to clean up: the running exe becomes TLOverlay.exe.old, the new one takes its name, the app restarts and deletes the leftover. If the update dies between those two renames - the one moment the folder can hold a .old and no program - the next startup puts the old one back.

What is checked before anything is replaced, in order: the download starts with MZ, its SHA-256 matches SHA256SUMS.txt from the release, and the new build runs --version and answers with the version it claims. Only then is anything renamed. That last check is not paranoia - a publish can be broken in ways that only appear at startup, and finding out here means you keep a working program instead of one that no longer opens.

Installed somewhere unwritable (Program Files, read-only media), the app says so and sends you to the release page rather than asking for administrator rights an overlay has no business holding.

Memory

Two processes, and they are worth understanding separately.

The app should sit at roughly 150-250 MB and stay there. It is flat by construction: a 1080p frame is 8 MB and several are pulled every second, so every frame buffer is rented from a pool and returned rather than allocated. Frames go through one WinRT buffer that is reused until the capture size changes - a fresh one per frame is native memory the GC cannot see, so nothing about allocating it creates any pressure to collect the wrapper that owns it. Getting that wrong once took the app past 10 GB inside two minutes.

Polling backs off on its own. Eight frames a second is right while dialogue is moving and pure waste in a menu, so after a quiet stretch the interval doubles up to half a second, and returns to full rate on the first change. The control panel shows the current interval next to the memory figures.

The model server needs roughly the model's file size plus a few hundred megabytes. That is the number that decides which model a machine can run, so Setup shows it beside each model along with the machine's own RAM, and says so plainly when the two do not leave room for a game. On 8 GB, use Gemma 3 1B.

If the app's figure climbs steadily during a session, that is a bug - the readout is on the panel so it can be reported with a number.

Known limits

  • Exclusive fullscreen cannot be captured. Borderless only.
  • Windows 10 builds before Windows 11 show a yellow capture border around the game that the OS will not let us turn off.
  • Text baked into textures with heavily stylised fonts may not read.
  • A local LLM competes with the game for VRAM. gpuLayers defaults to 0 (CPU) in %AppData%\TLOverlay\settings.json for that reason; raise it if you have headroom and want sub-second translations.