15 KiB
Editor Architecture
High-level layout of the in-process editor binary (crates/editor). For mission and phased work see roadmap.md and mission.md.
Crate boundaries
editor/ egui UI, PIE, BRP, scene I/O, editor-only cameras, hot-reload (dev)
└─ uses ──► game/ GamePlugin shell, re-exports
game_hot/ Hot-reloadable gameplay systems (cdylib; dev)
settings/ assets/project.ron, SimTuning, camera FX profile
shared/ Authoring components, hydration types
sim/ Components + resources (TypeIds stay static)
protocol/ Input intent (network-ready)
Native-only dependencies (rfd, BRP HTTP) stay in editor. settings, shared, sim, and protocol must remain usable from a future WASM client.
Plugin graph (startup order matters)
| Plugin | Role |
|---|---|
GamePlugin |
Shared runtime: rendering, player, sim schedule |
ProjectSettingsPlugin |
Load assets/project.ron, emit changes, sync SimTuning |
ProjectIoPlugin |
User prefs, workspace metadata, window title inputs |
EditorStatePlugin |
EditorMode (Edit/Play), PlayPossession, camera/input gating |
ViewportPlugin |
Grid, focus, bookmarks, snap settings |
EditorCameraPlugin |
Fly camera; FX applied via render_view + viewport_camera sync |
ActorIconsPlugin |
Selectable actor root billboard icons |
EditorVisualizersPlugin |
Collider/light/spawn/prefab/runtime visualizers and selectable proxies |
SceneIoPlugin |
Level New/Open/Save, recent paths |
SettingsUiPlugin |
egui Project Settings panel |
EditorUiPlugin |
Dock, themed egui shell, viewport overlays, menus, status bar |
BrpPlugin |
Bevy Remote Protocol HTTP (editor-only) |
HotReloadPlugin |
Dev-only: game_hot dylib watch + status bar (--features dev) |
Hot reload (dev)
Gameplay system bodies live in game_hot as a cdylib. Stable component/resource TypeIds remain in sim / protocol / shared / settings so ECS inserts stay valid after reload (see ADR 0004).
| Piece | Location |
|---|---|
| Hot system implementations | crates/game_hot/src/ |
| Schedule registration + hot wrappers | crates/game/src/lib.rs (hot-reload feature) |
| Dylib poll, notify watcher, Ctrl+Shift+R | crates/editor/src/hot_reload.rs |
| Status bar label | ui/status_bar.rs |
Workflow: run cargo watch … build -p game_hot in one terminal, cargo run -p editor --features dev in another. Logic-only edits apply without restarting the editor; component layout changes still need a full restart.
Settings split (maintainability)
| Concern | Location |
|---|---|
| Serializable project data | crates/settings — ProjectSettings, save/load |
| Path + dirty flag | ProjectSettingsIo resource |
| Panel open/close | ProjectSettingsPanel in settings_ui.rs |
| Live apply to sim/rendering | ProjectSettingsChanged message (draft edits commit on Apply) |
Do not add egui or editor-only state to the settings crate.
Camera and viewport model
See ADR 0014 for the unified viewport decision.
Three camera roles coexist:
- Editor fly camera (
EditorCamera) - active while editing or ejected from PIE. - Player camera - active while playing + possessed.
- egui overlay camera (
PrimaryEguiContext) - full-windowCamera2d; never viewport-cropped. Attaching egui to a cropped 3D camera causes NaN layout panics.
Unified viewport: one docked Viewport tab owns the primary offscreen HDR target (ViewportRenderTarget). Edit mode and Play-ejected bind the editor fly camera to that target; Play-possessed binds the player camera to the same target. Saved layouts with the old Game View tab are migrated to one Viewport tab.
WYSIWYG rendering: render_view.rs is the only editor system that mutates viewport post-FX. It calls game_hot::sync_viewport_camera_stack on the active camera (editor fly or possessed player), driven by ActiveCameraRenderProfile from project settings and volumes plus EffectiveRenderStack for requested/effective GI fallback. scene_view and play/session set HDR render targets and is_active only—they do not strip FX. Only one camera carries the stack at a time (mesh-view bind group limits). See ADR 0015 and ADR 0016.
The viewport uses render-to-texture: the active camera renders to an offscreen HDR target sized to the dock panel (panel_physical_size()), then egui displays that texture. Cameras use viewport = None on image targets so the scissor always matches the texture; panel size only drives texture allocation, not a sub-viewport on the image.
HDR invariant: Atmosphere, authored HDR, and effective Solari all require an Hdr
camera target; they must never render to the primary window swapchain
(Rgba8UnormSrgb). When the HDR offscreen target is missing, scene_view / play
deactivate cameras; render_view skips stack sync until a target exists.
Render-target changes and Solari/Forward fallback changes force a full camera-stack rebuild so
stale sky or post-FX pipelines cannot survive across texture-format transitions.
Viewport picking: LMB selection maps the egui panel UV through scene_view_ray and raycasts with MeshRayCast. Object pick runs in Last after transform_gizmo_bevy so gizmo drags take priority. GizmoOptions.viewport_rect is synced from the viewport panel rect so gizmo hit-testing matches render-to-texture layout. Actor root icon and visualizer proxy hits resolve to their source entity, so clicking an icon or light/spawn/collider marker selects the authored or runtime object, not the helper. Actor icon hits are ordered ahead of ordinary mesh hits when both are under the cursor. In Edit mode, selecting the runtime Player redirects to an authored PlayerSpawn (Player Start), creating one if needed.
Selection overlay: selection_outline.rs draws a shared unit-box shell per selected LevelObject (bounds from Primitive, physics collider, or mesh AABB), scaled to fit. Custom shader (assets/shaders/selection_outline.wgsl) uses rim-only fresnel with two cheap depth passes: Greater (soft cyan x-ray) and LessEqual (amber edge). Sync runs only when selection changes. Editor-only.
Clean game-view overlay: ViewportDisplayMode.clean_game_view hides editor chrome, grid, selection outlines, transform gizmos, actor root icons, visualizers, and selectable proxy meshes without changing camera ownership. G toggles this mode; Ctrl+G toggles the grid.
Actor root icons: actor_icons.rs registers the editor PNG icon set as internal binary image assets and spawns screen-stable, camera-facing editor-only billboards at LevelObject roots. A custom overlay material draws them unlit with always-pass depth so opaque meshes do not obstruct them. The billboards are RaytracingExcluded, hidden with clean game-view, filtered from hierarchy and scene serialization, and expose per-category viewport options for actors, meshes, lights, physics, gameplay, volumes, prefab/model anchors, warnings, and icon size.
Scene visualizers: visualizers.rs draws transient gizmo lines for authoring components without obvious meshes: colliders, lights, PlayerSpawn, prefab/model anchors, Project Sun, and runtime player/camera markers. Small editor-only proxy meshes make those markers selectable but are filtered from hierarchy and scene serialization. Player/start markers use the runtime capsule constants from sim::tuning.
World lighting: Project settings provide default ambient light and a runtime ProjectSun. A scene-authored directional LightDesc is treated as a scene sun override; when present, sync_project_sun_visibility hides/disables the project sun to avoid duplicate directional lighting.
Editor UI shell
The egui layer lives under crates/editor/src/ui/:
| Module | Role |
|---|---|
mod.rs |
EditorUiPlugin, UiState, dock host, tab titles |
theme.rs |
Unity-like dark pro palette + egui_dock chrome |
fonts.rs |
Phosphor icon font on PrimaryEguiContext |
widgets.rs |
Icon buttons, tool toggles, menu helpers |
asset_browser.rs |
Project/file browser with tree, breadcrumb, filters, list/grid modes, embedded model shelves, details/import settings |
menu.rs |
Top menu bar with shortcuts |
viewport_chrome.rs |
Unified viewport RTT + overlay toolbars |
actor_icons.rs |
Screen-stable actor root icons and selectable icon proxies |
visualizers.rs |
Scene visualizer settings, gizmo drawing, selectable proxies |
toolbar.rs |
Bottom dock icon toolbar |
status_bar.rs |
Fixed bottom status strip |
hierarchy.rs / asset_browser.rs / inspector.rs |
Panel bodies |
diagnostics.rs |
Detailed stats (Window → Diagnostics, Asset Browser footer) |
layout.rs |
Dock layout RON persistence in editor_prefs.ron |
The Viewport fills the tab (no inline help text). Gizmo/grid/play controls sit on a semi-transparent overlay, and G hides editor-only overlays for a clean game view. A blue status bar shows scene path, mode, selection count, and history. Dock layout restores from ~/.config/bevy-fps/editor_prefs.ron on startup; View → Reset Layout restores defaults.
Follow-up (Phase 4b): hierarchy expand/collapse persistence, inspector search, and richer per-asset previews.
PIE state machine
stateDiagram-v2
[*] --> Editing
Editing --> Playing_Possessed: F5 enter play
Playing_Possessed --> Playing_Ejected: F8 eject
Playing_Ejected --> Playing_Possessed: F8 possess
Playing_Possessed --> Editing: F5 stop
Playing_Ejected --> Editing: F5 stop
| Resource | Editing | Playing possessed | Playing ejected |
|---|---|---|---|
SimEnabled |
false | true | true |
GameInputEnabled |
false | true | false |
| Editor camera active | true | false | true |
| Player camera active | false | true | false |
On play enter/exit the editor snapshots player sim state only (transform, velocity, jump timers). Authored scene entities are not rolled back so in-play edits persist when stopping PIE.
Scene authoring flow
- User edits entities with
LevelObject+ reflectable components fromshared. EditorOnlyentities (cameras, helpers) are filtered from hierarchy and save; visualizer proxies can be picked but resolve back to source entities.- Player placement is stored as
PlayerSpawn; the runtimePlayeris never serialized. SceneIowrites native Bevy dynamic scenes underassets/levels/, routed through the Bevy-freescene::document::SceneDocumentseam. The document layer preserves the current RON storage while exposing stable actor/component document concepts and patch vocabulary for tools.- Hydration systems in
gamespawn meshes, colliders, lights, and static mesh renderer parts at runtime.ActorIdandComponentInstanceIdare the persisted identities; raw BevyEntityIDs are runtime-only and must not become tool-facing document IDs.
Model import (glTF + FBX)
- Import: File → Import Assets copies glTF/GLB/FBX into
assets/models/. FBX imports also copy a sibling{name}.fbm/folder when present (embedded textures). - Processing: the asset registry generates normalized static mesh manifests under
assets/meshes/generated/. The manifests store stable part IDs, glTF/FBX mesh/material subasset labels, source metadata, dependencies, and import settings. These artifacts are hidden from the Asset Browser catalog. - Browser subassets: model rows can expand into a content shelf backed by the generated manifest. Mesh subassets can be selected or dragged into the viewport as
StaticMeshRendereractors, material subassets expose source defaults, and texture dependencies can be applied to selected actors without treating glTF and FBX differently. The thumbnail studio renders each generated model/subasset/material preview into its own render target before registering it in the cache so later thumbnails cannot overwrite earlier cache entries. - Default placement: model drag/drop creates
ActorKind::StaticMesh + StaticMeshRenderer. Renderer slots reference importedEditorAssetRefvalues (asset_id+sub_asset_id) and hydration resolves those through the generated artifact.SingleActorhierarchy mode stores all parts in one renderer;SourceHierarchycreates a root with child static mesh actors. - Collision: when collider generation is enabled, placement adds
RigidBodyDescandColliderDesc::StaticMeshusing the same imported mesh refs. Renderer slots no longer own collider state. - Scene-instance placement: asset details can switch placement to
SceneInstance; that keepsImportedModel + ModelRef, which hydrates toSceneRoot. glTF usesGltfAssetLabel::Scene; FBX usesbevy_ufbx(path#SceneN).FbxPluginis registered inGamePlugin. - Limitations: Binary FBX only (not ASCII). Static mesh placement stores animation/skinning/camera/light metadata and warnings but does not play those features; use Scene Instance for full-scene playback.
Undo / history
Structural edits (spawn, delete, transform, rename) go through EditorHistory command objects in history.rs.
PIE stop restores player simulation state only; authored LevelObject edits made during PIE remain in the scene.
Editor crate layout (ADR 0008)
crates/editor is a library + binary. EditorPluginGroup in lib.rs registers plugins in a fixed order. Domain modules:
| Directory | Responsibility |
|---|---|
scene/ |
Level I/O, schema plugin, viewport RTT setup |
viewport/ |
Camera, selection, gizmos, render views, panel settings |
play/ |
PIE session, editor mode, net editor profiles |
assets/ |
Catalog, asset DB, static mesh artifacts, prefab overrides |
project/ |
Workspace, project I/O, settings UI |
ext/ |
Command palette, BRP, game panel adapters |
history/ |
Undo commands + plugin |
ui/ |
egui dock shell |
Shared scene schema lives in crates/scene (stamp/migrate/validate on save, load, and CI).
HDR / swapchain invariant
The unified viewport renders through an HDR offscreen target (render_target.rs). When the panel target is missing (egui reflow), cameras must strip atmosphere/post-FX before targeting the swapchain. User-authored HDR may be disabled, but atmosphere and effective Solari still force Hdr on the active viewport camera because Bevy sky/Solari pipelines are not safe to carry across SDR targets. render_view::sync_project_render_view runs in PostUpdate after RTT target assignment and reapplies the project FX stack whenever the active panel target handle changes (startup, resize), not only on settings Apply. Project Settings uses a draft buffer; Apply commits after render targets resync.
The workspace patches bevy_render under third_party/bevy_render so Linux wgpu::SurfaceError::Timeout during swapchain texture acquisition logs and skips the frame instead of panicking in prepare_windows. Keep this patch scoped to transient surface acquire timeouts and re-evaluate it during Bevy upgrades.
Extensibility (phase 6)
extensibility.rs provides EditorCommand, EditorPlugin, and a working command palette (Ctrl+P). Game crates register panel hooks via game::editor_ext::editor_panel_setups(); the editor binary wraps them without editing ui/mod.rs (ADR 0007, dogfood: FPS Demo panel, Ctrl+Shift+G).