use std::num::NonZeroU32;
use std::ffi::CString;
use std::time::{Duration, Instant};
use glutin::config::ConfigTemplateBuilder;
use glutin::context::{ContextApi, ContextAttributesBuilder, NotCurrentGlContext,
PossiblyCurrentContext, Version};
use glutin::display::{GetGlDisplay, GlDisplay};
use glutin::surface::{GlSurface, Surface, WindowSurface};
use glutin_winit::{DisplayBuilder, GlWindow};
use glow::HasContext;
use raw_window_handle::HasWindowHandle;
use winit::dpi::LogicalSize;
use winit::event::{ElementState, Event, MouseButton, MouseScrollDelta, WindowEvent};
use winit::event_loop::EventLoop;
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus};
use winit::window::Window;
use glam::{Mat4, Vec2, Vec3};
use bytemuck::cast_slice;
use crate::config::{EngineConfig, RenderConfig};
use crate::scene::Scene;
use crate::render::camera::ProofCamera;
use crate::render::postfx::PostFxPipeline;
use crate::input::{InputState, Key};
use crate::glyph::atlas::FontAtlas;
use crate::glyph::batch::GlyphInstance;
use crate::render::ui_layer::UiLayer;
use crate::render::screen_fx::ScreenFx;
use crate::particle::gpu_density::{GpuDensityEntityData, GpuDensityRenderer};
const VERT_SRC: &str = r#"
#version 330 core
layout(location = 0) in vec2 v_pos;
layout(location = 1) in vec2 v_uv;
layout(location = 2) in vec3 i_position;
layout(location = 3) in vec2 i_scale;
layout(location = 4) in float i_rotation;
layout(location = 5) in vec4 i_color;
layout(location = 6) in float i_emission;
layout(location = 7) in vec3 i_glow_color;
layout(location = 8) in float i_glow_radius;
layout(location = 9) in vec2 i_uv_offset;
layout(location = 10) in vec2 i_uv_size;
// x: 1.0 for a fill (ground, panel) that must not cast shadow.
layout(location = 11) in vec2 i_flags;
uniform mat4 u_view_proj;
// Oversampling: each base instance is rendered u_n_copies times.
// vertex_attrib_divisor is set to u_n_copies so all copies share the same
// instance attributes. gl_InstanceID / u_n_copies selects the base particle;
// gl_InstanceID % u_n_copies selects the copy and drives the jitter hash.
uniform uint u_n_copies;
out vec2 f_uv;
out vec4 f_color;
out float f_emission;
out vec3 f_glow_color;
out float f_glow_radius;
out float f_fill;
// Hash matching the CPU hf(seed, v) function — same constants, same bit ops.
float hf(uint seed, uint v) {
uint n = seed * 374761393u + v * 668265263u;
n ^= (n >> 13u);
n *= 0x5851F42Du;
n ^= (n >> 16u);
return float(n & 0x00FFFFFFu) / float(0x01000000u);
}
void main() {
float c = cos(i_rotation);
float s = sin(i_rotation);
vec2 rotated = vec2(
v_pos.x * c - v_pos.y * s,
v_pos.x * s + v_pos.y * c
) * i_scale;
// Per-copy position jitter — only active when oversampling.
// Seeds mirror the CPU oversampling: seed = real_id*17 + copy_id, slots 30/31.
vec3 jitter = vec3(0.0);
if (u_n_copies > 1u) {
uint real_id = uint(gl_InstanceID) / u_n_copies;
uint copy_id = uint(gl_InstanceID) % u_n_copies;
uint jseed = real_id * 17u + copy_id;
jitter = vec3(
(hf(jseed, 30u) - 0.5) * i_scale.x * 0.70,
(hf(jseed, 31u) - 0.5) * i_scale.y * 0.70,
0.0
);
}
gl_Position = u_view_proj * vec4(i_position + jitter + vec3(rotated, 0.0), 1.0);
gl_Position.y = -gl_Position.y; // FBO renders upside-down relative to screen
f_uv = i_uv_offset + v_uv * i_uv_size;
// Divide alpha and emission by n_copies so n_copies additive contributions
// sum to the same luminance as a single unscaled instance.
float inv_n = 1.0 / float(u_n_copies);
f_color = vec4(i_color.rgb, i_color.a * inv_n);
f_emission = i_emission * inv_n;
f_glow_color = i_glow_color;
f_glow_radius = i_glow_radius;
f_fill = i_flags.x;
}
"#;
const FRAG_SRC: &str = r#"
#version 330 core
in vec2 f_uv;
in vec4 f_color;
in float f_emission;
in vec3 f_glow_color;
in float f_glow_radius;
in float f_fill;
uniform sampler2D u_atlas;
layout(location = 0) out vec4 o_color;
layout(location = 1) out vec4 o_emission;
// Coverage of matter, for the light map's shadows. Fills write none, and
// cover whatever was under them.
layout(location = 2) out vec4 o_occluder;
void main() {
float dist = texture(u_atlas, f_uv).r;
// SDF thresholds (in normalized distance, 0.5 = edge)
float edge = 0.5;
float softness = 0.08; // edge softness (antialiasing)
// Core glyph alpha (sharp edge with antialiasing)
float alpha = smoothstep(edge - softness, edge + softness, dist);
// Outline: a band just outside the edge
float outline_width = 0.06;
float outline_alpha = smoothstep(edge - outline_width - softness, edge - outline_width, dist)
* (1.0 - smoothstep(edge - softness * 0.5, edge + softness * 0.5, dist));
vec3 outline_color = f_glow_color * 0.6;
// Glow halo: soft falloff outside the glyph
float glow_size = 0.15 + f_glow_radius * 0.05;
float glow_alpha = smoothstep(edge - glow_size - 0.1, edge - 0.02, dist) * (1.0 - alpha);
glow_alpha *= clamp(f_glow_radius * 0.3, 0.0, 0.5);
// Drop shadow (offset sample)
vec2 shadow_offset = vec2(0.003, -0.004);
float shadow_dist = texture(u_atlas, f_uv + shadow_offset).r;
float shadow_alpha = smoothstep(edge - softness, edge + softness, shadow_dist) * 0.2;
// Discard if nothing visible
float total_alpha = max(max(alpha, outline_alpha), max(glow_alpha, shadow_alpha));
if (total_alpha < 0.01) discard;
// Composite layers
float em = clamp(f_emission * 0.5, 0.0, 1.0);
vec3 base_col = mix(f_color.rgb, f_glow_color, em);
// Shadow (darkest layer)
vec3 col = vec3(0.0);
float a = shadow_alpha * f_color.a * 0.3;
// Glow halo (behind outline)
col = mix(col, f_glow_color * 0.5, glow_alpha);
a = max(a, glow_alpha * f_color.a * 0.4);
// Outline
col = mix(col, outline_color, outline_alpha);
a = max(a, outline_alpha * f_color.a * 0.7);
// Core fill (on top)
col = mix(col, base_col, alpha);
a = max(a, alpha * f_color.a);
o_color = vec4(col, a);
// Emission for bloom
float bloom_strength = clamp(f_emission - 0.3, 0.0, 1.0);
float glow_boost = clamp(f_glow_radius * 0.15, 0.0, 0.8);
float em_alpha = max(alpha, glow_alpha * 0.5) * f_color.a;
o_emission = vec4(f_glow_color * (bloom_strength + glow_boost), em_alpha);
o_occluder = vec4(alpha * f_color.a * (1.0 - f_fill), 0.0, 0.0, a);
}
"#;
#[rustfmt::skip]
const QUAD_VERTS: [f32; 24] = [
-0.5, 0.5, 0.0, 1.0,
-0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 1.0, 1.0,
-0.5, -0.5, 0.0, 0.0,
0.5, -0.5, 1.0, 0.0,
0.5, 0.5, 1.0, 1.0,
];
#[derive(Clone, Debug, Default)]
pub struct FrameStats {
pub fps: f32,
pub dt: f32,
pub glyph_count: usize,
pub particle_count: usize,
pub draw_calls: u32,
pub frame_number: u64,
}
struct FpsCounter {
samples: [f32; 60],
head: usize,
filled: bool,
}
impl FpsCounter {
fn new() -> Self { Self { samples: [0.016; 60], head: 0, filled: false } }
fn push(&mut self, dt: f32) {
self.samples[self.head] = dt.max(f32::EPSILON);
self.head = (self.head + 1) % 60;
if self.head == 0 { self.filled = true; }
}
fn fps(&self) -> f32 {
let count = if self.filled { 60 } else { self.head.max(1) };
let avg_dt: f32 = self.samples[..count].iter().sum::<f32>() / count as f32;
1.0 / avg_dt
}
}
#[allow(dead_code)]
pub struct Pipeline {
pub width: u32,
pub height: u32,
pub stats: FrameStats,
running: bool,
render_config: RenderConfig,
event_loop: EventLoop<()>,
window: Window,
surface: Surface<WindowSurface>,
context: PossiblyCurrentContext,
gl: glow::Context,
program: glow::Program,
vao: glow::VertexArray,
quad_vbo: glow::Buffer,
instance_vbo: glow::Buffer,
atlas_tex: glow::Texture,
loc_view_proj: glow::UniformLocation,
loc_n_copies: Option<glow::UniformLocation>,
postfx: PostFxPipeline,
atlas: FontAtlas,
ui_renderer: super::ui_layer_renderer::UiLayerRenderer,
ui_prepared: bool,
density: Option<GpuDensityRenderer>,
density_entities: Vec<GpuDensityEntityData>,
density_budget: u32,
pub svogi: crate::svogi::integration::CascadedSvogi,
pub fog: crate::volumetric_fog::VolumetricFogPipeline,
instances: Vec<GlyphInstance>,
fps_counter: FpsCounter,
frame_start: Instant,
scene_time: f32,
mouse_pos: Vec2,
mouse_pos_prev: Vec2,
mouse_ndc: Vec2,
pub raw_window_events: Vec<winit::event::WindowEvent>,
}
impl Pipeline {
pub fn init(config: &EngineConfig) -> Self {
let event_loop = EventLoop::new().expect("EventLoop::new");
let window_attrs = Window::default_attributes()
.with_title(&config.window_title)
.with_inner_size(LogicalSize::new(config.window_width, config.window_height))
.with_resizable(true)
.with_visible(!crate::capture::hidden_window())
.with_active(!crate::capture::hidden_window());
let template = ConfigTemplateBuilder::new()
.with_alpha_size(8)
.with_depth_size(0);
let display_builder = DisplayBuilder::new()
.with_window_attributes(Some(window_attrs));
let (window, gl_config) = display_builder
.build(&event_loop, template, |mut configs| {
configs.next().expect("no suitable GL config found")
})
.expect("DisplayBuilder::build failed");
let window = window.expect("window was not created");
let display = gl_config.display();
let raw_handle = window.window_handle().unwrap().as_raw();
let ctx_attrs_43 = ContextAttributesBuilder::new()
.with_context_api(ContextApi::OpenGl(Some(Version::new(4, 3))))
.build(Some(raw_handle));
let ctx_attrs_33 = ContextAttributesBuilder::new()
.with_context_api(ContextApi::OpenGl(Some(Version::new(3, 3))))
.build(Some(raw_handle));
let not_current = unsafe {
display.create_context(&gl_config, &ctx_attrs_43)
.unwrap_or_else(|_| {
display.create_context(&gl_config, &ctx_attrs_33)
.expect("create_context failed (both GL 4.3 and 3.3)")
})
};
let size = window.inner_size();
let w = size.width.max(1);
let h = size.height.max(1);
let surface_attrs = window
.build_surface_attributes(Default::default())
.expect("build_surface_attributes failed");
let surface = unsafe {
display.create_window_surface(&gl_config, &surface_attrs)
.expect("create_window_surface failed")
};
let context = not_current.make_current(&surface)
.expect("make_current failed");
if config.render.vsync {
use glutin::surface::SwapInterval;
if let Err(e) = surface.set_swap_interval(
&context,
SwapInterval::Wait(NonZeroU32::new(1).unwrap()),
) {
log::warn!("vsync unavailable: {e}");
}
}
let gl = unsafe {
glow::Context::from_loader_function(|sym| {
let sym_c = CString::new(sym).unwrap();
display.get_proc_address(sym_c.as_c_str()) as *const _
})
};
let program = unsafe { compile_program(&gl, VERT_SRC, FRAG_SRC) };
let loc_view_proj = unsafe {
gl.get_uniform_location(program, "u_view_proj")
.expect("uniform u_view_proj not found")
};
let loc_n_copies = unsafe {
gl.get_uniform_location(program, "u_n_copies")
};
unsafe {
gl.use_program(Some(program));
if let Some(loc) = gl.get_uniform_location(program, "u_atlas") {
gl.uniform_1_i32(Some(&loc), 0);
}
if let Some(ref loc) = loc_n_copies {
gl.uniform_1_u32(Some(loc), 1u32);
}
}
let (vao, quad_vbo, instance_vbo) = unsafe { setup_vao(&gl) };
let atlas = FontAtlas::build(config.render.font_size as f32);
let atlas_tex = unsafe { upload_atlas(&gl, &atlas) };
let postfx = unsafe { PostFxPipeline::new(&gl, w, h, config.render.render_scale) };
unsafe {
gl.enable(glow::BLEND);
gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA);
gl.clear_color(0.02, 0.02, 0.05, 1.0);
gl.viewport(0, 0, w as i32, h as i32);
}
log::info!(
"Pipeline ready — {}×{} — font atlas {}×{} ({} chars) — PostFxPipeline wired",
w, h, atlas.width, atlas.height, atlas.uvs.len()
);
Self {
width: w, height: h,
stats: FrameStats::default(),
running: true,
render_config: config.render.clone(),
event_loop, window, surface, context,
gl, program, vao, quad_vbo, instance_vbo, atlas_tex, loc_view_proj, loc_n_copies,
postfx,
atlas,
ui_renderer: super::ui_layer_renderer::UiLayerRenderer::new(),
ui_prepared: false,
density: None,
density_entities: Vec::new(),
density_budget: 0,
instances: Vec::with_capacity(8192),
fps_counter: FpsCounter::new(),
frame_start: Instant::now(),
scene_time: 0.0,
mouse_pos: Vec2::ZERO,
mouse_pos_prev: Vec2::ZERO,
mouse_ndc: Vec2::ZERO,
raw_window_events: Vec::new(),
svogi: crate::svogi::integration::CascadedSvogi::new(3, 64, 50.0),
fog: crate::volumetric_fog::VolumetricFogPipeline::new(
crate::volumetric_fog::FogPresets::combat()
),
}
}
pub fn set_density_entities(&mut self, entities: &[GpuDensityEntityData], budget: u32) {
self.density_entities.clear();
self.density_entities.extend_from_slice(entities);
self.density_budget = budget;
if !self.density_entities.is_empty() && self.density.is_none() {
self.density = Some(unsafe { GpuDensityRenderer::new(&self.gl) });
}
}
pub fn update_render_config(&mut self, config: &RenderConfig) {
let old_scale = self.render_config.render_scale;
self.render_config = config.clone();
if (config.render_scale - old_scale).abs() > 1e-4 {
unsafe { self.postfx.resize(&self.gl, self.width, self.height, config.render_scale); }
}
}
pub fn poll_events(&mut self, input: &mut InputState) -> bool {
input.clear_frame();
self.mouse_pos_prev = self.mouse_pos;
let mut should_exit = false;
let mut resize: Option<(u32, u32)> = None;
let mut key_events: Vec<(KeyCode, bool)> = Vec::new();
let mut mouse_moved: Option<(f64, f64)> = None;
let mut mouse_buttons: Vec<(MouseButton, bool)> = Vec::new();
let mut scroll_delta: f32 = 0.0;
self.raw_window_events.clear();
#[allow(deprecated)]
let status = self.event_loop.pump_events(Some(Duration::ZERO), |event, elwt| {
match event {
Event::WindowEvent { event: we, .. } => match we {
WindowEvent::CloseRequested => {
should_exit = true;
elwt.exit();
}
WindowEvent::Resized(s) => {
resize = Some((s.width, s.height));
}
WindowEvent::KeyboardInput { event: key_ev, .. } => {
if let PhysicalKey::Code(kc) = key_ev.physical_key {
let pressed = key_ev.state == ElementState::Pressed;
key_events.push((kc, pressed));
}
}
WindowEvent::CursorMoved { position, .. } => {
mouse_moved = Some((position.x, position.y));
}
WindowEvent::MouseInput { button, state, .. } => {
let pressed = state == ElementState::Pressed;
mouse_buttons.push((button, pressed));
}
WindowEvent::MouseWheel { delta, .. } => {
scroll_delta += match delta {
MouseScrollDelta::LineDelta(_, y) => y,
MouseScrollDelta::PixelDelta(d) => d.y as f32 / 40.0,
};
}
_ => {}
}
_ => {}
}
});
if let Some((w, h)) = resize {
if w > 0 && h > 0 {
self.surface.resize(
&self.context,
NonZeroU32::new(w).unwrap(),
NonZeroU32::new(h).unwrap(),
);
unsafe { self.gl.viewport(0, 0, w as i32, h as i32); }
self.width = w;
self.height = h;
input.window_resized = Some((w, h));
unsafe { self.postfx.resize(&self.gl, w, h, self.render_config.render_scale); }
}
}
for (kc, pressed) in key_events {
if let Some(key) = keycode_to_engine(kc) {
if pressed {
input.keys_pressed.insert(key);
input.keys_just_pressed.insert(key);
} else {
input.keys_pressed.remove(&key);
input.keys_just_released.insert(key);
}
}
}
if let Some((x, y)) = mouse_moved {
self.mouse_pos = Vec2::new(x as f32, y as f32);
input.mouse_x = x as f32;
input.mouse_y = y as f32;
let w = self.width.max(1) as f32;
let h = self.height.max(1) as f32;
self.mouse_ndc = Vec2::new(
(x as f32 / w) * 2.0 - 1.0,
1.0 - (y as f32 / h) * 2.0,
);
input.mouse_ndc = self.mouse_ndc;
input.mouse_delta = self.mouse_pos - self.mouse_pos_prev;
}
for (button, pressed) in mouse_buttons {
match button {
MouseButton::Left => {
if pressed { input.mouse_left_just_pressed = true; }
else { input.mouse_left_just_released = true; }
input.mouse_left = pressed;
}
MouseButton::Right => {
if pressed { input.mouse_right_just_pressed = true; }
else { input.mouse_right_just_released = true; }
input.mouse_right = pressed;
}
MouseButton::Middle => {
if pressed { input.mouse_middle_just_pressed = true; }
input.mouse_middle = pressed;
}
_ => {}
}
}
input.scroll_delta = scroll_delta;
if should_exit || matches!(status, PumpStatus::Exit(_)) {
self.running = false;
}
self.running
}
pub fn render(&mut self, scene: &Scene, camera: &ProofCamera) {
let fx = ScreenFx::default();
self.render_frame(scene, camera, None, &fx);
}
pub fn render_frame(
&mut self,
scene: &Scene,
camera: &ProofCamera,
ui: Option<&UiLayer>,
fx: &ScreenFx,
) {
let now = Instant::now();
let dt = now.duration_since(self.frame_start).as_secs_f32().min(0.1);
self.frame_start = now;
self.scene_time = scene.time;
self.fps_counter.push(dt);
self.stats.fps = self.fps_counter.fps();
self.stats.dt = dt;
self.stats.frame_number += 1;
let pos = camera.position.position();
let tgt = camera.target.position();
let fov = camera.fov.position;
let aspect = if self.height > 0 { self.width as f32 / self.height as f32 } else { 1.0 };
let view = Mat4::look_at_rh(pos, tgt, Vec3::Y);
let proj = Mat4::perspective_rh_gl(fov.to_radians(), aspect, camera.near, camera.far);
let view_proj = proj * view;
if self.render_config.global_illumination {
use crate::svogi::inject::{LightSource, DirectionalLight, PointLight};
let sun = LightSource::Directional(DirectionalLight {
direction: Vec3::new(-0.5, 1.0, 0.8).normalize(),
color: Vec3::new(0.8, 0.75, 0.65),
intensity: 1.0,
});
let mut all_lights = vec![sun];
let mut emissive_count = 0;
for (_, glyph) in scene.glyphs.iter() {
if glyph.emission > 0.5 && emissive_count < 50 {
all_lights.push(LightSource::Point(PointLight {
position: glyph.position,
color: glyph.glow_color,
intensity: glyph.emission * 0.3,
radius: glyph.glow_radius * 2.0,
}));
emissive_count += 1;
}
}
self.svogi.update(dt, &[], &all_lights, &[]);
}
if self.render_config.volumetric_fog {
use crate::volumetric_fog::FogLight;
let inv_vp = view_proj.inverse();
let fog_lights = vec![
FogLight::Directional {
direction: Vec3::new(-0.3, -0.8, -0.5).normalize(),
color: Vec3::new(0.6, 0.55, 0.5),
intensity: 0.5,
},
];
self.fog.update(dt, &inv_vp, pos, &fog_lights, &[]);
}
self.instances.clear();
let mut glyph_count = 0;
let mut particle_count = 0;
for (_, glyph) in scene.glyphs.iter() {
if !glyph.visible { continue; }
let life_scale = if let Some(ref f) = glyph.life_function {
f.evaluate(scene.time, 0.0)
} else {
1.0
};
let uv = self.atlas.uv_for(glyph.character);
let dist = (glyph.position - pos).length();
let fog_density = 0.003; let fog_factor = (-dist * fog_density).exp(); let fog_color = [0.03f32, 0.04, 0.06]; let mut color = glyph.color.to_array();
color[0] = color[0] * fog_factor + fog_color[0] * (1.0 - fog_factor);
color[1] = color[1] * fog_factor + fog_color[1] * (1.0 - fog_factor);
color[2] = color[2] * fog_factor + fog_color[2] * (1.0 - fog_factor);
color[3] *= fog_factor.max(0.1); let emission_fogged = glyph.emission * fog_factor;
self.instances.push(GlyphInstance {
position: glyph.position.to_array(),
scale: [glyph.scale.x * life_scale, glyph.scale.y * life_scale],
rotation: glyph.rotation,
color,
emission: emission_fogged,
glow_color: glyph.glow_color.to_array(),
glow_radius: glyph.glow_radius * fog_factor,
uv_offset: uv.offset(),
uv_size: uv.size(),
_pad: [0.0; 2],
});
glyph_count += 1;
}
for particle in scene.particles.iter() {
let g = &particle.glyph;
if !g.visible { continue; }
let uv = self.atlas.uv_for(g.character);
let dist = (g.position - pos).length();
let fog_factor = (-dist * 0.003).exp();
let fog_color = [0.03f32, 0.04, 0.06];
let mut color = g.color.to_array();
color[0] = color[0] * fog_factor + fog_color[0] * (1.0 - fog_factor);
color[1] = color[1] * fog_factor + fog_color[1] * (1.0 - fog_factor);
color[2] = color[2] * fog_factor + fog_color[2] * (1.0 - fog_factor);
color[3] *= fog_factor.max(0.1);
self.instances.push(GlyphInstance {
position: g.position.to_array(),
scale: [g.scale.x, g.scale.y],
rotation: g.rotation,
color,
emission: g.emission * fog_factor,
glow_color: g.glow_color.to_array(),
glow_radius: g.glow_radius * fog_factor,
uv_offset: uv.offset(),
uv_size: uv.size(),
_pad: [0.0; 2],
});
particle_count += 1;
}
self.stats.glyph_count = glyph_count;
self.stats.particle_count = particle_count;
self.stats.draw_calls = 0;
let mut world_proj = None;
if let Some(ui) = ui {
self.ui_renderer.build_instances(ui, &self.atlas);
self.ui_prepared = true;
if self.render_config.world_ui_in_scene && self.ui_renderer.world_count() > 0 {
let trauma = camera.shake.trauma.clamp(0.0, 1.0);
let amp = trauma * trauma * self.render_config.shake_pixels;
let t = self.scene_time;
let shake = Vec3::new((t * 47.3).sin() * amp, (t * 31.7).cos() * amp, 0.0);
world_proj = Some(ui.world_projection() * Mat4::from_translation(shake));
}
}
unsafe { self.execute_render_passes(view_proj, world_proj, fx); }
}
pub fn render_ui(&mut self, ui: &super::ui_layer::UiLayer) {
let prepared = std::mem::replace(&mut self.ui_prepared, false);
if ui.command_count() == 0 {
return;
}
if !prepared {
self.ui_renderer.build_instances(ui, &self.atlas);
}
let proj = ui.projection();
if !self.render_config.world_ui_in_scene && self.ui_renderer.world_count() > 0 {
unsafe { self.draw_ui_pass(proj, true) };
self.stats.draw_calls += 1;
}
if self.ui_renderer.glyph_count() == 0 {
return;
}
unsafe { self.draw_ui_pass(proj, false) };
self.stats.draw_calls += 1;
}
unsafe fn draw_ui_pass(&mut self, proj: Mat4, world: bool) {
let gl = &self.gl;
let (bytes, count) = if world {
(self.ui_renderer.world_bytes(), self.ui_renderer.world_count())
} else {
(self.ui_renderer.glyph_bytes(), self.ui_renderer.glyph_count())
};
gl.bind_framebuffer(glow::FRAMEBUFFER, None);
gl.viewport(0, 0, self.width as i32, self.height as i32);
gl.disable(glow::DEPTH_TEST);
gl.enable(glow::BLEND);
gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA);
gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.instance_vbo));
gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, bytes, glow::DYNAMIC_DRAW);
gl.use_program(Some(self.program));
gl.uniform_matrix_4_f32_slice(Some(&self.loc_view_proj), false, &proj.to_cols_array());
gl.uniform_1_u32(self.loc_n_copies.as_ref(), 1);
gl.active_texture(glow::TEXTURE0);
gl.bind_texture(glow::TEXTURE_2D, Some(self.atlas_tex));
gl.bind_vertex_array(Some(self.vao));
for loc in 2u32..=11 {
gl.vertex_attrib_divisor(loc, 1);
}
gl.draw_arrays_instanced(glow::TRIANGLES, 0, 6, count as i32);
gl.enable(glow::DEPTH_TEST);
}
pub fn swap(&mut self) -> bool {
if let Err(e) = self.surface.swap_buffers(&self.context) {
log::error!("swap_buffers failed: {e}");
self.running = false;
}
self.running
}
pub fn gl(&self) -> &glow::Context {
&self.gl
}
pub fn window(&self) -> &Window {
&self.window
}
pub fn render_size(&self) -> (u32, u32) {
(self.width, self.height)
}
pub fn read_frame(&self) -> (u32, u32, Vec<u8>) {
let (w, h) = (self.width, self.height);
let mut buf = vec![0u8; (w as usize) * (h as usize) * 4];
unsafe {
let gl = &self.gl;
gl.bind_framebuffer(glow::FRAMEBUFFER, None);
gl.read_buffer(glow::BACK);
gl.pixel_store_i32(glow::PACK_ALIGNMENT, 1);
gl.read_pixels(
0,
0,
w as i32,
h as i32,
glow::RGBA,
glow::UNSIGNED_BYTE,
glow::PixelPackData::Slice(Some(&mut buf)),
);
}
(w, h, buf)
}
pub fn window_size(&self) -> (u32, u32) {
let size = self.window.inner_size();
(size.width, size.height)
}
unsafe fn execute_render_passes(
&mut self,
view_proj: Mat4,
world_proj: Option<Mat4>,
fx: &ScreenFx,
) {
let gl = &self.gl;
let (sw, sh) = self.postfx.scene_size();
gl.bind_framebuffer(glow::FRAMEBUFFER, Some(self.postfx.scene_fbo));
gl.viewport(0, 0, sw as i32, sh as i32);
gl.clear_color(0.02, 0.025, 0.04, 1.0);
gl.clear(glow::COLOR_BUFFER_BIT);
gl.clear_buffer_f32_slice(glow::COLOR, 2, &[0.0, 0.0, 0.0, 0.0]);
gl.enable(glow::BLEND);
gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA);
if !self.instances.is_empty() {
gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.instance_vbo));
gl.buffer_data_u8_slice(
glow::ARRAY_BUFFER,
cast_slice(self.instances.as_slice()),
glow::DYNAMIC_DRAW,
);
let n_copies = self.render_config.particle_multiplier
.ceil().max(1.0) as u32;
gl.use_program(Some(self.program));
gl.uniform_matrix_4_f32_slice(
Some(&self.loc_view_proj),
false,
&view_proj.to_cols_array(),
);
gl.uniform_1_u32(self.loc_n_copies.as_ref(), n_copies);
gl.active_texture(glow::TEXTURE0);
gl.bind_texture(glow::TEXTURE_2D, Some(self.atlas_tex));
gl.bind_vertex_array(Some(self.vao));
for loc in 2u32..=11 {
gl.vertex_attrib_divisor(loc, n_copies);
}
gl.draw_arrays_instanced(
glow::TRIANGLES, 0, 6,
(self.instances.len() as u32 * n_copies) as i32,
);
self.stats.draw_calls += 1;
}
if let Some(ref mut density) = self.density {
if !self.density_entities.is_empty() {
let draws = density.draw(
gl,
&self.density_entities,
self.density_budget,
&view_proj,
(sw, sh),
self.scene_time,
);
self.stats.draw_calls += draws;
self.stats.particle_count += density.drawn as usize;
}
}
if let Some(wp) = world_proj {
let count = self.ui_renderer.world_count();
if count > 0 {
gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.instance_vbo));
gl.buffer_data_u8_slice(
glow::ARRAY_BUFFER,
self.ui_renderer.world_bytes(),
glow::DYNAMIC_DRAW,
);
gl.use_program(Some(self.program));
gl.uniform_matrix_4_f32_slice(
Some(&self.loc_view_proj),
false,
&wp.to_cols_array(),
);
gl.uniform_1_u32(self.loc_n_copies.as_ref(), 1);
gl.active_texture(glow::TEXTURE0);
gl.bind_texture(glow::TEXTURE_2D, Some(self.atlas_tex));
gl.bind_vertex_array(Some(self.vao));
for loc in 2u32..=11 {
gl.vertex_attrib_divisor(loc, 1);
}
gl.draw_arrays_instanced(glow::TRIANGLES, 0, 6, count as i32);
self.stats.draw_calls += 1;
}
}
let draws = self.postfx.run(
gl, &self.render_config, fx, self.width, self.height, self.scene_time,
);
self.stats.draw_calls += draws;
}
}
unsafe fn compile_program(gl: &glow::Context, vert_src: &str, frag_src: &str) -> glow::Program {
let vs = gl.create_shader(glow::VERTEX_SHADER).expect("create vertex shader");
gl.shader_source(vs, vert_src);
gl.compile_shader(vs);
if !gl.get_shader_compile_status(vs) {
let log = gl.get_shader_info_log(vs);
panic!("Vertex shader compile error:\n{log}");
}
let fs = gl.create_shader(glow::FRAGMENT_SHADER).expect("create fragment shader");
gl.shader_source(fs, frag_src);
gl.compile_shader(fs);
if !gl.get_shader_compile_status(fs) {
let log = gl.get_shader_info_log(fs);
panic!("Fragment shader compile error:\n{log}");
}
let prog = gl.create_program().expect("create shader program");
gl.attach_shader(prog, vs);
gl.attach_shader(prog, fs);
gl.link_program(prog);
if !gl.get_program_link_status(prog) {
let log = gl.get_program_info_log(prog);
panic!("Shader link error:\n{log}");
}
gl.detach_shader(prog, vs);
gl.detach_shader(prog, fs);
gl.delete_shader(vs);
gl.delete_shader(fs);
prog
}
unsafe fn setup_vao(gl: &glow::Context) -> (glow::VertexArray, glow::Buffer, glow::Buffer) {
let vao = gl.create_vertex_array().expect("create vao");
gl.bind_vertex_array(Some(vao));
let quad_vbo = gl.create_buffer().expect("create quad_vbo");
gl.bind_buffer(glow::ARRAY_BUFFER, Some(quad_vbo));
gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, cast_slice(&QUAD_VERTS), glow::STATIC_DRAW);
gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, 16, 0);
gl.enable_vertex_attrib_array(0);
gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, 16, 8);
gl.enable_vertex_attrib_array(1);
let instance_vbo = gl.create_buffer().expect("create instance_vbo");
gl.bind_buffer(glow::ARRAY_BUFFER, Some(instance_vbo));
let stride = std::mem::size_of::<GlyphInstance>() as i32;
macro_rules! inst_attr {
($loc:expr, $count:expr, $off:expr) => {{
gl.vertex_attrib_pointer_f32($loc, $count, glow::FLOAT, false, stride, $off);
gl.enable_vertex_attrib_array($loc);
gl.vertex_attrib_divisor($loc, 1); }};
}
inst_attr!(2, 3, 0); inst_attr!(3, 2, 12); inst_attr!(4, 1, 20); inst_attr!(5, 4, 24); inst_attr!(6, 1, 40); inst_attr!(7, 3, 44); inst_attr!(8, 1, 56); inst_attr!(9, 2, 60); inst_attr!(10, 2, 68); inst_attr!(11, 2, 76);
(vao, quad_vbo, instance_vbo)
}
unsafe fn upload_atlas(gl: &glow::Context, atlas: &FontAtlas) -> glow::Texture {
let tex = gl.create_texture().expect("create atlas texture");
gl.bind_texture(glow::TEXTURE_2D, Some(tex));
gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1);
gl.tex_image_2d(
glow::TEXTURE_2D, 0, glow::R8 as i32,
atlas.width as i32, atlas.height as i32,
0, glow::RED, glow::UNSIGNED_BYTE,
glow::PixelUnpackData::Slice(Some(&atlas.pixels)),
);
gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR as i32);
gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32);
gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32);
gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32);
tex
}
fn keycode_to_engine(kc: KeyCode) -> Option<Key> {
Some(match kc {
KeyCode::KeyA => Key::A, KeyCode::KeyB => Key::B, KeyCode::KeyC => Key::C,
KeyCode::KeyD => Key::D, KeyCode::KeyE => Key::E, KeyCode::KeyF => Key::F,
KeyCode::KeyG => Key::G, KeyCode::KeyH => Key::H, KeyCode::KeyI => Key::I,
KeyCode::KeyJ => Key::J, KeyCode::KeyK => Key::K, KeyCode::KeyL => Key::L,
KeyCode::KeyM => Key::M, KeyCode::KeyN => Key::N, KeyCode::KeyO => Key::O,
KeyCode::KeyP => Key::P, KeyCode::KeyQ => Key::Q, KeyCode::KeyR => Key::R,
KeyCode::KeyS => Key::S, KeyCode::KeyT => Key::T, KeyCode::KeyU => Key::U,
KeyCode::KeyV => Key::V, KeyCode::KeyW => Key::W, KeyCode::KeyX => Key::X,
KeyCode::KeyY => Key::Y, KeyCode::KeyZ => Key::Z,
KeyCode::Digit1 => Key::Num1, KeyCode::Digit2 => Key::Num2,
KeyCode::Digit3 => Key::Num3, KeyCode::Digit4 => Key::Num4,
KeyCode::Digit5 => Key::Num5, KeyCode::Digit6 => Key::Num6,
KeyCode::Digit7 => Key::Num7, KeyCode::Digit8 => Key::Num8,
KeyCode::Digit9 => Key::Num9, KeyCode::Digit0 => Key::Num0,
KeyCode::ArrowUp => Key::Up, KeyCode::ArrowDown => Key::Down,
KeyCode::ArrowLeft => Key::Left, KeyCode::ArrowRight => Key::Right,
KeyCode::Enter | KeyCode::NumpadEnter => Key::Enter,
KeyCode::Escape => Key::Escape,
KeyCode::Space => Key::Space,
KeyCode::Backspace => Key::Backspace,
KeyCode::Tab => Key::Tab,
KeyCode::ShiftLeft => Key::LShift, KeyCode::ShiftRight => Key::RShift,
KeyCode::ControlLeft => Key::LCtrl, KeyCode::ControlRight => Key::RCtrl,
KeyCode::AltLeft => Key::LAlt, KeyCode::AltRight => Key::RAlt,
KeyCode::F1 => Key::F1, KeyCode::F2 => Key::F2, KeyCode::F3 => Key::F3,
KeyCode::F4 => Key::F4, KeyCode::F5 => Key::F5, KeyCode::F6 => Key::F6,
KeyCode::F7 => Key::F7, KeyCode::F8 => Key::F8, KeyCode::F9 => Key::F9,
KeyCode::F10 => Key::F10, KeyCode::F11 => Key::F11, KeyCode::F12 => Key::F12,
KeyCode::Slash => Key::Slash,
KeyCode::Backslash => Key::Backslash,
KeyCode::Period => Key::Period,
KeyCode::Comma => Key::Comma,
KeyCode::Semicolon => Key::Semicolon,
KeyCode::Quote => Key::Quote,
KeyCode::BracketLeft => Key::LBracket,
KeyCode::BracketRight => Key::RBracket,
KeyCode::Minus => Key::Minus,
KeyCode::Equal => Key::Equals,
KeyCode::Backquote => Key::Backtick,
KeyCode::PageUp => Key::PageUp,
KeyCode::PageDown => Key::PageDown,
KeyCode::Home => Key::Home,
KeyCode::End => Key::End,
KeyCode::Insert => Key::Insert,
KeyCode::Delete => Key::Delete,
_ => return None,
})
}