Skip to main content

concinnity_engine/gfx/graphics_system/
mod.rs

1// src/gfx/graphics_system/mod.rs
2//
3// GraphicsSystem: the 3D renderer driver. An internal system (not a declarable
4// asset); `World::start` constructs one when the world declares a
5// `GraphicsConfig`. Deliberately a directory rather than a single file; the
6// system is large enough that splitting it by responsibility is worth it:
7//   mod.rs       struct + System/Debug trait impls (init/step delegate out)
8//   init.rs      run_init: one-time backend + draw-list setup
9//   lines.rs     published world-space lines -> ribbon geometry
10//   frame.rs     run_step: extraction of the frame's draw inputs into the
11//                owned RenderSnapshot (the only per-frame world reads)
12//   submit.rs    replay of one RenderSnapshot onto the backend (no world
13//                access by construction)
14//   streaming.rs texture / normal-map / mesh / voxel-world streaming setup
15//                (the per-frame drive lives in gfx::streaming_system)
16//   scene.rs     scene-flow wiring + scene visibility
17//   helpers.rs   shared free functions
18
19use crate::components::{PostProcessResolve, Window};
20use crate::ecs::asset_id::AssetId;
21use crate::ecs::{PipelineContext, StepResult, System};
22use crate::gfx::backend::RenderBackend;
23use crate::gfx::{scene_flow, text};
24use std::time::Instant;
25
26const IDENTITY4: [[f32; 4]; 4] = crate::gfx::draw_list::IDENTITY4;
27
28// Initialises the GPU backend and draws frame data.
29//
30// Components drained during init():
31//   Window          -- window title, size, and mode
32//   GraphicsConfig  -- frames-in-flight, clear color, max frames
33//   Mesh            -- raw inline geometry payloads (keyed by asset name)
34//   ProceduralMesh  -- generator-built geometry payloads (keyed by asset name)
35//   Model           -- multi-mesh model definitions (keyed by asset name)
36//   Prop            -- scene objects referencing a Mesh/ProceduralMesh or Model
37//   Shader          -- compiled shader payloads (vertex, fragment, instanced)
38//   Texture         -- one or more compiled RGBA texture payloads (keyed by asset name)
39//
40// Components queried (not drained) each step():
41//   Camera3D       -- current view matrix and projection parameters
42//
43// Build process:
44//   Each Mesh is deserialized and kept in a name-keyed map. For each Prop the
45//   corresponding mesh is looked up and appended to the shared vertex/index
46//   buffers; a DrawObject records its slice offsets, model matrix, and texture
47//   slot. One implicit DrawObject is also created for any Mesh that has no Prop
48//   referencing it (e.g. the room itself), placed at the world origin.
49//
50// Input polling + the FrameInput deposit live in InputSystem, scheduled
51// immediately after this system (the OS event pump runs inside draw_frame on
52// Metal, so sampling right after the draw is freshest). Camera3DSystem queries
53// the deposit to update Camera3D, then writes the new view matrix back in time
54// for the next frame, so it runs after both.
55
56// One viewport-pick candidate captured at init: the prop's asset id, its
57// entity (the live GlobalTransform source), and its local-space bounds.
58struct PickCandidate {
59    asset_id: AssetId,
60    entity: crate::ecs::Entity,
61    local_min: [f32; 3],
62    local_max: [f32; 3],
63}
64
65/// Drives the render backend: builds it at init, submits a frame per step.
66pub struct GraphicsSystem {
67    window_args: Window,
68    clear_color: [f32; 4],
69    frames_in_flight: usize,
70    vsync: bool,
71    // Frame-rate cap in FPS (GraphicsConfig.fps_cap; 0 = unlimited). Applied by
72    // the App-level frame pacer, which reads it through the `FrameRateCap`
73    // resource this system publishes (at init and on the settings row's live
74    // change). Held here so the settings row can cycle from the current value.
75    // Independent of the quality preset (a user/hardware preference, like vsync).
76    fps_cap: u32,
77    // The display modes the Resolution row offers, shaped at init from the
78    // backend's enumeration (or the static fallback when it cannot enumerate)
79    // and published once as the `DisplayModes` resource for the dropdown list.
80    display_modes: Vec<crate::gfx::display_mode::DisplayMode>,
81    // The user's chosen fullscreen display mode, persisted as `resolution`.
82    // `None` = never chosen: the display keeps its own mode and the row shows
83    // `current_mode`. Fullscreen-only: windowed sizes come from the window
84    // (authored / dragged) and borderless covers the display, so the row is
85    // grayed + inert outside Fullscreen and never resizes the window.
86    resolution: Option<crate::gfx::display_mode::DisplayMode>,
87    // The mode the display was running at init (the row's display value until
88    // the user chooses one). `None` when the backend cannot read it.
89    current_mode: Option<crate::gfx::display_mode::DisplayMode>,
90    // The Resolution row's labels with their authored colors, captured at init
91    // so window-mode changes can gray the row out and restore it (mirrors
92    // `perf_sub_row_labels`).
93    resolution_row_labels: Vec<(AssetId, [f32; 3])>,
94    // Stats-HUD display state (GraphicsSettings perf_stats / show_fps / show_vram;
95    // default shown). `perf_stats` is the master "Display performance stats"
96    // toggle; the per-readout flags gate the FPS / VRAM chips under it. Published
97    // each frame as the `HudPrefs` resource for StatHudSystem, and persisted +
98    // applied live by the settings-menu rows. When the master is off the two
99    // sub-rows are grayed (their captured labels in `perf_sub_row_labels`) and
100    // made inert (the `DisabledSettingRows` resource read by UiInputSystem).
101    perf_stats: bool,
102    show_fps: bool,
103    show_vram: bool,
104    // The TextLabel ids of the show_fps / show_vram rows with their authored
105    // colors, captured at init so the master toggle can gray them and restore
106    // them (the menu's HitRegions are drained after init, so the row -> label map
107    // is captured once rather than re-queried).
108    perf_sub_row_labels: Vec<(AssetId, [f32; 3])>,
109    max_frames: Option<u64>,
110    shadow_map_size: u32,
111    shadow_update: crate::components::ShadowUpdate,
112    // Shadow distance in world units (GraphicsConfig.shadow_distance). Applied
113    // live via set_shadow_distance (the per-frame cascade-split math reads it);
114    // preset-governed (a manual change flips the master preset to Custom).
115    shadow_distance: u32,
116    // Active shadow cascade count, 1..=4 (GraphicsConfig.shadow_cascades). Applied
117    // live via set_shadow_cascades (the per-frame split + schedule read it);
118    // preset-governed (a manual change flips the master preset to Custom).
119    shadow_cascades: u32,
120    // Scene-sampler max anisotropy. Restart-required (the sampler is built once at
121    // backend init from this), so this is display/persist state; the value reaches
122    // the backend through the ctor. Preset-governed (a manual change flips the
123    // master preset to Custom).
124    anisotropy: u32,
125    failed: bool,
126    start_time: Option<Instant>,
127    frame_count: u64,
128    // Per-class recovery for failed frames; see `frame_policy`.
129    frame_policy: frame_policy::FramePolicy,
130    // A togglable menu (a Screen) coexists with a controlled Camera3D. When set,
131    // cursor capture is driven each frame by whether a menu screen is active
132    // (release while open, capture otherwise) rather than fixed at startup.
133    menu_mode: bool,
134    // Current render-scale (upscaling) quality, seeded at init from the world's
135    // PostProcessConfig overridden by any persisted choice. The settings row
136    // cycles + persists it; it is restart-required, so this is display/persist
137    // state only (the upscaler is sized once at init).
138    render_scale: crate::components::UpscaleQuality,
139    // Current upscaler backend (Auto / FSR3 / DLSS / XeSS), seeded at init from
140    // the world's PostProcessConfig overridden by any persisted choice. Like
141    // render_scale this is restart-required display/persist state (the upscaler
142    // is selected + built once at init); DirectX / Vulkan only.
143    upscale_backend: crate::components::UpscalerBackend,
144    // The render backend while init constructs and wires it. Boxed
145    // `dyn RenderBackend` so the setup logic in init.rs / streaming.rs /
146    // scene.rs runs as one cfg-free path across Metal, DirectX, and Vulkan.
147    // At the end of a successful init it is parked in the world's
148    // `ActiveRenderBackend` resource, where every per-step user (this system's
149    // frame encode, InputSystem's poll) takes and returns it; `None` from then
150    // on.
151    backend: Option<Box<dyn RenderBackend>>,
152    // active scene-flow bookkeeping while init builds it; handed to the shared
153    // `ActiveSceneFlow` resource at the end of init (SettingsSystem jumps it,
154    // this system ticks it). None when no Scene assets were declared.
155    scene_flow: Option<scene_flow::SceneFlow>,
156    // Per-entity scene-visibility snapshot, refreshed (buffers reused) every
157    // frame a fade runs and on scene-visibility applies.
158    scene_visibility: scene::SceneVisibilityScratch,
159    // Overlay build inputs assembled during init() and handed to OverlaySystem
160    // (as the `OverlayAssets` resource) at its end; empty afterwards. Fonts is
161    // the atlas data keyed by handle; sprite_texture_slots maps a Sprite's
162    // texture into the text-atlas pool (appended after the font atlases); the
163    // chip id lists and scroll clip bands drive the per-frame HUD layout.
164    loaded_fonts: text::FontSet,
165    sprite_texture_slots: crate::gfx::overlay_maps::TextureSlots,
166    debug_hud_chips: Vec<AssetId>,
167    stat_hud_chips: Vec<AssetId>,
168    // Viewport-pick candidates captured at init, one per prop entity, only
169    // when a `PickIndex` resource was present (the editor's opt-in). The frame
170    // step refreshes the published index from these + the live transforms;
171    // empty in a shipped runtime, which skips the refresh entirely.
172    pick_candidates: Vec<PickCandidate>,
173    // Streaming pools built during init (shared albedo+normal texture pool,
174    // mesh geometry, and voxel-world chunks), each `Some` only when a
175    // `StreamingConfig` / `VoxelWorld` was declared and the backend supports it
176    // (Metal). Init scratch: they are moved into the parked `StreamingState`
177    // resource at the end of init, where StreamingSystem drives them each frame,
178    // so they are `None` here from then on.
179    texture_streamer: Option<crate::gfx::streaming::texture::TextureStreamer>,
180    mesh_streamer: Option<crate::gfx::streaming::mesh::MeshStreamer>,
181    // Maps a streamed mesh's id to its DrawObject index, so completed loads
182    // and evictions are applied to the right draw. Empty when not streaming.
183    mesh_stream_draw_indices: Vec<usize>,
184    chunk_stream: Option<crate::gfx::streaming_system::ChunkStreamState>,
185    // Shader buckets whose pipeline init deferred, with the payload source the
186    // pump reads when their scene pins. Init scratch like the pools above.
187    shader_warmup: Option<crate::gfx::streaming::shader::ShaderWarmup>,
188    // Which scene exclusively owns each deferred bucket, so scene residency
189    // can claim it as a member.
190    deferred_shader_scenes: Vec<(u32, AssetId)>,
191    // Source catalogues captured at init for asset hot-reload, handed off to
192    // the `cn debug` binary's reload machinery (which owns the watcher + the
193    // live `AssetHotReloadState`). `Some` only under `cn debug` with at least
194    // one file-backed asset / world.jsonl; taken once by the debug drive via
195    // `take_hot_reload_sources`. `cn run` never captures these; production
196    // reads asset payloads from the compiled blob and never re-touches disk.
197    pending_hot_reload_sources: Option<hot_reload_sources::HotReloadSources>,
198    // Texture-name map captured at init for runtime decal / emitter spawn to
199    // resolve an authored Texture name to its live pool slot. `Some` only under
200    // `cn debug`; read-only after init.
201    world_reload: Option<WorldReloadState>,
202    // The persisted settings-menu graphics overrides as they stood at init
203    // (each field `None` when the user never changed that row). Held so the
204    // live-lighting seam can re-derive a knob exactly as init did: an authoring
205    // edit to a row the user has overridden moves the authored baseline only,
206    // matching what a relaunch of the edited world would show.
207    persisted_graphics: crate::config::GraphicsSettings,
208    // Whether the world declared enabled fog at init, so the backend built the
209    // fog pass. A backend that never built it cannot be handed fog live.
210    fog_built: bool,
211    // Last `VolumetricFog` settings pushed to the backend, used by the
212    // world.jsonl reload pass to dedupe: if the resolved value matches what's
213    // already live, the reload skips the trait call and the log entry. Tracks
214    // both `None` (no fog / disabled) and `Some(settings)`. Initialised by
215    // `run_init` to whatever was passed into the backend constructor.
216    last_fog_settings: Option<crate::gfx::volumetric_fog::FogSettings>,
217    // Live post-process parameters (bloom / exposure / vignette / LUT blend),
218    // the source of truth for slider settings. Seeded at init from the world's
219    // resolved PostProcessConfig (with any persisted overrides applied); a
220    // slider drag mutates a field here and pushes the whole struct to the
221    // backend via `update_post_process`.
222    post_process: crate::gfx::render_types::PostProcessTunables,
223    // Live ambient (IBL) light scale, the source of truth for the Ambient
224    // slider. Lives in the backend's `LightUniforms` (not `PostProcessParams`),
225    // so it is held + pushed separately via `set_ambient_intensity`. Seeded at
226    // init from the world's `PostProcessConfig.ambient_intensity` (with any
227    // persisted override applied) and pushed to the backend once after it is
228    // built.
229    ambient_intensity: f32,
230    // The world's resolved PostProcessConfig with the user's persisted
231    // quality-toggle overrides applied (defaulted when the world declares none).
232    // The source of truth for the Quality-group toggles: a toggle flips the
233    // matching field here, re-derives the per-feature settings, and pushes them
234    // to the backend's live rebuild. The non-toggle fields (exposure, bloom,
235    // ambient) keep their authored values here; the sliders own those via
236    // `post_process` / `ambient_intensity` instead.
237    post_config: crate::components::PostProcessConfig,
238    // Slider rows in the world, captured at init from their drag HitRegions +
239    // handle Sprites. Drives the handle position + value-label update when a
240    // slider changes, and the one-time sync of both to the live value at init.
241    sliders: Vec<SliderViz>,
242    // Cycle rows' setting key -> value-label id, captured at init from their
243    // `setting:<key>:next` HitRegions (drained by UiInputSystem afterwards). Lets
244    // a runtime change relabel a row other than the one clicked: the master
245    // "Graphics Quality" preset relabels the quality toggles + render scale it
246    // re-derives, and an individual quality-row change relabels the master row.
247    cycle_value_labels: std::collections::HashMap<String, AssetId>,
248    // Per-element clip bands (reference space) captured at init from the world's
249    // ScrollPanels: each scroll-content element id maps to its panel's content
250    // band, so the draw path scissors it and off-band rows do not bleed over the
251    // panel chrome. Empty when no ScrollPanel was declared; handed to
252    // OverlaySystem (inside `OverlayAssets`) at the end of init.
253    clip_rects: crate::gfx::overlay_maps::ClipRects,
254    // Live gameplay movement key map (the source of truth for the Controls-tab
255    // rebind rows). Seeded at init from the persisted `ControlsSettings.keymap`
256    // or the engine default, pushed to the backend once after it is built, and
257    // updated (with a swap) + re-pushed + persisted on each rebind.
258    keymap: crate::gfx::keymap::KeyMap,
259    // Rebind rows in the world, captured at init from their `setting:key_*:rebind`
260    // HitRegions. Maps each rebindable action to its value `TextLabel`, so a
261    // rebind (and the swap it may trigger) can refresh both affected row labels.
262    rebind_rows: Vec<RebindViz>,
263    // Live gamepad action -> button map. Seeded at init from the persisted
264    // `ControlsSettings.gamepad_map` or the engine default; InputSystem applies
265    // it (the gamepad is polled engine-side, so no backend push).
266    gamepad_map: crate::components::GamepadMap,
267    // Gamepad rebind rows in the world, captured at init from their
268    // `setting:pad_*:rebind` HitRegions, like `rebind_rows`.
269    pad_rebind_rows: Vec<PadRebindViz>,
270    // Device capability flags, queried from the backend once it is built. Drives
271    // the capability gating at init: a settings row whose feature the device
272    // cannot provide (e.g. ray-traced reflections without hardware ray tracing)
273    // is grayed out and made inert. Held in memory only, never persisted.
274    caps: crate::gfx::backend::DeviceCapabilities,
275    // Coarse GPU performance profile, probed before the backend is built so the
276    // auto-config quality ceiling can influence the render targets / effect
277    // pipelines sized at backend init. Held in memory only, never persisted.
278    gpu_profile: crate::gfx::backend::GpuProfile,
279    // The live master "Graphics Quality" preset the settings-menu row cycles.
280    // Seeded at init from the persisted choice (or `Auto` on first launch);
281    // changing a preset re-derives the quality toggles + render scale under its
282    // ceiling, and changing any individual quality row flips this to `Custom`.
283    quality_preset: crate::gfx::quality_preset::QualityPreset,
284    // The world's authored PostProcessConfig before the user overrides + preset
285    // ceiling are applied (defaulted when the world declares none). The pristine
286    // baseline a live preset change re-clamps from, so up-shifting a preset
287    // restores the world's features and down-shifting clamps them off.
288    authored_post_config: crate::components::PostProcessConfig,
289    // Display-output / upscaling preferences (the Display settings rows). Resolved
290    // at init from the world's `PostProcessConfig` overridden by any persisted
291    // choice, passed to the backend ctor, and held here so the rows display +
292    // cycle them. Restart-required (swapchain format / render targets are sized
293    // once at init), so a runtime change only persists + relabels; independent of
294    // the quality preset.
295    temporal_upscaling: bool,
296    hdr_display: bool,
297    hdr_pq: bool,
298    // The world's authored shadow knobs before the user overrides + preset ceiling
299    // (defaulted when the world declares no GraphicsConfig). The pristine baseline
300    // a live preset change re-clamps from, like `authored_post_config`. The live
301    // values are `shadow_map_size` / `shadow_update` above.
302    authored_shadow_map_size: u32,
303    authored_shadow_update: crate::components::ShadowUpdate,
304    // The world's authored shadow distance, the baseline a live preset change
305    // re-clamps from. The live value is `shadow_distance` above.
306    authored_shadow_distance: u32,
307    // The world's authored shadow cascade count, the baseline a live preset
308    // change re-clamps from. The live value is `shadow_cascades` above.
309    authored_shadow_cascades: u32,
310    // The world's authored anisotropy degree before the user override + preset
311    // ceiling, the baseline a live preset change re-clamps from (like
312    // `authored_shadow_map_size`). The live value is `anisotropy` above.
313    authored_anisotropy: u32,
314    // System / streaming restart preferences (the Advanced "Frame Buffering",
315    // "Occlusion Culling", and "Texture Quality" rows). Resolved at init from the
316    // world's config overridden by any persisted choice, passed to the backend
317    // ctor / streamer, and held here so the rows display + cycle them. Restart-
318    // required, independent of the quality preset. `frames_in_flight` lives above.
319    occlusion_two_pass: bool,
320    texture_cap: u32,
321    texture_budget: u32,
322    // Reused scratch + change-tracking for the per-frame transform propagation
323    // (`transform_propagation::propagate_transforms_cached`): buffers are refilled in place
324    // and the pass is skipped on frames where no Transform / Parent changed.
325    transform_cache: crate::gfx::transform_propagation::TransformCache,
326    // Last-pushed model matrix per draw slot / skinned instance: a static
327    // slot costs a compare instead of a snapshot entry, and each family
328    // crosses the backend trait once per frame.
329    model_push: model_push::ModelPushCache,
330    skinned_model_push: model_push::ModelPushCache,
331    // The owned per-frame draw inputs `extract` fills from world state and
332    // `submit` replays onto the backend. Held here so its buffers keep their
333    // capacity across frames; taken out of `self` for the duration of one
334    // step.
335    snapshot: crate::gfx::snapshot::RenderSnapshot,
336    // Logical viewport size the line builder maps ribbon widths with. Seeded
337    // from the backend at init, refreshed each frame from `FrameInput`.
338    viewport: (f32, f32),
339    // Test-only injection seam: pre-resolved settings, a fabricated GPU
340    // profile, and a mock backend factory, so unit tests can drive
341    // run_init / run_step without a GPU device or the on-disk settings store.
342    #[cfg(test)]
343    pub(crate) test_hooks: Option<crate::gfx::mock_backend::TestHooks>,
344}
345
346// One key-rebind row's runtime bookkeeping: the action it rebinds and the value
347// `TextLabel` showing its bound key. Built at init (`init_rebind_rows`) from the
348// row's `setting:key_*:rebind` HitRegion (`action` -> `Bindable`, `label`) and
349// handed to SettingsState, which drives the live rebind drain.
350pub(crate) struct RebindViz {
351    pub(crate) action: crate::gfx::keymap::Bindable,
352    pub(crate) value_id: AssetId,
353}
354
355// One gamepad-rebind row's runtime bookkeeping, mirroring `RebindViz`: built at
356// init (`init_pad_rebind_rows`) from the row's `setting:pad_*:rebind` HitRegion
357// and handed to SettingsState for the button-rebind drain.
358pub(crate) struct PadRebindViz {
359    pub(crate) action: crate::components::GamepadAction,
360    pub(crate) value_id: AssetId,
361}
362
363// One slider row's runtime bookkeeping: the engine setting it controls, the
364// track geometry it maps a fraction onto, and the handle Sprite + value
365// TextLabel it drives. Built at init (`init_sliders`) from the row's
366// `setting:<key>:drag` HitRegion (track `x`/`width`, `label`, `drag_handle`) and
367// the handle Sprite's width, then handed to SettingsState for the slider drain.
368pub(crate) struct SliderViz {
369    pub(crate) key: String,
370    pub(crate) track_x: f32,
371    pub(crate) track_w: f32,
372    pub(crate) handle_w: f32,
373    pub(crate) handle_id: AssetId,
374    pub(crate) value_id: AssetId,
375}
376
377/// Init-time asset-resolution tables consulted by the world.jsonl hot-reload
378/// pass when applying adds and non-transform edits. Captured at init and
379/// never mutated afterwards: the reload path cannot introduce new
380/// Materials / Textures / Meshes / Models on the fly (those need a process
381/// restart), but every authored Prop that points at an asset already in the
382/// init world resolves through these maps without re-running build.
383/// Built by init, read only by the `cn debug` binary's world.jsonl reload pass,
384/// so its fields read as dead under `cargo check --lib`.
385pub struct WorldReloadState {
386    /// Texture asset name -> live pool slot, so runtime decal / emitter spawn
387    /// (`cn debug`) can resolve an authored Texture name to its slot.
388    pub texture_name_to_slot: std::collections::HashMap<AssetId, usize>,
389}
390
391/// Disjoint mutable screen of the `GraphicsSystem` fields the hot-reload passes
392/// edit in one tick: the active backend, the texture-name map for runtime
393/// decal / emitter spawn, and the fog bookkeeping the world.jsonl reload pass
394/// dedupes against. Returned by [`GraphicsSystem::hot_reload_apply_parts`] so the
395/// binary-only `DebugHook::tick` drive can apply the reload passes from outside
396/// the per-system step without the library depending on it. The reload
397/// catalogue and in-flight state live on the debug side
398/// (`crate::debug::hot_reload`), built from
399/// [`HotReloadSources`](crate::gfx::graphics_system::hot_reload_sources::HotReloadSources).
400/// The library never constructs this; the fields are read from the debug
401/// drive alone.
402pub struct HotReloadApplyParts<'a> {
403    /// The live render backend.
404    pub backend: &'a mut dyn RenderBackend,
405    /// The in-flight world reload, when one is running.
406    pub world_reload: &'a Option<WorldReloadState>,
407    /// The fog settings last pushed, so a reload can detect a change.
408    pub last_fog_settings: &'a mut Option<crate::gfx::volumetric_fog::FogSettings>,
409}
410
411impl std::fmt::Debug for GraphicsSystem {
412    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413        f.debug_struct("GraphicsSystem")
414            .field("frame_count", &self.frame_count)
415            .field("failed", &self.failed)
416            .finish()
417    }
418}
419
420impl GraphicsSystem {
421    /// Fresh renderer driver with no backend yet. Config (frames-in-flight,
422    /// clear color, `max_frames`, shadow-map size) is read from the world's
423    /// `GraphicsConfig` in [`System::init`].
424    pub fn new() -> Self {
425        // The schema's own defaults, so a world with no GraphicsConfig sees the
426        // same values as one that declares an all-default component.
427        let gfx = crate::components::GraphicsConfig::default();
428        Self {
429            window_args: Default::default(),
430            clear_color: gfx.clear_color,
431            frames_in_flight: gfx.frames_in_flight as usize,
432            vsync: gfx.vsync,
433            fps_cap: gfx.fps_cap,
434            display_modes: Vec::new(),
435            resolution: None,
436            current_mode: None,
437            resolution_row_labels: Vec::new(),
438            perf_stats: true,
439            show_fps: true,
440            show_vram: true,
441            perf_sub_row_labels: Vec::new(),
442            max_frames: gfx.max_frames,
443            shadow_map_size: gfx.shadow_map_size,
444            shadow_update: gfx.shadow_update,
445            shadow_distance: gfx.shadow_distance,
446            shadow_cascades: gfx.shadow_cascades,
447            anisotropy: gfx.anisotropy,
448            failed: false,
449            start_time: None,
450            frame_count: 0,
451            frame_policy: frame_policy::FramePolicy::default(),
452            menu_mode: false,
453            render_scale: crate::components::UpscaleQuality::default(),
454            upscale_backend: crate::components::UpscalerBackend::default(),
455            backend: None,
456            scene_flow: None,
457            scene_visibility: Default::default(),
458            loaded_fonts: text::FontSet::default(),
459            sprite_texture_slots: crate::gfx::overlay_maps::TextureSlots::new(),
460            debug_hud_chips: Vec::new(),
461            stat_hud_chips: Vec::new(),
462            pick_candidates: Vec::new(),
463            texture_streamer: None,
464            mesh_streamer: None,
465            mesh_stream_draw_indices: Vec::new(),
466            chunk_stream: None,
467            shader_warmup: None,
468            deferred_shader_scenes: Vec::new(),
469            pending_hot_reload_sources: None,
470            world_reload: None,
471            persisted_graphics: crate::config::GraphicsSettings::default(),
472            fog_built: false,
473            last_fog_settings: None,
474            post_process: crate::gfx::render_types::PostProcessTunables::DEFAULT,
475            // Matches PostProcessConfig's ambient_intensity default; overwritten
476            // at init from the world / persisted store.
477            ambient_intensity: 1.0,
478            // Default until init resolves the world's config + persisted toggles.
479            post_config: crate::components::PostProcessConfig::default(),
480            sliders: Vec::new(),
481            cycle_value_labels: std::collections::HashMap::new(),
482            clip_rects: crate::gfx::overlay_maps::ClipRects::new(),
483            keymap: crate::gfx::keymap::KeyMap::default(),
484            rebind_rows: Vec::new(),
485            gamepad_map: crate::components::GamepadMap::default(),
486            pad_rebind_rows: Vec::new(),
487            // All-capable until the backend reports otherwise at init.
488            caps: crate::gfx::backend::DeviceCapabilities::ALL,
489            // Conservative until probed at init.
490            gpu_profile: crate::gfx::backend::GpuProfile::UNKNOWN,
491            // Seeded at init from the persisted preset (Auto on first launch).
492            quality_preset: crate::gfx::quality_preset::QualityPreset::Auto,
493            // Defaulted until init captures the world's authored config.
494            authored_post_config: crate::components::PostProcessConfig::default(),
495            // Resolved at init from the world's config + persisted overrides.
496            temporal_upscaling: false,
497            hdr_display: false,
498            hdr_pq: false,
499            authored_shadow_map_size: gfx.shadow_map_size,
500            authored_shadow_update: gfx.shadow_update,
501            authored_shadow_distance: gfx.shadow_distance,
502            authored_shadow_cascades: gfx.shadow_cascades,
503            authored_anisotropy: gfx.anisotropy,
504            occlusion_two_pass: crate::components::PostProcessConfig::default().occlusion_two_pass,
505            texture_cap: 96,
506            texture_budget: 4,
507            transform_cache: crate::gfx::transform_propagation::TransformCache::default(),
508            model_push: model_push::ModelPushCache::default(),
509            skinned_model_push: model_push::ModelPushCache::default(),
510            snapshot: crate::gfx::snapshot::RenderSnapshot::default(),
511            viewport: (0.0, 0.0),
512            #[cfg(test)]
513            test_hooks: None,
514        }
515    }
516
517    // The persisted settings store consulted at init. Reads the on-disk file
518    // in production; a test-injected copy takes its place so unit tests never
519    // read (or depend on) the developer's real settings.
520    fn persisted_settings(&self) -> crate::config::Settings {
521        #[cfg(test)]
522        if let Some(hooks) = &self.test_hooks {
523            return hooks.settings.clone();
524        }
525        crate::config::Settings::load()
526    }
527
528    // Detect the GPU performance profile for quality auto-config. Probes the
529    // real device in production; a test-injected profile takes its place so
530    // unit tests never create a GPU handle.
531    fn detect_gpu_profile(&self) -> crate::gfx::backend::GpuProfile {
532        #[cfg(test)]
533        if let Some(hooks) = &self.test_hooks {
534            return hooks.gpu_profile;
535        }
536        crate::device::probe_gpu_profile()
537    }
538
539    // Seed and persist the first-launch `Auto` quality preset. Skipped under
540    // the test injection seam so tests never write the settings file.
541    fn seed_first_launch_preset(&self) {
542        #[cfg(test)]
543        if self.test_hooks.is_some() {
544            return;
545        }
546        let mut s = crate::config::Settings::load();
547        s.graphics.quality_preset = Some(crate::gfx::quality_preset::QualityPreset::Auto);
548        if let Err(e) = s.save() {
549            tracing::warn!("first-launch quality preset save failed: {e}");
550        }
551    }
552
553    // The mode the Resolution row displays and cycles from: the user's choice,
554    // else the display's own mode, else the authored window size (a backend
555    // that cannot read the display; snaps to the nearest listed mode).
556    fn effective_resolution(&self) -> crate::gfx::display_mode::DisplayMode {
557        self.resolution
558            .or(self.current_mode)
559            .unwrap_or(crate::gfx::display_mode::DisplayMode {
560                width: self.window_args.width,
561                height: self.window_args.height,
562                refresh_hz: 0,
563            })
564    }
565}
566
567impl Default for GraphicsSystem {
568    fn default() -> Self {
569        Self::new()
570    }
571}
572
573impl System for GraphicsSystem {
574    fn init(&mut self, ctx: &mut PipelineContext) {
575        self.run_init(ctx);
576    }
577
578    fn step(&mut self, ctx: &mut PipelineContext) -> StepResult {
579        self.run_step(ctx)
580    }
581}
582
583impl GraphicsSystem {
584    /// Disjoint mutable screen of the backend + hot-reload bookkeeping the
585    /// binary-only `DebugHook::tick` reload drive applies changes through. The
586    /// caller supplies the backend (borrowed from the world's parked slot via
587    /// `World::systems_and_render_backend`) since this system yields it after
588    /// init. The library never calls this (the asset hot-reload drive lives in
589    /// the `cn debug` binary), so it reads as dead code under
590    /// `cargo check --lib`.
591    pub fn hot_reload_apply_parts<'a>(
592        &'a mut self,
593        backend: &'a mut dyn RenderBackend,
594    ) -> HotReloadApplyParts<'a> {
595        HotReloadApplyParts {
596            backend,
597            world_reload: &self.world_reload,
598            last_fog_settings: &mut self.last_fog_settings,
599        }
600    }
601
602    /// Take the init-captured hot-reload source catalogues, leaving `None`
603    /// behind. The `cn debug` drive calls this once on its first tick to build
604    /// the filesystem watcher + `AssetHotReloadState`. `None` under `cn run`,
605    /// or when no file-backed asset / world.jsonl was declared.
606    pub fn take_hot_reload_sources(&mut self) -> Option<hot_reload_sources::HotReloadSources> {
607        self.pending_hot_reload_sources.take()
608    }
609
610    // Stand up the albedo-texture streaming subsystem when a StreamingConfig
611    // was declared. Every streamable slot is evicted to a placeholder now; the
612    // streamer brings them back resident over the next frames, nearest first.
613    //
614    // The payload source depends on where the world came from: a disk-backed
615    // `cn run` world re-reads each payload from its blob file (no RAM copy), an
616    // in-memory `cn debug` world keeps the payloads RAM-resident.
617}
618
619// Quality-toggle plumbing shared by init (value-label sync + initial overlay)
620// and the per-frame drain. Centralising the key -> `PostProcessConfig` field
621// mapping here keeps the three call sites (read state, flip state, derive the
622// backend settings) from drifting apart.
623
624// The current on/off state of quality toggle `key` in `cfg`, or `None` for a
625// key that is not a quality toggle.
626pub(crate) fn quality_toggle_on(
627    cfg: &crate::components::PostProcessConfig,
628    key: &str,
629) -> Option<bool> {
630    match key {
631        "ssao" => Some(cfg.ssao),
632        "ssr" => Some(cfg.ssr),
633        "ray_traced_reflections" => Some(cfg.ray_traced_reflections),
634        "ssgi" => Some(cfg.indirect_lighting == crate::components::IndirectLighting::Ssgi),
635        "auto_exposure" => Some(cfg.auto_exposure),
636        _ => None,
637    }
638}
639
640// Flip quality toggle `key` to `on` in `cfg`. Unknown keys are ignored.
641pub(crate) fn set_quality_toggle(
642    cfg: &mut crate::components::PostProcessConfig,
643    key: &str,
644    on: bool,
645) {
646    match key {
647        "ssao" => cfg.ssao = on,
648        "ssr" => cfg.ssr = on,
649        "ray_traced_reflections" => cfg.ray_traced_reflections = on,
650        "ssgi" => {
651            cfg.indirect_lighting = if on {
652                crate::components::IndirectLighting::Ssgi
653            } else {
654                crate::components::IndirectLighting::Ibl
655            }
656        }
657        "auto_exposure" => cfg.auto_exposure = on,
658        _ => {}
659    }
660}
661
662// Whether `key` is one of the cycle (dropdown) quality knobs governed by the
663// preset ceiling like the boolean toggles (a manual change flips the preset to
664// Custom). The set lives in `settings::QUALITY_CYCLE_KEYS`.
665pub(crate) fn is_quality_cycle(key: &str) -> bool {
666    crate::gfx::settings::QUALITY_CYCLE_KEYS.contains(&key)
667}
668
669// The current menu option index of cycle quality knob `key` in `cfg`, or `None`
670// for a key that is not a cycle quality knob.
671pub(crate) fn quality_cycle_index(
672    cfg: &crate::components::PostProcessConfig,
673    key: &str,
674) -> Option<usize> {
675    use crate::gfx::settings;
676    match key {
677        "aa_mode" => Some(settings::aa_mode_index(cfg.aa_mode)),
678        "ssgi_resolution" => Some(settings::ssgi_resolution_index(cfg.ssgi_resolution)),
679        "ssgi_rays" => Some(settings::ssgi_rays_index(cfg.ssgi_rays)),
680        "ssgi_steps" => Some(settings::ssgi_steps_index(cfg.ssgi_steps)),
681        "reflection_blur_resolution" => Some(settings::reflection_blur_index(
682            cfg.reflection_blur_resolution,
683        )),
684        _ => None,
685    }
686}
687
688// Set cycle quality knob `key` in `cfg` from a menu option index. Unknown keys
689// are ignored.
690pub(crate) fn set_quality_cycle(
691    cfg: &mut crate::components::PostProcessConfig,
692    key: &str,
693    index: usize,
694) {
695    use crate::gfx::settings;
696    match key {
697        "aa_mode" => cfg.aa_mode = settings::aa_mode_at(index),
698        "ssgi_resolution" => cfg.ssgi_resolution = settings::ssgi_resolution_at(index),
699        "ssgi_rays" => cfg.ssgi_rays = settings::ssgi_rays_at(index),
700        "ssgi_steps" => cfg.ssgi_steps = settings::ssgi_steps_at(index),
701        "reflection_blur_resolution" => {
702            cfg.reflection_blur_resolution = settings::reflection_blur_at(index)
703        }
704        _ => {}
705    }
706}
707
708// Clamp cycle quality knob `key` in `cfg` DOWN under the ceiling (coarser
709// resolution / smaller count; never raises), a no-op when the user explicitly
710// overrode it. Shared by the init clamp and the live preset re-derive so both
711// produce the same result.
712pub(crate) fn clamp_quality_cycle(
713    cfg: &mut crate::components::PostProcessConfig,
714    key: &str,
715    ceiling: &crate::gfx::quality_preset::QualityCeiling,
716    overridden: bool,
717) {
718    if overridden {
719        return;
720    }
721    use crate::gfx::quality_preset::{
722        clamp_aa_mode, coarser_reflection_blur, coarser_ssgi_resolution,
723    };
724    match key {
725        "aa_mode" => cfg.aa_mode = clamp_aa_mode(cfg.aa_mode, ceiling.aa_mode),
726        "ssgi_resolution" => {
727            cfg.ssgi_resolution =
728                coarser_ssgi_resolution(cfg.ssgi_resolution, ceiling.ssgi_resolution)
729        }
730        "ssgi_rays" => cfg.ssgi_rays = cfg.ssgi_rays.min(ceiling.ssgi_rays),
731        "ssgi_steps" => cfg.ssgi_steps = cfg.ssgi_steps.min(ceiling.ssgi_steps),
732        "reflection_blur_resolution" => {
733            cfg.reflection_blur_resolution = coarser_reflection_blur(
734                cfg.reflection_blur_resolution,
735                ceiling.reflection_blur_resolution,
736            )
737        }
738        _ => {}
739    }
740}
741
742// Derive the backend's per-feature `QualitySettings` from a resolved config.
743// Mirrors the init-time derivation (the same `*_settings()` methods), so a
744// live rebuild reproduces exactly what a launch with this config would build.
745pub(crate) fn derive_quality_settings(
746    cfg: &crate::components::PostProcessConfig,
747) -> crate::gfx::backend::QualitySettings {
748    crate::gfx::backend::QualitySettings {
749        taa: cfg.aa_mode.taa_enabled(),
750        ssao: cfg.ssao_settings(),
751        ssr: cfg.ssr_settings(),
752        rt_reflections: cfg.rt_reflection_settings(),
753        ssgi: cfg.ssgi_settings(),
754        reflection_blur_scale: cfg.reflection_blur_divisor(),
755        auto_exposure: cfg.auto_exposure_settings(),
756        auto_exposure_bias_ev: cfg.exposure_ev,
757    }
758}
759
760pub(crate) mod character_shape;
761mod frame;
762pub(crate) mod frame_policy;
763mod helpers;
764pub mod hot_reload_sources;
765mod init;
766mod lines;
767mod model_push;
768pub(crate) mod scene;
769mod streaming;
770pub(crate) mod submit;
771#[cfg(test)]
772mod tests;