Skip to main content

proof_engine/
lib.rs

1#![allow(dead_code, unused_variables, unused_imports, unused_mut, unused_parens, non_snake_case, unreachable_patterns, unused_assignments, unused_labels, unused_doc_comments, private_interfaces, non_camel_case_types, non_upper_case_globals, clippy::all)]
2
3//! # Proof Engine
4//!
5//! A Rust engine for real-time graphics where every point, glyph and
6//! particle on screen is moved by a real equation: an ODE, a force field, a
7//! spring, a noise function.
8//!
9//! <img src="https://gitlab.com/mattbusel/proof-engine/-/raw/main/assets/gifs/strange_attractors.gif" width="100%" alt="Seven strange attractors, each 1,500 points integrated with RK4">
10//!
11//! ## The math runs without a window
12//!
13//! The integrators, attractors, fields and [`MathFunction`]s are plain Rust
14//! and can be used on their own. This example runs as a doc test:
15//!
16//! ```rust
17//! use proof_engine::math::attractors::{rk4_step, AttractorType};
18//! use proof_engine::prelude::Vec3;
19//!
20//! // Two Lorenz states that start 0.0001 apart...
21//! let mut a = Vec3::new(1.0, 1.0, 1.0);
22//! let mut b = Vec3::new(1.0001, 1.0, 1.0);
23//! for _ in 0..6000 {
24//!     a = rk4_step(AttractorType::Lorenz, a, 0.005);
25//!     b = rk4_step(AttractorType::Lorenz, b, 0.005);
26//! }
27//! // ...are far apart after 30 simulated seconds: that is chaos.
28//! assert!((a - b).length() > 1.0);
29//! ```
30//!
31//! ## Open a window and draw
32//!
33//! The `quickstart` example, in full. It needs a GPU with OpenGL 3.3, so it
34//! is compiled here but not run:
35//!
36//! ```rust,no_run
37//! use proof_engine::math::attractors::rk4_step;
38//! use proof_engine::prelude::*;
39//! use proof_engine::render::ui_layer::UiParticle;
40//!
41//! let mut engine = ProofEngine::new(EngineConfig::default());
42//! let mut points: Vec<Vec3> = (0..5000)
43//!     .map(|i| Vec3::new(1.0 + i as f32 * 4e-4, 1.0, 1.0))
44//!     .collect();
45//!
46//! engine.run_ui(move |engine, dt| {
47//!     for p in points.iter_mut() {
48//!         *p = rk4_step(AttractorType::Lorenz, *p, dt);
49//!     }
50//!     let (w, h) = engine.render_size();
51//!     let (cx, cy, s) = (w as f32 / 2.0, h as f32 / 2.0, h as f32 / 60.0);
52//!     let color = Vec4::new(0.5, 1.2, 1.6, 1.0);
53//!     let dots = points
54//!         .iter()
55//!         .map(|p| UiParticle::new(cx + p.x * s, cy - (p.z - 25.0) * s, 3.0, 3.0, '●', color))
56//!         .collect();
57//!     engine.ui.draw_particles(dots);
58//! });
59//! ```
60//!
61//! ## Where to look
62//!
63//! - [`ProofEngine`]: the window and main loop. [`ProofEngine::run`] for 3D
64//!   glyph scenes, [`ProofEngine::run_ui`] for screen-space games and plots.
65//! - [`EngineConfig`] and [`config::RenderConfig`]: window size, bloom,
66//!   tonemap, grain and every other post-processing setting.
67//! - [`Glyph`] and [`ProofEngine::spawn_glyph`]: the basic on-screen object.
68//! - [`ForceField`], [`MathFunction`] and [`math::attractors`]: the math.
69//! - [`render::ui_layer::UiParticle`]: fast screen-space points, as above.
70//! - [`prelude`]: the common imports in one line.
71//!
72//! Every example in the repository can save its own frames to disk with the
73//! `PROOF_SHOT` environment variables; see the
74//! [README](https://gitlab.com/mattbusel/proof-engine#capture-frames-from-any-program).
75
76pub mod math;
77pub mod glyph;
78pub mod entity;
79pub mod particle;
80pub mod scene;
81pub mod render;
82pub mod audio;
83pub mod integration;
84pub mod input;
85pub mod config;
86pub mod tween;
87pub mod debug;
88pub mod ui;
89pub mod timeline;
90pub mod procedural;
91pub mod physics;
92pub mod combat;
93pub mod spatial;
94pub mod effects;
95pub mod anim;
96pub mod animation;
97pub mod ai;
98pub mod networking;
99pub mod replay;
100pub mod scripting;
101pub mod terrain;
102pub mod ecs;
103pub mod editor;
104pub mod asset;
105pub mod save;
106/// Character stats, inventory, skills and quests.
107pub mod character;
108pub mod dsp;
109pub mod game;
110pub mod profiler;
111pub mod vfx;
112pub mod netcode;
113pub mod network;
114pub mod world;
115pub mod crafting;
116/// Navigation meshes, A* search and steering.
117pub mod pathfinding;
118pub mod economy;
119pub mod behavior;
120/// Atmosphere, precipitation and climate simulation.
121pub mod weather;
122pub mod deferred;
123pub mod shader_graph;
124pub mod surfaces;
125pub mod rendergraph;
126pub mod compute;
127pub mod lighting;
128pub mod number_theory;
129/// Graph data structures, layout, community detection, paths and flows.
130pub mod graph;
131/// Hyperbolic, spherical and toroidal spaces and portals.
132pub mod topology;
133pub mod stochastic;
134pub mod ml;
135pub mod wgpu_backend;
136pub mod geometry;
137pub mod symbolic;
138pub mod solver;
139pub mod fractal;
140pub mod metaball;
141pub mod worldgen;
142pub mod ecology;
143pub mod narrative;
144pub mod electromagnetic;
145pub mod relativistic;
146/// Quantum mechanics simulations: Schrodinger solvers, tunneling, entanglement and more.
147pub mod quantum;
148/// Sparse voxel octree global illumination (voxel cone tracing), not wired into the demos.
149pub mod svogi;
150pub mod curves;
151pub mod nishita_sky;
152pub mod volumetric_fog;
153pub mod tiled_lighting;
154pub mod export;
155mod capture;
156
157pub use config::EngineConfig;
158pub use math::{MathFunction, ForceField, Falloff, AttractorType};
159pub use glyph::{Glyph, RenderLayer, BlendMode};
160pub use entity::AmorphousEntity;
161pub use particle::{MathParticle, ParticleInteraction};
162pub use scene::SceneGraph;
163pub use render::camera::ProofCamera;
164pub use input::{InputState, Key};
165pub use render::pipeline::FrameStats;
166pub use audio::AudioEvent;
167
168/// The main engine struct. Create once, run forever.
169pub struct ProofEngine {
170    /// Engine settings; most render settings are read every frame.
171    pub config: EngineConfig,
172    /// The glyphs, particles and force fields in the world.
173    pub scene: SceneGraph,
174    /// The camera the world pass is drawn from.
175    pub camera: ProofCamera,
176    /// Keyboard and mouse state for the current frame.
177    pub input: InputState,
178    /// Screen-space UI, in pixel coordinates. Cleared at the start of every
179    /// frame by `run_ui`, so games redraw it immediate-mode style.
180    pub ui: render::ui_layer::UiLayer,
181    /// Transient screen effects: shockwaves, flashes, light shafts. Fire and
182    /// forget; ticked and uploaded by `run_ui` every frame.
183    pub fx: render::screen_fx::ScreenFx,
184    /// GPU density entities queued for this frame. Drained after the render.
185    density_queue: Vec<particle::gpu_density::GpuDensityEntityData>,
186    /// The particle budget per density entity, set by `init_gpu_density`.
187    density_budget: u32,
188    /// Optional audio engine — None if no output device is available.
189    pub audio: Option<audio::AudioEngine>,
190    // Internal render pipeline (initialized lazily when run() is called)
191    pipeline: Option<render::Pipeline>,
192}
193
194impl ProofEngine {
195    /// Create the engine. The window and GL context are created when `run`
196    /// or `run_ui` is called; audio starts now if enabled and a device exists.
197    pub fn new(mut config: EngineConfig) -> Self {
198        if let Some((w, h)) = capture::window_size() {
199            config.window_width = w;
200            config.window_height = h;
201        }
202        let audio = if config.audio.enabled {
203            audio::AudioEngine::try_new()
204        } else {
205            None
206        };
207        Self {
208            camera: ProofCamera::new(&config),
209            scene: SceneGraph::new(),
210            input: InputState::new(),
211            ui: render::ui_layer::UiLayer::new(
212                config.window_width as f32,
213                config.window_height as f32,
214            ),
215            fx: render::screen_fx::ScreenFx::new(),
216            density_queue: Vec::new(),
217            density_budget: 0,
218            audio,
219            config,
220            pipeline: None,
221        }
222    }
223
224    /// Turn on GPU density entities with a per-entity particle budget.
225    ///
226    /// The budget is capped at
227    /// [`MAX_PARTICLES_PER_ENTITY`](particle::gpu_density::MAX_PARTICLES_PER_ENTITY):
228    /// past that there are more particles than pixels and the picture stops
229    /// improving while the frame time keeps climbing. Asking for more is
230    /// fine; you get the cap and a log line.
231    pub fn init_gpu_density(&mut self, particles: u32) {
232        let cap = particle::gpu_density::MAX_PARTICLES_PER_ENTITY;
233        if particles > cap {
234            log::info!("gpu density: {particles} particles requested, drawing {cap} per entity");
235        }
236        self.density_budget = particles.min(cap);
237    }
238
239    /// Draw a density entity this frame. Call every frame it should show.
240    pub fn queue_gpu_density_entity(&mut self, entity: particle::gpu_density::GpuDensityEntityData) {
241        self.density_queue.push(entity);
242    }
243
244    /// Send an audio event. No-op if audio is unavailable.
245    pub fn emit_audio(&self, event: audio::AudioEvent) {
246        if let Some(ref a) = self.audio {
247            a.emit(event);
248        }
249    }
250
251    /// Run the engine. Calls `update` every frame with elapsed seconds.
252    /// Blocks until the window is closed.
253    pub fn run<F>(&mut self, mut update: F)
254    where
255        F: FnMut(&mut ProofEngine, f32),
256    {
257        self.run_with_overlay(move |engine, dt, _gl| {
258            update(engine, dt);
259        });
260    }
261
262    /// Run the engine with an overlay callback.
263    /// The overlay callback receives the glow GL context reference and is called
264    /// AFTER scene rendering but BEFORE buffer swap — perfect for egui.
265    pub fn run_with_overlay<F>(&mut self, mut update: F)
266    where
267        F: FnMut(&mut ProofEngine, f32, &glow::Context),
268    {
269        let pipeline = render::Pipeline::init(&self.config);
270        self.pipeline = Some(pipeline);
271        let mut capture = capture::Capture::from_env();
272        let fixed_dt = capture::fixed_dt();
273
274        let mut last = std::time::Instant::now();
275        loop {
276            let now = std::time::Instant::now();
277            let dt = fixed_dt.unwrap_or_else(|| now.duration_since(last).as_secs_f32().min(0.1));
278            last = now;
279
280            // Poll input
281            if let Some(ref mut p) = self.pipeline {
282                if !p.poll_events(&mut self.input) {
283                    break;
284                }
285            }
286
287            // Step force fields and physics
288            self.scene.tick(dt);
289
290            // User update (logic only — no GL calls here)
291            // We pass a dummy gl ref for the logic phase; the real painting
292            // happens after the scene render.
293            let gl_ptr = self.pipeline.as_ref().map(|p| p.gl() as *const glow::Context);
294
295            // Sync render config so runtime changes (particle_multiplier, bloom, etc.)
296            // take effect this frame.
297            if let Some(ref mut p) = self.pipeline {
298                p.update_render_config(&self.config.render);
299            }
300
301            // Render scene first
302            if let Some(ref mut p) = self.pipeline {
303                p.set_density_entities(&self.density_queue, self.density_budget);
304                p.render(&self.scene, &self.camera);
305            }
306            self.density_queue.clear();
307            self.fx.lights.clear();
308
309            // NOW paint the overlay (egui) on top of the rendered scene
310            if let Some(ptr) = gl_ptr {
311                let gl_ref = unsafe { &*ptr };
312                update(self, dt, gl_ref);
313            }
314
315            // PROOF_SHOT: read the finished frame back before it is swapped away.
316            if let Some(ref mut c) = capture {
317                if c.after_draw(self) {
318                    break;
319                }
320            }
321
322            // Honour a quit asked for during the update, as `run_ui` does.
323            if self.input.quit_requested {
324                break;
325            }
326
327            // Swap
328            if let Some(ref mut p) = self.pipeline {
329                if !p.swap() {
330                    break;
331                }
332            }
333        }
334    }
335
336    /// Run a UI-driven game.
337    ///
338    /// Unlike \[`run`\], `update` is called *before* the scene is drawn, and the
339    /// screen-space `ui` layer is painted afterwards. That ordering matters for
340    /// a game: what you push this frame is what appears this frame, rather than
341    /// showing up one frame late.
342    ///
343    /// The UI layer is cleared before each `update`, so games redraw it in full
344    /// every frame instead of tracking what to erase.
345    pub fn run_ui<F>(&mut self, mut update: F)
346    where
347        F: FnMut(&mut ProofEngine, f32),
348    {
349        let pipeline = render::Pipeline::init(&self.config);
350        self.pipeline = Some(pipeline);
351
352        // Size the UI layer from the framebuffer, which is what the viewport
353        // uses; the window's own size can differ on a scaled display.
354        let (w, h) = self.render_size();
355        self.ui.resize(w as f32, h as f32);
356
357        let mut capture = capture::Capture::from_env();
358        let fixed_dt = capture::fixed_dt();
359
360        let mut last = std::time::Instant::now();
361        let mut last_size = (w, h);
362        loop {
363            let now = std::time::Instant::now();
364            let dt = fixed_dt.unwrap_or_else(|| now.duration_since(last).as_secs_f32().min(0.1));
365            last = now;
366
367            if let Some(ref mut p) = self.pipeline {
368                if !p.poll_events(&mut self.input) {
369                    break;
370                }
371            }
372
373            // Keep the UI projection matched to the framebuffer.
374            let size = self.render_size();
375            if size != last_size {
376                last_size = size;
377                self.ui.resize(size.0 as f32, size.1 as f32);
378            }
379
380            self.scene.tick(dt);
381
382            // Game logic and UI construction, both before anything is drawn.
383            self.ui.begin_frame();
384            update(self, dt);
385
386            // Honour a quit asked for during the update.
387            //
388            // `request_quit` used to set a flag that nothing read, so a game's
389            // own Quit menu did nothing at all and the only way out was the
390            // window's close button. The check goes here, after the update and
391            // before the render, so the frame that asked to quit is the last
392            // one and nothing half-drawn reaches the screen.
393            if self.input.quit_requested {
394                break;
395            }
396
397            // Trauma decays here. It used to be added and never ticked in
398            // this loop, so the first hit left the camera shaking forever.
399            self.camera.shake.tick(dt);
400            self.fx.tick(dt);
401
402            if let Some(ref mut p) = self.pipeline {
403                p.update_render_config(&self.config.render);
404                p.set_density_entities(&self.density_queue, self.density_budget);
405                // The scene, then the UI's world pass into the same buffer,
406                // then post-processing over both.
407                p.render_frame(&self.scene, &self.camera, Some(&self.ui), &self.fx);
408                // The HUD, painted after post-processing so it stays sharp.
409                p.render_ui(&self.ui);
410            }
411            self.density_queue.clear();
412            self.fx.lights.clear();
413
414            if let Some(ref mut c) = capture {
415                if c.after_draw(self) {
416                    break;
417                }
418            }
419
420            if let Some(ref mut p) = self.pipeline {
421                if !p.swap() {
422                    break;
423                }
424            }
425        }
426    }
427
428    /// Add a force field to the scene.
429    pub fn add_field(&mut self, field: ForceField) -> scene::FieldId {
430        self.scene.add_field(field)
431    }
432
433    /// Remove a force field.
434    pub fn remove_field(&mut self, id: scene::FieldId) {
435        self.scene.remove_field(id)
436    }
437
438    /// Spawn a glyph into the scene.
439    pub fn spawn_glyph(&mut self, glyph: Glyph) -> glyph::GlyphId {
440        self.scene.spawn_glyph(glyph)
441    }
442
443    /// Spawn an amorphous entity, creating its formation glyphs.
444    pub fn spawn_entity(&mut self, mut entity: AmorphousEntity) -> entity::EntityId {
445        // If no formation was specified, generate a default diamond
446        if entity.formation.is_empty() {
447            use entity::formation::Formation;
448            let f = Formation::diamond(2);
449            entity.formation = f.positions;
450            entity.formation_chars = f.chars;
451        }
452        // Ensure colors are filled (white if unspecified)
453        while entity.formation_colors.len() < entity.formation.len() {
454            entity.formation_colors.push(glam::Vec4::ONE);
455        }
456        // Spawn one glyph per formation slot
457        for i in 0..entity.formation.len() {
458            let offset = entity.formation[i];
459            let ch = entity.formation_chars.get(i).copied().unwrap_or('◆');
460            let color = entity.formation_colors.get(i).copied().unwrap_or(glam::Vec4::ONE);
461            let id = self.scene.spawn_glyph(Glyph {
462                character: ch,
463                position: entity.position + offset,
464                color,
465                emission: 0.8,
466                glow_color: glam::Vec3::new(color.x, color.y, color.z),
467                glow_radius: 1.2,
468                mass: entity.entity_mass / entity.formation.len().max(1) as f32,
469                layer: RenderLayer::Entity,
470                ..Default::default()
471            });
472            entity.glyph_ids.push(id);
473        }
474        self.scene.spawn_entity(entity)
475    }
476
477    /// Emit a burst of particles at a position.
478    pub fn emit_particles(&mut self, emitter: particle::EmitterPreset, origin: glam::Vec3) {
479        particle::emit(&mut self.scene, emitter, origin);
480    }
481
482    /// Apply trauma (screen shake). 0.0 = none, 1.0 = maximum.
483    pub fn add_trauma(&mut self, amount: f32) {
484        self.camera.add_trauma(amount);
485    }
486}
487
488/// Request quit on next frame.
489impl ProofEngine {
490    /// Ask the run loop to exit at the end of the current frame.
491    pub fn request_quit(&mut self) {
492        self.input.quit_requested = true;
493    }
494
495    /// Get a reference to the glow GL context (for egui integration).
496    /// Returns None if the pipeline hasn't been initialized yet.
497    pub fn gl(&self) -> Option<&glow::Context> {
498        self.pipeline.as_ref().map(|p| p.gl())
499    }
500
501    /// Get the window reference (for egui-winit event processing).
502    pub fn window(&self) -> Option<&winit::window::Window> {
503        self.pipeline.as_ref().map(|p| p.window())
504    }
505
506    /// Get the current window size in pixels.
507    pub fn window_size(&self) -> (u32, u32) {
508        self.pipeline.as_ref().map(|p| p.window_size()).unwrap_or((1600, 1000))
509    }
510
511    /// The framebuffer size, in the same units the viewport uses.
512    ///
513    /// Screen-space UI must lay out against this, not the window size: on a
514    /// scaled display the two differ and the UI ends up magnified.
515    pub fn render_size(&self) -> (u32, u32) {
516        self.pipeline.as_ref().map(|p| p.render_size()).unwrap_or((1600, 1000))
517    }
518
519    /// The frame currently in the back buffer, as RGBA8 with the top row
520    /// first, or `None` before the window exists.
521    ///
522    /// Read straight off the GPU after post-processing, so it is exactly
523    /// what the pipeline drew. Asking the window manager for a picture of a
524    /// hardware-accelerated window is unreliable: it hands back whatever it
525    /// last cached, which can be a stale frame or a blank one.
526    pub fn frame_pixels(&self) -> Option<(u32, u32, Vec<u8>)> {
527        let p = self.pipeline.as_ref()?;
528        let (w, h, mut rgba) = p.read_frame();
529        if w == 0 || h == 0 {
530            return None;
531        }
532        export::flip_rows(w, h, &mut rgba);
533        Some((w, h, rgba))
534    }
535
536    /// Write the frame currently on screen to an image file.
537    ///
538    /// The extension picks the format (`.png`, `.jpg`, `.bmp`, `.tga`,
539    /// `.gif`); an unknown extension writes a BMP, as this always did
540    /// before. The image is opaque. See [`export`] for animated GIFs and for
541    /// saving buffers of your own.
542    pub fn save_frame(&self, path: &str) -> std::io::Result<()> {
543        let (w, h, rgba) = self.frame_pixels().ok_or_else(|| {
544            std::io::Error::new(std::io::ErrorKind::Other, "no frame to read: the window is not open")
545        })?;
546        export::save_opaque(path, w, h, &rgba)
547    }
548}
549
550/// Common imports for using Proof Engine.
551pub mod prelude {
552    pub use crate::{
553        ProofEngine, EngineConfig,
554        MathFunction, ForceField, Falloff, AttractorType,
555        Glyph, RenderLayer, BlendMode,
556        AmorphousEntity,
557        MathParticle, ParticleInteraction,
558        AudioEvent,
559        particle::EmitterPreset,
560        render::camera::ProofCamera,
561        input::{InputState, Key},
562        scene::{SceneGraph, FieldId},
563        audio::MusicVibe,
564        tween::{Tween, Easing, TweenState, Tweens, AnimationGroup},
565        tween::easing::Easing as EasingFn,
566        tween::keyframe::{KeyframeTrack, Keyframe, CameraPath, ExtrapolateMode},
567        tween::sequence::{TweenSequence, TweenTimeline, SequenceBuilder},
568        debug::DebugOverlay,
569        render::pipeline::FrameStats,
570        render::screen_fx::{ScreenFx, ScreenLight, Shockwave},
571        render::ui_layer::UiPass,
572    };
573    // Quat and Mat4 belong here too: the skeleton and animation APIs hand out
574    // transforms built from them, so a caller who only has the prelude cannot
575    // pose a figure without reaching past it into glam.
576    pub use glam::{Mat4, Quat, Vec2, Vec3, Vec4};
577}