Skip to content

Architecture

Deepratna Awale edited this page Oct 6, 2026 · 5 revisions

Architecture

A summary of docs/architecture.md: how Open Wallpaper Engine is divided into modules, which way dependencies point, how a scene flows from files to pixels, and how one wallpaper runs on several displays.

Big picture

┌──────────────────────── App shell (SwiftUI/AppKit) ────────────────────────┐
│ App/  Library/  Workshop/  Settings/  UI/            (views + view models) │
└───────────────┬───────────────────────────────┬────────────────────────────┘
                │ WEWallpaper                    │
      ┌─────────▼─────────┐  ┌─────────────┐  ┌──▼──────────┐
      │ Scene/  (engine)  │  │ Video/      │  │ Web/        │
      │  Format  → Values │  │ AVPlayer +  │  │ WKWebView + │
      │  Shaders → Render │  │ music sync  │  │ WE web API  │
      │  Scripting, Audio │  └──────┬──────┘  └─────────────┘
      └─────────┬─────────┘         │
                └──────────┬────────┘
                    ┌──────▼──────┐
                    │ Audio/      │  system capture (process tap or SCK), item taps
                    └─────────────┘
Core/  logging, diagnostics, settings store, asset locations: usable by everything above

Dependency direction: arrows point down only. Scene/ never references UI/, Library/, Settings/ views or AppDelegate. Core/ depends on nothing else in the app. The engine (Scene/, Audio/, Core/) is meant to become a local Swift package later, so it can be unit-tested and run headless; the rules make that a move, not a rewrite.

Modules

Folder Responsibility
Core/ Logging (OWELog, signposts, frame metrics), the settings model and store, the WE assets location, the Objective-C exception catcher, the instance registry, the Keychain
Scene/Format/ Decode WE files into plain Swift models, with no side effects: scene.json, project.json, effect.json, materials, models, particles, .pkg, .tex
Scene/Values/ Resolve every dynamic value the same way: literal, {"user":…}, {"user":{"name","condition"}}, {"script":…}, {"animation":…}
Scene/Shaders/ GLSL → SPIR-V → MSL translation, reflection, the variant cache, the pipeline archive, engine combos
Scene/Rendering/ Metal: layers, the effect pass graph, render targets, text, particles, models, lights, the camera
Scene/Scripting/ The SceneScript runtime (JavaScriptCore), one per wallpaper instance on its own thread; Host/ ties it to the renderer
Scene/Loading/ Turns a wallpaper into render content: loads, resolves and builds
Scene/Sound/ Sound layers, mixing and spatialisation
Scene/UI/ Scene-specific SwiftUI (the inspector, user properties); the only scene files allowed to import SwiftUI views
Audio/ System audio capture with a restart lifecycle, per-player taps, the spectrum; one producer, many consumers
Video/, Web/ Each type's player, view, view model and features
Library/ The library model, import paths, playlists, installed listing, property scopes
Workshop/ SteamCMD, the Workshop API, downloads, dependencies, the assets installer
Settings/, UI/, App/ Settings pages; the main window and shared components; entry point, AppDelegate, windows, menus, playback monitoring, safe restart

Each view model lives next to its view. No Wallpaper Engine files ship in the repository or the app; Core/WallpaperEngineAssets resolves them at run time (see Assets system).

Scene data flow

  1. Load. Scene/Format decodes project.json, scene.json (from disk or the .pkg), then models, materials, effects and textures.
  2. Resolve. Scene/Values binds user properties (per wallpaper), scripts and animations to typed values. Nothing downstream reads raw JSON.
  3. Build. Scene/Loading produces the render content: an ordered layer list in authored order, each layer with its parent transform, effect pass graph, and text, particle and sound state.
  4. Render. Scene/Rendering executes the pass graph each frame through translated WE shaders, with uniforms from reflection, built-ins (g_Time, resolutions, pointer, audio spectrum) and resolved constants.
  5. Script. Scene/Scripting runs once per frame on its own thread. Scripts read and write a shared object table; the renderer feeds it the drawn values before the frame and draws what scripts wrote after it.

Details: Scene pipeline.

Wallpaper instances

A wallpaper runs once, however many displays show it with the same user properties.

  • Registry. WallpaperInstanceRegistry holds running instances keyed by WallpaperInstanceKey (folder, file, type and the property store). Each display holds a WallpaperInstanceLease; an instance stops one main-queue turn after the last release, so a rebuilt display takes hold again first.
  • Properties per display. Each display has its own store (SceneUserProperties.<identity>.display.<id>), started from the shared one. "Sync properties across displays" makes every display use the shared store. WallpaperPropertyGroups regroups displays whose stores are equal into one instance.
  • Scenes. SceneWallpaperInstance owns the loader, one SceneMetalRenderer and the observers. Each display is a SceneWallpaperPresenter with its own MTKView. With several displays, the one with the highest frame rate drives: the frame renders once at the largest target any display needs, then each display presents it at its own size and placement.
  • AVKit videos. One player per video; each display's view shows it.
  • Web. A WKWebView can't be in two windows, so each display keeps its page; only the audible display's page plays sound.
  • Sound (WallpaperAudioRouting) plays once per running wallpaper.
  • The watchdog gets one frame time per rendered frame of an instance.

Invariants

  • WE semantics, not approximations. WE's shaders and effect definitions are the reference.
  • Per-wallpaper state. State belongs to an instance, never to a process-wide singleton.
  • Loud failure. Failures are logged once, with the wallpaper, layer and reason.
  • Honest caches. Everything derived is keyed on its inputs and the version of the code that produced it.

See also Contributing and WE-fidelity principles.

User guide: What is Open Wallpaper Engine? and Displays and playback rules

Clone this wiki locally