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, 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;
106pub mod character;
107pub mod dsp;
108pub mod game;
109pub mod profiler;
110pub mod vfx;
111pub mod netcode;
112pub mod network;
113pub mod world;
114pub mod crafting;
115pub mod pathfinding;
116pub mod economy;
117pub mod behavior;
118pub mod weather;
119pub mod deferred;
120pub mod shader_graph;
121pub mod surfaces;
122pub mod rendergraph;
123pub mod compute;
124pub mod lighting;
125pub mod number_theory;
126pub mod graph;
127pub mod topology;
128pub mod stochastic;
129pub mod ml;
130pub mod wgpu_backend;
131pub mod geometry;
132pub mod symbolic;
133pub mod solver;
134pub mod fractal;
135pub mod metaball;
136pub mod worldgen;
137pub mod ecology;
138pub mod narrative;
139pub mod electromagnetic;
140pub mod relativistic;
141pub mod quantum;
142pub mod svogi;
143pub mod curves;
144pub mod nishita_sky;
145pub mod volumetric_fog;
146pub mod tiled_lighting;
147mod capture;
148
149pub use config::EngineConfig;
150pub use math::{MathFunction, ForceField, Falloff, AttractorType};
151pub use glyph::{Glyph, RenderLayer, BlendMode};
152pub use entity::AmorphousEntity;
153pub use particle::{MathParticle, ParticleInteraction};
154pub use scene::SceneGraph;
155pub use render::camera::ProofCamera;
156pub use input::{InputState, Key};
157pub use render::pipeline::FrameStats;
158pub use audio::AudioEvent;
159
160/// The main engine struct. Create once, run forever.
161pub struct ProofEngine {
162    pub config: EngineConfig,
163    pub scene: SceneGraph,
164    pub camera: ProofCamera,
165    pub input: InputState,
166    /// Screen-space UI, in pixel coordinates. Cleared at the start of every
167    /// frame by `run_ui`, so games redraw it immediate-mode style.
168    pub ui: render::ui_layer::UiLayer,
169    /// Transient screen effects: shockwaves, flashes, light shafts. Fire and
170    /// forget; ticked and uploaded by `run_ui` every frame.
171    pub fx: render::screen_fx::ScreenFx,
172    /// GPU density entities queued for this frame. Drained after the render.
173    density_queue: Vec<particle::gpu_density::GpuDensityEntityData>,
174    /// The particle budget per density entity, set by `init_gpu_density`.
175    density_budget: u32,
176    /// Optional audio engine — None if no output device is available.
177    pub audio: Option<audio::AudioEngine>,
178    // Internal render pipeline (initialized lazily when run() is called)
179    pipeline: Option<render::Pipeline>,
180}
181
182impl ProofEngine {
183    pub fn new(mut config: EngineConfig) -> Self {
184        if let Some((w, h)) = capture::window_size() {
185            config.window_width = w;
186            config.window_height = h;
187        }
188        let audio = if config.audio.enabled {
189            audio::AudioEngine::try_new()
190        } else {
191            None
192        };
193        Self {
194            camera: ProofCamera::new(&config),
195            scene: SceneGraph::new(),
196            input: InputState::new(),
197            ui: render::ui_layer::UiLayer::new(
198                config.window_width as f32,
199                config.window_height as f32,
200            ),
201            fx: render::screen_fx::ScreenFx::new(),
202            density_queue: Vec::new(),
203            density_budget: 0,
204            audio,
205            config,
206            pipeline: None,
207        }
208    }
209
210    /// Turn on GPU density entities with a per-entity particle budget.
211    ///
212    /// The budget is capped at
213    /// [`MAX_PARTICLES_PER_ENTITY`](particle::gpu_density::MAX_PARTICLES_PER_ENTITY):
214    /// past that there are more particles than pixels and the picture stops
215    /// improving while the frame time keeps climbing. Asking for more is
216    /// fine; you get the cap and a log line.
217    pub fn init_gpu_density(&mut self, particles: u32) {
218        let cap = particle::gpu_density::MAX_PARTICLES_PER_ENTITY;
219        if particles > cap {
220            log::info!("gpu density: {particles} particles requested, drawing {cap} per entity");
221        }
222        self.density_budget = particles.min(cap);
223    }
224
225    /// Draw a density entity this frame. Call every frame it should show.
226    pub fn queue_gpu_density_entity(&mut self, entity: particle::gpu_density::GpuDensityEntityData) {
227        self.density_queue.push(entity);
228    }
229
230    /// Send an audio event. No-op if audio is unavailable.
231    pub fn emit_audio(&self, event: audio::AudioEvent) {
232        if let Some(ref a) = self.audio {
233            a.emit(event);
234        }
235    }
236
237    /// Run the engine. Calls `update` every frame with elapsed seconds.
238    /// Blocks until the window is closed.
239    pub fn run<F>(&mut self, mut update: F)
240    where
241        F: FnMut(&mut ProofEngine, f32),
242    {
243        self.run_with_overlay(move |engine, dt, _gl| {
244            update(engine, dt);
245        });
246    }
247
248    /// Run the engine with an overlay callback.
249    /// The overlay callback receives the glow GL context reference and is called
250    /// AFTER scene rendering but BEFORE buffer swap — perfect for egui.
251    pub fn run_with_overlay<F>(&mut self, mut update: F)
252    where
253        F: FnMut(&mut ProofEngine, f32, &glow::Context),
254    {
255        let pipeline = render::Pipeline::init(&self.config);
256        self.pipeline = Some(pipeline);
257        let mut capture = capture::Capture::from_env();
258        let fixed_dt = capture::fixed_dt();
259
260        let mut last = std::time::Instant::now();
261        loop {
262            let now = std::time::Instant::now();
263            let dt = fixed_dt.unwrap_or_else(|| now.duration_since(last).as_secs_f32().min(0.1));
264            last = now;
265
266            // Poll input
267            if let Some(ref mut p) = self.pipeline {
268                if !p.poll_events(&mut self.input) {
269                    break;
270                }
271            }
272
273            // Step force fields and physics
274            self.scene.tick(dt);
275
276            // User update (logic only — no GL calls here)
277            // We pass a dummy gl ref for the logic phase; the real painting
278            // happens after the scene render.
279            let gl_ptr = self.pipeline.as_ref().map(|p| p.gl() as *const glow::Context);
280
281            // Sync render config so runtime changes (particle_multiplier, bloom, etc.)
282            // take effect this frame.
283            if let Some(ref mut p) = self.pipeline {
284                p.update_render_config(&self.config.render);
285            }
286
287            // Render scene first
288            if let Some(ref mut p) = self.pipeline {
289                p.set_density_entities(&self.density_queue, self.density_budget);
290                p.render(&self.scene, &self.camera);
291            }
292            self.density_queue.clear();
293            self.fx.lights.clear();
294
295            // NOW paint the overlay (egui) on top of the rendered scene
296            if let Some(ptr) = gl_ptr {
297                let gl_ref = unsafe { &*ptr };
298                update(self, dt, gl_ref);
299            }
300
301            // PROOF_SHOT: read the finished frame back before it is swapped away.
302            if let Some(ref mut c) = capture {
303                if c.after_draw(self) {
304                    break;
305                }
306            }
307
308            // Honour a quit asked for during the update, as `run_ui` does.
309            if self.input.quit_requested {
310                break;
311            }
312
313            // Swap
314            if let Some(ref mut p) = self.pipeline {
315                if !p.swap() {
316                    break;
317                }
318            }
319        }
320    }
321
322    /// Run a UI-driven game.
323    ///
324    /// Unlike [`run`], `update` is called *before* the scene is drawn, and the
325    /// screen-space `ui` layer is painted afterwards. That ordering matters for
326    /// a game: what you push this frame is what appears this frame, rather than
327    /// showing up one frame late.
328    ///
329    /// The UI layer is cleared before each `update`, so games redraw it in full
330    /// every frame instead of tracking what to erase.
331    pub fn run_ui<F>(&mut self, mut update: F)
332    where
333        F: FnMut(&mut ProofEngine, f32),
334    {
335        let pipeline = render::Pipeline::init(&self.config);
336        self.pipeline = Some(pipeline);
337
338        // Size the UI layer from the framebuffer, which is what the viewport
339        // uses; the window's own size can differ on a scaled display.
340        let (w, h) = self.render_size();
341        self.ui.resize(w as f32, h as f32);
342
343        let mut capture = capture::Capture::from_env();
344        let fixed_dt = capture::fixed_dt();
345
346        let mut last = std::time::Instant::now();
347        let mut last_size = (w, h);
348        loop {
349            let now = std::time::Instant::now();
350            let dt = fixed_dt.unwrap_or_else(|| now.duration_since(last).as_secs_f32().min(0.1));
351            last = now;
352
353            if let Some(ref mut p) = self.pipeline {
354                if !p.poll_events(&mut self.input) {
355                    break;
356                }
357            }
358
359            // Keep the UI projection matched to the framebuffer.
360            let size = self.render_size();
361            if size != last_size {
362                last_size = size;
363                self.ui.resize(size.0 as f32, size.1 as f32);
364            }
365
366            self.scene.tick(dt);
367
368            // Game logic and UI construction, both before anything is drawn.
369            self.ui.begin_frame();
370            update(self, dt);
371
372            // Honour a quit asked for during the update.
373            //
374            // `request_quit` used to set a flag that nothing read, so a game's
375            // own Quit menu did nothing at all and the only way out was the
376            // window's close button. The check goes here, after the update and
377            // before the render, so the frame that asked to quit is the last
378            // one and nothing half-drawn reaches the screen.
379            if self.input.quit_requested {
380                break;
381            }
382
383            // Trauma decays here. It used to be added and never ticked in
384            // this loop, so the first hit left the camera shaking forever.
385            self.camera.shake.tick(dt);
386            self.fx.tick(dt);
387
388            if let Some(ref mut p) = self.pipeline {
389                p.update_render_config(&self.config.render);
390                p.set_density_entities(&self.density_queue, self.density_budget);
391                // The scene, then the UI's world pass into the same buffer,
392                // then post-processing over both.
393                p.render_frame(&self.scene, &self.camera, Some(&self.ui), &self.fx);
394                // The HUD, painted after post-processing so it stays sharp.
395                p.render_ui(&self.ui);
396            }
397            self.density_queue.clear();
398            self.fx.lights.clear();
399
400            if let Some(ref mut c) = capture {
401                if c.after_draw(self) {
402                    break;
403                }
404            }
405
406            if let Some(ref mut p) = self.pipeline {
407                if !p.swap() {
408                    break;
409                }
410            }
411        }
412    }
413
414    /// Add a force field to the scene.
415    pub fn add_field(&mut self, field: ForceField) -> scene::FieldId {
416        self.scene.add_field(field)
417    }
418
419    /// Remove a force field.
420    pub fn remove_field(&mut self, id: scene::FieldId) {
421        self.scene.remove_field(id)
422    }
423
424    /// Spawn a glyph into the scene.
425    pub fn spawn_glyph(&mut self, glyph: Glyph) -> glyph::GlyphId {
426        self.scene.spawn_glyph(glyph)
427    }
428
429    /// Spawn an amorphous entity, creating its formation glyphs.
430    pub fn spawn_entity(&mut self, mut entity: AmorphousEntity) -> entity::EntityId {
431        // If no formation was specified, generate a default diamond
432        if entity.formation.is_empty() {
433            use entity::formation::Formation;
434            let f = Formation::diamond(2);
435            entity.formation = f.positions;
436            entity.formation_chars = f.chars;
437        }
438        // Ensure colors are filled (white if unspecified)
439        while entity.formation_colors.len() < entity.formation.len() {
440            entity.formation_colors.push(glam::Vec4::ONE);
441        }
442        // Spawn one glyph per formation slot
443        for i in 0..entity.formation.len() {
444            let offset = entity.formation[i];
445            let ch = entity.formation_chars.get(i).copied().unwrap_or('◆');
446            let color = entity.formation_colors.get(i).copied().unwrap_or(glam::Vec4::ONE);
447            let id = self.scene.spawn_glyph(Glyph {
448                character: ch,
449                position: entity.position + offset,
450                color,
451                emission: 0.8,
452                glow_color: glam::Vec3::new(color.x, color.y, color.z),
453                glow_radius: 1.2,
454                mass: entity.entity_mass / entity.formation.len().max(1) as f32,
455                layer: RenderLayer::Entity,
456                ..Default::default()
457            });
458            entity.glyph_ids.push(id);
459        }
460        self.scene.spawn_entity(entity)
461    }
462
463    /// Emit a burst of particles at a position.
464    pub fn emit_particles(&mut self, emitter: particle::EmitterPreset, origin: glam::Vec3) {
465        particle::emit(&mut self.scene, emitter, origin);
466    }
467
468    /// Apply trauma (screen shake). 0.0 = none, 1.0 = maximum.
469    pub fn add_trauma(&mut self, amount: f32) {
470        self.camera.add_trauma(amount);
471    }
472}
473
474/// Request quit on next frame.
475impl ProofEngine {
476    pub fn request_quit(&mut self) {
477        self.input.quit_requested = true;
478    }
479
480    /// Get a reference to the glow GL context (for egui integration).
481    /// Returns None if the pipeline hasn't been initialized yet.
482    pub fn gl(&self) -> Option<&glow::Context> {
483        self.pipeline.as_ref().map(|p| p.gl())
484    }
485
486    /// Get the window reference (for egui-winit event processing).
487    pub fn window(&self) -> Option<&winit::window::Window> {
488        self.pipeline.as_ref().map(|p| p.window())
489    }
490
491    /// Get the current window size in pixels.
492    pub fn window_size(&self) -> (u32, u32) {
493        self.pipeline.as_ref().map(|p| p.window_size()).unwrap_or((1600, 1000))
494    }
495
496    /// The framebuffer size, in the same units the viewport uses.
497    ///
498    /// Screen-space UI must lay out against this, not the window size: on a
499    /// scaled display the two differ and the UI ends up magnified.
500    pub fn render_size(&self) -> (u32, u32) {
501        self.pipeline.as_ref().map(|p| p.render_size()).unwrap_or((1600, 1000))
502    }
503
504    /// Write the frame currently on screen to an uncompressed 24-bit BMP.
505    ///
506    /// The point of this is being able to see what the engine actually drew.
507    /// Asking the window manager for a picture of a hardware-accelerated window
508    /// is unreliable — it hands back whatever it last cached, which can be a
509    /// stale frame or a blank one — so the only trustworthy answer comes from
510    /// reading the framebuffer back off the GPU.
511    ///
512    /// BMP because it needs no compression and therefore no dependency; the
513    /// row order matches OpenGL's, so no flip is needed either.
514    pub fn save_frame(&self, path: &str) -> std::io::Result<()> {
515        use std::io::Write;
516        let Some(p) = self.pipeline.as_ref() else {
517            return Err(std::io::Error::new(
518                std::io::ErrorKind::Other,
519                "no pipeline to read from",
520            ));
521        };
522        let (w, h, rgba) = p.read_frame();
523        if w == 0 || h == 0 {
524            return Err(std::io::Error::new(
525                std::io::ErrorKind::Other,
526                "empty framebuffer",
527            ));
528        }
529
530        // Each BMP row is padded to a multiple of four bytes.
531        let stride = ((w as usize * 3) + 3) & !3;
532        let pixels = stride * h as usize;
533        let mut out = Vec::with_capacity(54 + pixels);
534        out.extend_from_slice(b"BM");
535        out.extend_from_slice(&((54 + pixels) as u32).to_le_bytes());
536        out.extend_from_slice(&0u32.to_le_bytes());
537        out.extend_from_slice(&54u32.to_le_bytes());
538        out.extend_from_slice(&40u32.to_le_bytes());
539        out.extend_from_slice(&(w as i32).to_le_bytes());
540        out.extend_from_slice(&(h as i32).to_le_bytes());
541        out.extend_from_slice(&1u16.to_le_bytes());
542        out.extend_from_slice(&24u16.to_le_bytes());
543        for _ in 0..6 {
544            out.extend_from_slice(&0u32.to_le_bytes());
545        }
546
547        for y in 0..h as usize {
548            let row = y * w as usize * 4;
549            for x in 0..w as usize {
550                let i = row + x * 4;
551                // BMP stores blue first.
552                out.push(rgba[i + 2]);
553                out.push(rgba[i + 1]);
554                out.push(rgba[i]);
555            }
556            for _ in 0..(stride - w as usize * 3) {
557                out.push(0);
558            }
559        }
560
561        let mut f = std::fs::File::create(path)?;
562        f.write_all(&out)
563    }
564}
565
566/// Common imports for using Proof Engine.
567pub mod prelude {
568    pub use crate::{
569        ProofEngine, EngineConfig,
570        MathFunction, ForceField, Falloff, AttractorType,
571        Glyph, RenderLayer, BlendMode,
572        AmorphousEntity,
573        MathParticle, ParticleInteraction,
574        AudioEvent,
575        particle::EmitterPreset,
576        render::camera::ProofCamera,
577        input::{InputState, Key},
578        scene::{SceneGraph, FieldId},
579        audio::MusicVibe,
580        tween::{Tween, Easing, TweenState, Tweens, AnimationGroup},
581        tween::easing::Easing as EasingFn,
582        tween::keyframe::{KeyframeTrack, Keyframe, CameraPath, ExtrapolateMode},
583        tween::sequence::{TweenSequence, TweenTimeline, SequenceBuilder},
584        debug::DebugOverlay,
585        render::pipeline::FrameStats,
586        render::screen_fx::{ScreenFx, ScreenLight, Shockwave},
587        render::ui_layer::UiPass,
588    };
589    // Quat and Mat4 belong here too: the skeleton and animation APIs hand out
590    // transforms built from them, so a caller who only has the prelude cannot
591    // pose a figure without reaching past it into glam.
592    pub use glam::{Mat4, Quat, Vec2, Vec3, Vec4};
593}