{"id":47967740,"url":"https://github.com/ditrixnew/windows_gpu_recovery","last_synced_at":"2026-04-08T14:00:50.962Z","repository":{"id":348619868,"uuid":"1199089516","full_name":"DitriXNew/windows_gpu_recovery","owner":"DitriXNew","description":"Flutter Windows plugin that recovers from `EGL_CONTEXT_LOST` / `DXGI_ERROR_DEVICE_REMOVED` after system sleep, Hyper-V save/restore, TDR timeout, or GPU driver reset.","archived":false,"fork":false,"pushed_at":"2026-04-02T03:50:48.000Z","size":79,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-04T10:39:23.387Z","etag":null,"topics":["angle","d3d11","flutter","gpu","windows"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DitriXNew.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-02T03:23:49.000Z","updated_at":"2026-04-02T15:55:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/DitriXNew/windows_gpu_recovery","commit_stats":null,"previous_names":["ditrixnew/windows_gpu_recovery"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/DitriXNew/windows_gpu_recovery","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DitriXNew%2Fwindows_gpu_recovery","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DitriXNew%2Fwindows_gpu_recovery/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DitriXNew%2Fwindows_gpu_recovery/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DitriXNew%2Fwindows_gpu_recovery/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DitriXNew","download_url":"https://codeload.github.com/DitriXNew/windows_gpu_recovery/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DitriXNew%2Fwindows_gpu_recovery/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31433044,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-05T08:13:15.228Z","status":"ssl_error","status_checked_at":"2026-04-05T08:13:11.839Z","response_time":75,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["angle","d3d11","flutter","gpu","windows"],"created_at":"2026-04-04T10:39:00.965Z","updated_at":"2026-04-05T11:00:41.662Z","avatar_url":"https://github.com/DitriXNew.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# windows_gpu_recovery\n\n[![pub.dev](https://img.shields.io/pub/v/windows_gpu_recovery.svg)](https://pub.dev/packages/windows_gpu_recovery)\n[![pub points](https://img.shields.io/pub/points/windows_gpu_recovery)](https://pub.dev/packages/windows_gpu_recovery/score)\n[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![platform: Windows](https://img.shields.io/badge/platform-Windows-0078D4?logo=windows)](https://pub.dev/packages/windows_gpu_recovery)\n\nFlutter Windows plugin that recovers from `EGL_CONTEXT_LOST` / `DXGI_ERROR_DEVICE_REMOVED` after system sleep, Hyper-V save/restore, TDR timeout, or GPU driver reset. Without this plugin, the app freezes permanently with a white screen.\n\n\u003e **The recovery does NOT work when a debugger is attached** (VS Code `flutter run`, `F5`, or any external debugger). This is a Win32 debugging model limitation — the debugger intercepts exceptions before the plugin can handle them. **Run the compiled exe directly for testing and production.**\n\n## Demo\n\n\nhttps://github.com/user-attachments/assets/b769f469-1701-487c-9459-facba1dde675\n\n\n\n## What it does\n\nAfter GPU device loss, a Flutter Windows app freezes forever — rendering stops, the window goes white, Windows marks it \"Not Responding\". CPU logic (Dart isolate, timers, method channels) continues working, but no frames are drawn.\n\nThis plugin:\n1. **Detects** GPU device loss via a persistent sentinel D3D11 device (2-second polling)\n2. **Destroys** the dead Flutter engine with crash protection (Vectored Exception Handler catches ANGLE cleanup crashes)\n3. **Recreates** a fresh `FlutterViewController` with new ANGLE display, new D3D device, new Skia context\n4. **Preserves state** — app saves state to SharedPreferences before recreation, restores on startup\n\nThe window stays open. The process stays alive. User sees a brief white flash (~1-3 seconds), then the app is back with restored state.\n\n## Integration\n\n### 1. Add the dependency\n\n```yaml\ndependencies:\n  windows_gpu_recovery:\n    path: ../windows_gpu_recovery  # or git URL\n```\n\n### 2. Modify `windows/runner/flutter_window.cpp`\n\nAdd the `WM_GPU_RECOVERY` handler. This is the only C++ change needed:\n\n```cpp\n#include \u003cwindows_gpu_recovery/gpu_recovery_message.h\u003e\n\n// In FlutterWindow::MessageHandler, BEFORE the Flutter message dispatch:\nif (message == WM_GPU_RECOVERY) {\n  // Write a recovery marker file for the new Dart VM.\n  // (Your state-saving logic here — or use SharedPreferences from Dart side)\n\n  // Destroy old engine — VEH catches ANGLE cleanup crashes.\n  // eglTerminate clears the per-process Display singleton.\n  flutter_controller_.reset();\n\n  // Poll until GPU is ready (adapter recovered).\n  // D3D11CreateDevice every 200ms, max 10 seconds.\n  // (See example_with_recovery for full polling implementation)\n  Sleep(3000);  // simple version\n\n  // Create fresh engine.\n  RECT frame = GetClientArea();\n  flutter_controller_ = std::make_unique\u003cflutter::FlutterViewController\u003e(\n      frame.right - frame.left, frame.bottom - frame.top, project_);\n  RegisterPlugins(flutter_controller_-\u003eengine());\n  SetChildContent(flutter_controller_-\u003eview()-\u003eGetNativeWindow());\n  flutter_controller_-\u003eForceRedraw();\n  return 0;\n}\n```\n\n### 3. Persist state on Dart side\n\nThe Dart VM restarts on recreation (new isolate). SharedPreferences (on disk) survives:\n\n```dart\n// Save state continuously (every significant change):\nfinal prefs = await SharedPreferences.getInstance();\nawait prefs.setInt('counter', _counter);\n\n// On startup, check for recovery marker:\nfinal exeDir = File(Platform.resolvedExecutable).parent.path;\nfinal marker = File('$exeDir/gpu_recovery.marker');\nif (marker.existsSync()) {\n  marker.deleteSync();\n  final savedCounter = prefs.getInt('counter') ?? 0;\n  // Restore state, show banner, etc.\n}\n```\n\n### 4. No Dart-side initialization needed\n\nThe plugin works entirely at the native C++ level. Just adding the dependency is enough — the sentinel device and VEH are set up automatically on plugin registration.\n\n## Testing\n\n\u003e Recovery does NOT work under a debugger. Always test by running the exe directly.\n\n### Triggering GPU device loss\n\n**`dxcap -forcetdr`** (recommended — clean, repeatable):\n```cmd\n:: Run as Administrator. Part of Windows SDK / Visual Studio.\ndxcap -forcetdr\n```\nInstall: `C:\\Program Files (x86)\\Windows Kits\\10\\bin\\\u003cversion\u003e\\x64\\dxcap.exe`.\n\nOther methods:\n- **System sleep** — close laptop lid or Start \u003e Sleep (real hardware only, not VMs)\n- **Hyper-V** — Save VM + Start VM from host (no `WM_POWERBROADCAST` delivered)\n- **Device Manager** — Disable/Enable GPU adapter (not over RDP)\n- **PowerShell** — `Disable-PnpDevice` / `Enable-PnpDevice` (as Administrator)\n\n### A/B test procedure\n\n1. `flutter build windows --release` in both `example` and `example_without_recovery`\n2. Launch both exe files directly (not via VS Code)\n3. Click the counter several times\n4. Run `dxcap -forcetdr` as Administrator\n5. Observe:\n   - **Without plugin** (red theme): frozen white screen, counter lost\n   - **With plugin** (green theme): brief white flash, counter restored, green banner\n\n### Logs\n\nThe `example` app writes to `gpu_recovery.log` next to the exe:\n```\n[GPU_RECOVERY] Sentinel D3D device created\n[GPU_RECOVERY] Plugin initialized\n[GPU_RECOVERY] Watchdog started (2 s interval)\n[GPU_RECOVERY] Device loss detected — activating exception handler\n[GPU_RECOVERY] Posting WM_GPU_RECOVERY\n[GPU_RECOVERY] WM_GPU_RECOVERY received\n[GPU_RECOVERY] Destroying old engine...\n[GPU_RECOVERY] Old engine destroyed\n[GPU_RECOVERY] GPU ready after 200 ms\n[GPU_RECOVERY] Creating new engine...\n[GPU_RECOVERY] Plugin initialized\n[GPU_RECOVERY] Engine recreated successfully\n```\n\n## What survives vs what restarts\n\n```\nSURVIVES (same process):            RESTARTS (new engine):\n  Win32 window (same HWND)            Dart VM (new isolate)\n  C++ process (same PID)              Riverpod/Provider state\n  SharedPreferences (on disk)         Navigation stack\n  FlutterSecureStorage (on disk)      In-memory caches\n  Files, network connections          Stream subscriptions\n```\n\n## Project structure\n\n```\nwindows_gpu_recovery/\n  lib/windows_gpu_recovery.dart           — library declaration (no Dart API needed)\n  windows/\n    windows_gpu_recovery_plugin.cpp       — sentinel device + VEH + watchdog\n    windows_gpu_recovery_plugin.h\n    include/.../gpu_recovery_message.h    — WM_GPU_RECOVERY constant\n    CMakeLists.txt                        — links dxgi, d3d11\n  example/                               — counter app + plugin + state restore\n  example_without_recovery/               — counter app, no plugin (freezes)\n```\n\n---\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eHow it works internally\u003c/b\u003e\u003c/summary\u003e\n\n## Detection: sentinel D3D11 device\n\nA persistent D3D11 device is created on the same DXGI adapter that ANGLE uses (`FlutterDesktopViewGetGraphicsAdapter`). This sentinel device stays alive for the plugin's lifetime. When the GPU resets, `GetDeviceRemovedReason()` on the sentinel returns `DXGI_ERROR_DEVICE_REMOVED` — permanently, even after the adapter recovers (~100ms).\n\nA freshly created test device would succeed (the adapter is back), so polling with temporary devices gives false negatives. The sentinel device shares the fate of ANGLE's internal device — both get the same `DEVICE_REMOVED` status.\n\n## Recovery: VEH-protected engine recreation\n\n### The problem\n\n`flutter_controller_.reset()` calls the engine destructor → `eglTerminate` → ANGLE cleanup → `Renderer11::release()` → tries to `Release()` dead D3D COM objects → `ACCESS_VIOLATION` crash.\n\nWithout crash protection, the process dies. With `.release()` (leak without destructor), `eglTerminate` is never called → the per-process ANGLE `EGLDisplay` singleton stays in `mInitialized = true` state → new engine gets the same dead display.\n\n### The solution\n\nA Vectored Exception Handler (VEH) is installed via `AddVectoredExceptionHandler`. It catches `EXCEPTION_ACCESS_VIOLATION` inside `flutter_windows.dll`'s address range and skips the faulting instruction:\n\n1. Decodes x64 instruction length (handles REX prefixes, ModRM, SIB, displacements)\n2. Advances RIP past the crashing instruction\n3. Sets RAX=0, RDX=0 (fake null/success return values)\n4. Sets EFLAGS ZF=1 (dead pointers treated as null in subsequent conditional jumps)\n\nWith VEH active, `flutter_controller_.reset()` runs the full destructor. ANGLE's cleanup partially crashes (VEH catches), but `eglTerminate` reaches `Display::mInitialized = false` — clearing the singleton. The new `FlutterViewController` calls `eglInitialize`, which sees `mInitialized == false` and performs a full reinitialization: new `Renderer11`, new `D3D11Device`, new EGL contexts.\n\n### Why approach 6 (without VEH) failed\n\nIn approach 6, `.release()` was used to avoid crashes. The engine was leaked, `eglTerminate` never ran, the Display singleton stayed poisoned. The VEH (developed for approach 9's binary patching experiments) turned out to be the critical enabler — it lets the destructor run safely, which clears the singleton.\n\n### Debugger limitation\n\nWhen a debugger is attached, Windows sends first-chance exceptions to the debugger **before** the VEH. The Dart debug adapter treats any unhandled exception as fatal and terminates the process. This is a Win32 debugging model constraint, not a plugin bug. The plugin works correctly when no debugger is attached.\n\n\u003c/details\u003e\n\n---\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eInvestigation log — for those who enjoy a detective story\u003c/b\u003e\u003c/summary\u003e\n\n## The problem\n\nFlutter Windows applications become permanently unresponsive after any event that triggers a D3D11 device removal. The Flutter engine has no recovery path. Issue [flutter#124194](https://github.com/flutter/flutter/issues/124194) has been open since April 2023 at P2 priority with no fix or assigned engineer.\n\nWe conducted an exhaustive investigation: 11 approaches, 5 binary patches to `flutter_windows.dll`, a custom x64 instruction length decoder, a Vectored Exception Handler, and interactive reverse engineering with x64dbg.\n\n## The rendering stack\n\n```\nLayer 7: Vsync scheduler (DWM)     — DEAD (swap chain gone)\nLayer 6: Flutter frame scheduling   — IDLE (no vsync = no frames)\nLayer 5: Skia GrDirectContext       — ABANDONED (auto-detected)\nLayer 4: EGL Context (mContextLost) — PERMANENTLY LOST\nLayer 3: EGL Display (mDeviceLost)  — PERMANENTLY LOST\nLayer 2: ANGLE Renderer11 (D3D11)   — REMOVED\nLayer 1: D3D11 adapter (hardware)   — RECOVERED in ~100ms\n```\n\n## Approach 1: InvalidateRect\n\nForce repaint. Result: `eglMakeCurrent` returns `EGL_CONTEXT_LOST`. Can't paint with dead context.\n\n## Approach 2: WM_POWERBROADCAST + WM_SIZE\n\nIntercept resume from sleep, force resize. Result: swap chain resized but EGL context is dead. Also, `WM_POWERBROADCAST` not delivered in Hyper-V.\n\n## Approach 3: RunOnSeparateThread\n\n`UIThreadPolicy::RunOnSeparateThread`. Result: fixes performance regression (#178916), not context loss.\n\n## Approach 4: SW_HIDE/SW_SHOW on Flutter child HWND\n\nPlugin accesses `FlutterDesktopViewGetHWND`. Hide/show triggers surface recreation. Result: new surface on dead context → still fails.\n\n## Approach 5: Sentinel D3D11 device\n\nCreate persistent device on ANGLE's adapter, poll `GetDeviceRemovedReason()`. **Works for detection.** Temporary test devices give false negatives (adapter recovers in ~100ms).\n\n## Approach 6: FlutterViewController recreation\n\nDestroy old controller, create new. Finding: `.reset()` crashes (ANGLE cleanup). `.release()` leaks but new engine gets same dead `EGLDisplay` singleton. Dead end without VEH.\n\n## Approach 7: Binary patching — ANGLE recovery (5 patches)\n\nANGLE has `restoreLostDevice()` infrastructure. Blocked by two checks:\n\n**Patches 1-2:** NOP `isResetNotificationEnabled` check in `restoreLostDevice`:\n```\nPattern: 89 c1 b0 01 ba 0e 30 00 00 84 c9 75\nPatch:   75 XX → 90 90\n```\n\n**Patches 3-5:** Skip `isContextLost` checks in `ValidateContext` (block before `ValidateDisplay`):\n```\nPatch 3: 0x00b33247: 74 → EB\nPatch 4: 0x00b3585b: 74 → EB\nPatch 5: 0x00b387f5: 74 → EB\n```\n\nResult: `restoreLostDevice` reached but `Renderer11::resetDevice()` → `release()` crashes on dead COM objects.\n\n## Approach 8: ForceRedraw loop\n\nAfter device loss, DWM stops vsync. `ForceRedraw` bypasses via `ScheduleFrame`. Result: frames scheduled but all fail on dead ANGLE context. With patches, `release()` crashes.\n\n## Approach 9: Vectored Exception Handler\n\nCustom x64 instruction decoder catches ACCESS_VIOLATION inside `flutter_windows.dll`. Skips faulting instructions with ZF=1. Result: 300+ AVs caught, app stays alive, but `release()` + `initialize()` compiler-inlined → skipping release skips initialize too.\n\n## Approach 10: x64dbg reverse engineering\n\nFound `GpuSurfaceGLSkia::AcquireFrame` by error string reference. Traced to `GrDirectContext*` at `[this+0x10]`. Found `fAbandoned` at `[sub+0xD8]` — already true. Found `releaseResourcesAndAbandonContext` — never called. Manually invoked via register injection — early return (already abandoned). Nobody creates replacement `GrDirectContext`.\n\n## Approach 10b-c: Code cave + JMP patch\n\nFound `release()` + `initialize()` inlined into 630-byte function. Crash at +431 (`cmp [r13+0x38E0], 0`). Found conditional `je +0x23` at +424 that skips to initialize. Patched to `jmp` (one byte). Result: release skipped, but initialize reads dead Renderer11 fields → VEH returns 0 → `D3D11CreateDevice` gets wrong params.\n\n## 9 barriers deep\n\n| # | Barrier | How we broke it |\n|---|---------|----------------|\n| 1 | restoreLostDevice blocked | Patch: NOP jnz |\n| 2 | isContextLost blocks first | Patch: je → jmp |\n| 3 | Vsync dead | ForceRedraw loop |\n| 4 | release() crashes | VEH |\n| 5 | release+initialize inlined | Patch: skip release |\n| 6 | initialize reads dead fields | VEH returns 0 |\n| 7 | Skia auto-abandoned | x64dbg confirmed |\n| 8 | EGLDisplay singleton | ANGLE architecture |\n| 9 | Renderer11 layout unknown | No PDB/RTTI |\n\n## Approach 11: The breakthrough\n\nInstead of fixing ANGLE from below (9 barriers), attack from above: destroy the entire engine and create a new one. Approaches 6 failed because `.release()` skipped `eglTerminate`. But the VEH from approach 9 lets `.reset()` run safely — the destructor calls `eglTerminate`, which clears `Display::mInitialized`, which lets the new engine reinitialize ANGLE from scratch.\n\nThe VEH that failed for approach 9 (couldn't fix ANGLE internally) became the critical enabler for approach 11 (protects the destructor during engine recreation).\n\n\u003c/details\u003e\n\n---\n\n## What a proper Flutter engine fix would look like\n\nThis plugin is a workaround — it restarts the Dart VM. A proper engine fix (~200 lines) would preserve the VM:\n\n1. `egl/surface.cc` — detect `EGL_CONTEXT_LOST` specifically, signal to engine\n2. `egl/manager.cc` — add `Reinitialize()` with SEH-protected `eglTerminate`\n3. `flutter_windows_engine.cc` — `HandleDeviceLost()`: suspend raster thread, `GrDirectContext::abandonContext()`, reinitialize EGL, recreate compositor\n4. `flutter_window.cc` — handle `WM_POWERBROADCAST`\n\n## References\n\n- [flutter#124194](https://github.com/flutter/flutter/issues/124194) — App freezes when GPU disabled (P2, open, no fix)\n- [flutter#88649](https://github.com/flutter/flutter/issues/88649) — Desktop app freezes after sleep (closed, no fix)\n- [flutter#163732](https://github.com/flutter/flutter/issues/163732) — Black/white screen after sleep in Parallels\n- [ANGLE commit (2016)](https://chromium.googlesource.com/angle/angle/+/2823) — \"device-loss at the display level is non-recoverable\"\n- [ANGLE Renderer11.cpp](https://chromium.googlesource.com/angle/angle/+/refs/heads/main/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp)\n- [Microsoft: Handle device removed scenarios](https://learn.microsoft.com/en-us/windows/uwp/gaming/handling-device-lost-scenarios)\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fditrixnew%2Fwindows_gpu_recovery","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fditrixnew%2Fwindows_gpu_recovery","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fditrixnew%2Fwindows_gpu_recovery/lists"}