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://raw.githubusercontent.com/Mattbusel/proof-engine/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://github.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}