Unreal Engine Archives Explorer
Go to file
Rob Trame 4da1449515
feat(dispatcher): Replace Application.Current.Dispatcher with Avalonia UIThread [P2-010] (#78)
* feat(dispatcher): replace Application.Current.Dispatcher with Avalonia UIThread

Migrate all Application.Current.Dispatcher.Invoke() and BeginInvoke()
calls to Avalonia.Threading.Dispatcher.UIThread equivalents across the
three remaining affected files. CustomRichTextBox.cs was already migrated.

AudioPlayerViewModel.cs (7 calls):
- Remove 'using System.Windows;' (only used for Application)
- Add 'using Avalonia.Threading;'
- Replace all Application.Current.Dispatcher.Invoke(() => {...}) with
  Dispatcher.UIThread.Post(() => {...}) in Load(), AddToPlaylist(x2),
  Remove(), Replace(), SavePlaylist(), Dispose().
  Post() is appropriate because all callers are fire-and-forget
  cross-thread UI collection mutations.

CUE4ParseViewModel.cs (3 calls):
- Remove 'using Application = System.Windows.Application;'
- Add 'using Avalonia.Threading;'
- SnooperViewer getter: extract local function MakeSnooper() and use
  Dispatcher.UIThread.CheckAccess() ? MakeSnooper() :
  Dispatcher.UIThread.InvokeAsync(MakeSnooper).GetAwaiter().GetResult()
  Avoids deadlock when accessed from UI thread (MenuCommand), while
  still dispatching correctly when called from background thread.
- FindReferences(): Dispatcher.UIThread.Post(delegate {...})
- Audio player window: Dispatcher.UIThread.Post(delegate {...}) and
  remove the stale TODO comment (Post already solves the thread-lifetime
  issue the comment described).

Snooper.cs (1 call):
- Keep 'using Application = System.Windows.Application;' — still
  needed for Application.GetResourceStream in LoadWindowIcon().
- Add 'using Avalonia.Threading;'
- Run(): Dispatcher.UIThread.Post() so the background thread is not
  blocked for the lifetime of the game loop window.

Closes #25

* fix(dispatcher): address review findings and complete migration

Fix [C1] — Load() wrapped in Post() silently broke every Load→Play call:
  All callers of Load() are already on the UI thread (ICommand.Execute,
  key handlers, or inside an enclosing Post lambda). Wrapping Load() in
  Post() deferred it past the immediately-following Play() call, so Play()
  always found _soundOut == null. Fix: remove the Post wrapper and run
  Load() synchronously. The WPF Dispatcher.Invoke was a no-op re-entry
  from the dispatcher thread anyway; Post() does not have that property.

Fix [M1] — Dispose() as Post() risked leaked resources and a timer race:
  (a) _sourceTimer ticks every 10 ms on a threadpool thread. With deferred
  disposal, there was a window many ticks wide where _waveSource was being
  disposed concurrently with a tick accessing it (use-after-dispose).
  (b) If the Avalonia event loop shut down before processing the queued
  Post (e.g. last window closing), _waveSource/_soundOut would never be
  disposed. Fix: stop the timer synchronously with Change(Infinite,Infinite)
  before touching any audio resources; then dispose inline (Dispose() is
  always called from OnClosing, which fires on the UI thread).

Fix [M2] — Snooper.Run() was posting the blocking GLFW game loop to the
  Avalonia UI thread, freezing the entire UI for the lifetime of the 3D
  viewer. The Win32 shared message-pump trick that made this work on WPF
  does not apply to Avalonia on Linux. Fix: post only IsVisible=true to
  the UI thread (GLFW requires glfwShowWindow on the main/creating thread),
  set GLFW.SetWindowShouldClose (thread-safe per GLFW docs) from the
  calling thread, then start base.Run() on a new IsBackground thread named
  'Snooper-GameLoop'. OpenTK transfers the GL context to that thread via
  Context.MakeCurrent() at the start of Run().

Fix [m1] — SnooperViewer getter had no lock: two concurrent background
  threads could both see _snooper==null, both InvokeAsync MakeSnooper,
  and the second call would overwrite and leak the first Snooper. Fix:
  volatile field + double-checked lock (_snooperLock) around the slow
  path. The CheckAccess() fast path (UI thread caller) never touches the
  lock so there is no deadlock.

Expand scope to all remaining Application.Current.Dispatcher usages
  (addresses reviewer comment on PR #78):
  - BackupManagerViewModel.cs  (1 usage → Post)
  - AssetsFolderViewModel.cs   (CheckAccess+Invoke pattern → CheckAccess+
                                 InvokeAsync.GetAwaiter().GetResult(); BulkPopulate → Post)
  - TabControlViewModel.cs     (8 usages → Invoke for synchronous ops)
  - GameDirectoryViewModel.cs  (1 usage → Post)
  - Renderer.cs                (3 usages → Invoke)
  - EndpointEditor.xaml.cs     (1 usage → Post)
  GameFileViewModel.cs is intentionally excluded: its Dispatcher usage is
  inseparable from System.Windows.Media.ImageSource/BitmapImage types
  that require a separate migration ticket.

* fix(snooper): dispatch IsVisible to UI thread from game loop thread

After [M2] moved base.Run() off the UI thread to Snooper-GameLoop,
all game loop callbacks — OnClosing, key/button handlers, animation
freeze points — fire on that background thread, making every IsVisible
assignment a thread safety violation (glfwShowWindow and glfwHideWindow
must be called from the GLFW main thread, which is the Avalonia UI
thread that created the window).

GLFW.SetWindowShouldClose remains inline in both WindowShouldClose and
WindowShouldFreeze: it is explicitly documented as callable from any
thread.

IsVisible is now assigned inside Dispatcher.UIThread.Post() in both
methods. Post (not Invoke) is correct: no caller checks or depends on
the visibility change completing synchronously.
2026-03-15 18:11:20 -06:00
.github chore: update identity, branding, and CI for FModel Linux fork (#69) 2026-03-12 11:13:00 -06:00
.vscode Set up dev env 2026-03-11 15:51:34 -06:00
CUE4Parse@06fbf1aced chore: point CUE4Parse submodule at r6e/CUE4Parse-Linux (#66) 2026-03-11 19:49:17 -06:00
FModel feat(dispatcher): Replace Application.Current.Dispatcher with Avalonia UIThread [P2-010] (#78) 2026-03-15 18:11:20 -06:00
.editorconfig chore: update identity, branding, and CI for FModel Linux fork (#69) 2026-03-12 11:13:00 -06:00
.gitignore Finish setting up env 2026-03-11 17:28:01 -06:00
.gitmodules chore: point CUE4Parse submodule at r6e/CUE4Parse-Linux (#66) 2026-03-11 19:49:17 -06:00
LICENSE Re-add GPL-3 license 2021-05-22 16:33:08 -04:00
NOTICE chore: update identity, branding, and CI for FModel Linux fork (#69) 2026-03-12 11:13:00 -06:00
README.md chore: update identity, branding, and CI for FModel Linux fork (#69) 2026-03-12 11:13:00 -06:00

FModel Linux — Unreal Engine Archives Explorer for Linux

CI Status Latest


This is an unofficial Linux port of FModel, originally created by Asval (4sval) and contributors. It is not affiliated with or endorsed by the upstream project or its maintainers. Please do not report Linux-port-specific issues upstream.

Description

FModel Linux is a Linux port of FModel — an archive explorer for Unreal Engine games. It uses CUE4Parse as its core parsing library, providing robust support for the latest UE4 and UE5 archive formats, along with a comprehensive set of tools for previewing and converting game packages.

This fork replaces the Windows-only WPF UI stack with Avalonia UI and removes other Windows-specific dependencies to enable native Linux support. It is maintained by r6e.

Installation

Installation instructions for the Linux port are available in the project wiki (work in progress).

For the upstream Windows release, refer to the official FModel installation guide.

Supporting the Upstream Project

This fork does not accept donations. If you find FModel valuable, please consider supporting the original FModel project and its contributors.

License

FModel Linux is a derivative work licensed under GPL-3. The original FModel project is copyright © Asval and FModel contributors. Licenses of third-party libraries used are listed in NOTICE.