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