Purpose: Fast-loading reference for AI coding agents. Read this before operating on the codebase.
- Name: Animura — modular live wallpaper engine for Windows
- Language: C++20 (host), TypeScript + React 18 (frontend)
- Build: CMake 3.30+, MSVC 2022, static CRT (
/MT), Vite 5 - Platform: Windows only (Win32 + WebView2 + Qt 6)
Animura/
├── src/ # Host C++ sources (includes desktop integration)
├── include/animura/ # Host headers (includes desktop integration)
├── frontend/ # React + TypeScript UI (Vite)
│ └── src/
│ ├── components/ # React components
│ ├── bridge/ # C++ ↔ JS communication layer
│ ├── hooks/ # Custom React hooks
│ └── theme/ # CSS design tokens
├── modules/ # Wallpaper plugin DLLs
└── resources/ # .qrc (icon only), .rc, icon.ico
Module sources live separately at E:/coding/C/live-wallpaper-modules/<name>/.
| Class | File | Role |
|---|---|---|
WallpaperController |
src/WallpaperController.cpp |
Central orchestrator — module lifecycle, worker thread |
WebView2Host |
src/WebView2Host.cpp |
Embeds Edge WebView2 in a QWidget, hosts React UI |
NativeBridge |
src/NativeBridge.cpp |
COM IDispatch object — JS ↔ C++ communication |
ModuleCatalog |
src/ModuleCatalog.cpp |
Scans /modules for module.json files |
ModuleLibrary |
src/ModuleLibrary.cpp |
Wraps LoadLibraryEx/FreeLibrary/GetProcAddress |
IWallpaperModule |
include/animura/IWallpaperModule.hpp |
ABI interface: run(), stop(), hwnd(), virtual dtor |
ZipExtractor |
src/ZipExtractor.cpp |
ZIP read & extract via minizip-ng (module installation) |
SettingsSchemaValidator |
src/SettingsSchemaValidator.cpp |
Recursive JSON validation against schema |
AppSettings |
src/AppSettings.cpp |
Application-scoped persistent settings (singleton) |
Paths |
src/Paths.cpp |
Standardized filesystem paths via SHGetKnownFolderPath |
DesktopHostResolver |
src/DesktopHostResolver.cpp |
Probes shell window hierarchy for the correct WorkerW/Progman host |
WallpaperHost |
src/WallpaperHost.cpp |
RAII-style Attach(hwnd)/Detach() for wallpaper module windows |
SystemWallpaperBridge |
src/DesktopWallpaper.cpp |
Saves/restores static desktop wallpaper via COM + registry fallback |
Win32Utils |
src/Win32Utils.cpp |
Free functions for window style sanitization, geometry, transparency |
-
Rendering library thread affinity is LAW.
- Module initialization, window creation, context management, rendering, and teardown must all happen on the same thread.
- This applies to common libraries: GLFW (
glfwInit/glfwTerminate), OpenGL contexts, Direct3D devices, Vulkan instances. - The worker thread (
m_worker) is the rendering thread. The Qt main thread is NOT. - Never call rendering functions from the main thread.
-
Module lifecycle contract:
createModule(json)→run()→stop()→~IWallpaperModule()stop()only sets an atomic flag. It does not block or join.run()is a blocking call — it returns only afterstop()signals exit.- Module destruction MUST happen on the same thread that created it.
-
DLL loading order:
LoadLibraryEx→GetProcAddress("createModule")→ call factory → store resultm_module.reset()destroys the module objectFreeLibraryhappens later inModuleLibrary::unload()- DLL must NOT be freed while any code inside it is executing.
-
WebView2 threading:
- WebView2 callbacks arrive on arbitrary threads.
NativeBridge::Invoke()marshals all calls to the Qt main thread viaQMetaObject::invokeMethodwithQt::BlockingQueuedConnection.PostWebMessageAsJsonis thread-safe.
-
COM bridge return types:
- All NativeBridge methods return
BSTR(COM string). JS receives strings, not numbers. getRunningModuleId()returns a string like"0"— must be parsed withNumber()in JS.- Strict equality (
===) between string and number always fails.
- All NativeBridge methods return
Main Thread (Qt): UI events, WallpaperController public methods,
WebView2Host, NativeBridge marshaling,
m_module->stop(), m_module->hwnd()
WebView2 Threads: Managed internally — callbacks arrive here,
marshaled to main thread via NativeBridge
Worker Thread (m_worker): Module creation, rendering init, window creation,
mainLoop(), rendering, teardown, module destruction
class IWallpaperModule {
public:
virtual ~IWallpaperModule() = default;
virtual void run() = 0;
virtual void stop() = 0;
virtual HWND hwnd() const = 0;
};
extern "C" __declspec(dllexport) IWallpaperModule* createModule(const char* settingsJson);
- ❌ Do NOT assume
m_module.reset()is safe to call from any thread - ❌ Do NOT assume rendering libraries are thread-safe (many have strict thread affinity)
- ❌ Do NOT assume
stop()blocks untilrun()returns - ❌ Do NOT assume the module DLL is unloaded immediately after
m_module.reset() - ❌ Do NOT assume rendering objects can be deleted without a current context
- ❌ Do NOT assume all modules use the same CRT or compiler settings
- ❌ Do NOT assume all modules use the same rendering library
- ✅ The host uses static CRT
/MT— verify modules match
# Frontend (run before C++ build)
cd frontend && npm run build
# C++ — configure
cmake --preset vs2022-x64
# C++ — build
cmake --build build --config Release
App Start → main.cpp → QApplication → WebView2Host (creates QWidget + WebView2)
→ WallpaperController (ctor scans /modules)
→ Paths::init() → AppSettings::initialize() (loads %LOCALAPPDATA%/Animura/settings.json)
→ (if restoreLastWallpaper) find module by lastUsedWallpaperID → startWallpaper(idx)
→ WebView2 navigates to frontend/dist/index.html (production) or localhost:5173 (dev)
→ React renders module grid via NativeBridge.GetModulesList()
User clicks module card → SettingsPanel slides in
User clicks Start:
→ NativeBridge.StartWallpaper(idx) [COM → Qt main thread]
→ validate → ensureLibraryLoaded (LoadLibraryEx)
→ startWorker → spawn thread
→ createModule(json) → m_module.reset(ptr)
→ m_wallpaperHost->Attach(hwnd)
→ m_module->run() [BLOCKS]
→ emit runningModuleChanged → PostWebMessageAsJson → React updates button
User clicks Stop:
→ NativeBridge.StopWallpaper() [COM → Qt main thread]
→ m_wallpaperHost->Detach()
→ m_module->stop() [sets atomic, returns immediately]
→ m_worker.join() [waits for run() to return]
→ m_module.reset() [destroys module]
→ restoreWallpaper()
User clicks Add Module:
→ NativeBridge.InstallModule() [COM → Qt main thread]
→ QFileDialog → WallpaperController::installModuleFromPath(path)
→ ZipExtractor validates, reads module.json in-memory, extracts
→ ModuleCatalog::addModule() → emit modulesChanged()
→ React auto-refreshes module grid