1pub use ferravex_math::{Color, Mat4, Quat, Rect, Vec2, Vec3, Vec4, vec2, vec3, vec4};
4use std::{
5 cell::RefCell,
6 collections::BTreeSet,
7 future::Future,
8 pin::Pin,
9 task::{Context, Poll, Waker},
10};
11pub mod ao;
12pub mod canvas;
13pub mod demo;
14mod font;
15pub mod input;
16pub mod lighting;
17pub mod material;
18pub mod mesh;
19#[cfg(not(target_arch = "wasm32"))]
20pub mod native;
21pub mod retained;
22pub mod shaders;
23pub mod ui;
24pub mod visibility;
25pub use ferravex_core::telemetry;
26
27pub mod prelude {
28 pub use crate::{
29 BLACK, BLUE, Camera, Camera2D, Camera3D, Color, Conf, DARKGRAY, Font, GRAY, GREEN, KeyCode,
30 LIGHTGRAY, Mat4, Mesh, MouseButton, ORANGE, Quat, RED, Rect, TextDimensions, TextParams,
31 Touch, TouchPhase, Vec2, Vec3, Vec4, Vertex, WHITE, YELLOW, clear_background, draw_circle,
32 draw_circle_lines, draw_cube, draw_ellipse, draw_line, draw_line_3d, draw_mesh,
33 draw_mesh_transformed, draw_poly, draw_rectangle, draw_rectangle_lines, draw_text,
34 draw_text_ex, draw_triangle, get_default_font, get_frame_time, get_screen_data, get_time,
35 is_key_down, is_key_pressed, is_mouse_button_down, is_mouse_button_pressed, measure_text,
36 mouse_delta_position, mouse_position, mouse_wheel, next_frame, screen_dpi_scale,
37 screen_height, screen_width, set_camera, set_cursor_grab, set_default_camera, show_mouse,
38 simulate_mouse_with_touch, touches, vec2, vec3, vec4,
39 };
40}
41pub const WHITE: Color = Color::new(1., 1., 1., 1.);
42pub const BLACK: Color = Color::new(0., 0., 0., 1.);
43pub const RED: Color = Color::new(0.9, 0.2, 0.2, 1.);
44pub const GREEN: Color = Color::new(0.2, 0.8, 0.3, 1.);
45pub const BLUE: Color = Color::new(0.2, 0.4, 0.9, 1.);
46pub const YELLOW: Color = Color::new(1., 0.85, 0.2, 1.);
47pub const GRAY: Color = Color::new(0.5, 0.5, 0.5, 1.);
48pub const DARKGRAY: Color = Color::new(0.2, 0.2, 0.2, 1.);
49pub const LIGHTGRAY: Color = Color::new(0.8, 0.8, 0.8, 1.);
50pub const ORANGE: Color = Color::new(1., 0.5, 0.2, 1.);
51#[derive(Clone, Debug)]
52pub struct Conf {
53 pub window_title: String,
54 pub window_width: i32,
55 pub window_height: i32,
56 pub high_dpi: bool,
57 pub sample_count: i32,
58}
59impl Default for Conf {
60 fn default() -> Self {
61 window_conf("Ferravex")
62 }
63}
64pub fn window_conf(title: &str) -> Conf {
65 Conf {
66 window_title: title.into(),
67 window_width: 1280,
68 window_height: 800,
69 high_dpi: true,
70 sample_count: 4,
71 }
72}
73#[derive(Clone, Debug, Default)]
74pub struct Mesh {
75 pub vertices: Vec<Vertex>,
76 pub indices: Vec<u16>,
77 pub texture: Option<()>,
78}
79#[repr(C)]
80#[derive(Clone, Copy, Debug)]
81pub struct Vertex {
82 pub position: Vec3,
83 pub uv: Vec2,
84 pub color: [u8; 4],
85 pub normal: Vec4,
86}
87impl Vertex {
88 pub fn new2(position: Vec3, uv: Vec2, color: Color) -> Self {
89 Self {
90 position,
91 uv,
92 color: color.to_rgba(),
93 normal: Vec4::Y,
94 }
95 }
96}
97#[repr(C)]
98#[derive(Clone, Copy, Debug)]
99pub struct Batch {
100 pub start: u32,
101 pub count: u32,
102 pub depth: bool,
103}
104#[derive(Clone, Copy, Debug)]
105pub struct Camera2D {
106 pub target: Vec2,
107 pub zoom: Vec2,
108 pub offset: Vec2,
109 pub rotation: f32,
110}
111impl Default for Camera2D {
112 fn default() -> Self {
113 Self {
114 target: Vec2::ZERO,
115 zoom: Vec2::ONE,
116 offset: Vec2::ZERO,
117 rotation: 0.,
118 }
119 }
120}
121impl Camera2D {
122 pub fn from_display_rect(r: Rect) -> Self {
123 Self {
124 target: r.center(),
125 zoom: vec2(2. / r.w, 2. / r.h),
126 ..Self::default()
127 }
128 }
129}
130#[derive(Clone, Copy, Debug)]
131pub struct Camera3D {
132 pub position: Vec3,
133 pub target: Vec3,
134 pub up: Vec3,
135 pub fovy: f32,
136 pub aspect: Option<f32>,
137 pub z_near: f32,
138 pub z_far: f32,
139}
140impl Default for Camera3D {
141 fn default() -> Self {
142 Self {
143 position: vec3(0., 5., 10.),
144 target: Vec3::ZERO,
145 up: Vec3::Y,
146 fovy: 45f32.to_radians(),
147 aspect: None,
148 z_near: 0.1,
149 z_far: 1000.,
150 }
151 }
152}
153pub trait Camera {
154 fn matrix(&self, w: f32, h: f32) -> Mat4;
155 fn depth(&self) -> bool;
156 fn position(&self) -> Option<Vec3> {
157 None
158 }
159}
160impl Camera for Camera2D {
161 fn matrix(&self, _: f32, _: f32) -> Mat4 {
162 Mat4::from_translation(self.offset.extend(0.))
163 * Mat4::from_scale(vec3(self.zoom.x, -self.zoom.y, 1.))
164 * Mat4::from_rotation_z(-self.rotation.to_radians())
165 * Mat4::from_translation((-self.target).extend(0.))
166 }
167 fn depth(&self) -> bool {
168 false
169 }
170}
171impl Camera for Camera3D {
172 fn position(&self) -> Option<Vec3> {
173 Some(self.position)
174 }
175 fn matrix(&self, w: f32, h: f32) -> Mat4 {
176 Mat4::perspective_rh_gl(
177 self.fovy,
178 self.aspect.unwrap_or(w / h),
179 self.z_near,
180 self.z_far,
181 ) * Mat4::look_at_rh(self.position, self.target, self.up)
182 }
183 fn depth(&self) -> bool {
184 true
185 }
186}
187#[derive(Clone, Copy, Debug, PartialEq, Eq)]
188#[repr(u32)]
189pub enum KeyCode {
190 F1 = 0xffbe,
191 F2 = 0xffbf,
192 F3 = 0xffc0,
193 Space = 32,
194 A = 97,
195 B = 98,
196 C = 99,
197 D = 100,
198 E = 101,
199 F = 102,
200 G = 103,
201 H = 104,
202 I = 105,
203 J = 106,
204 K = 107,
205 L = 108,
206 M = 109,
207 N = 110,
208 O = 111,
209 P = 112,
210 Q = 113,
211 R = 114,
212 S = 115,
213 T = 116,
214 U = 117,
215 V = 118,
216 W = 119,
217 X = 120,
218 Y = 121,
219 Z = 122,
220 Key0 = 48,
221 Key1 = 49,
222 Key2 = 50,
223 Key3 = 51,
224 Key4 = 52,
225 Escape = 0xff1b,
226 Enter = 0xff0d,
227 Tab = 0xff09,
228 Backspace = 0xff08,
229 Left = 0xff51,
230 Up = 0xff52,
231 Right = 0xff53,
232 Down = 0xff54,
233 LeftShift = 0xffe1,
234 RightShift = 0xffe2,
235 LeftControl = 0xffe3,
236 RightControl = 0xffe4,
237}
238#[derive(Clone, Copy, Debug)]
239#[repr(u32)]
240pub enum MouseButton {
241 Left = 1,
242 Middle = 2,
243 Right = 3,
244}
245#[derive(Clone, Copy, Debug, PartialEq, Eq)]
246pub enum TouchPhase {
247 Started,
248 Moved,
249 Stationary,
250 Ended,
251 Cancelled,
252}
253#[derive(Clone, Copy, Debug)]
254pub struct Touch {
255 pub id: u64,
256 pub position: Vec2,
257 pub phase: TouchPhase,
258}
259struct State {
260 w: u32,
261 h: u32,
262 dt: f32,
263 time: f64,
264 matrix: Mat4,
265 depth: bool,
266 frustum: Option<ferravex_graphics::Frustum>,
267 eye: Option<Vec3>,
268 visibility: visibility::VisibilitySettings,
269 visibility_stats: visibility::VisibilityStats,
270 clear: Color,
271 vertices: Vec<[f32; 8]>,
272 batches: Vec<Batch>,
273 retained: retained::Frame,
274 lighting: Option<shaders::LitSettings>,
275 shader: Option<material::ShaderMaterial>,
276 shader_params: [f32; 16],
277 quality: lighting::Quality,
278 quality_override: Option<lighting::Quality>,
279 points: Vec<lighting::PointLight>,
280 sun: Option<lighting::SunShadow>,
281 keys: BTreeSet<u32>,
282 pressed: BTreeSet<u32>,
283 buttons: BTreeSet<u32>,
284 clicks: BTreeSet<u32>,
285 mouse: Vec2,
286 delta: Vec2,
287 wheel: f32,
288 touches: Vec<Touch>,
289 grab: bool,
290 visible: bool,
291 touch_mouse: bool,
292 #[cfg(not(target_arch = "wasm32"))]
293 screenshots: Vec<String>,
294 future: Option<Pin<Box<dyn Future<Output = ()>>>>,
295 finished: bool,
296 autoplay: bool,
297 frame: u64,
298}
299impl Default for State {
300 fn default() -> Self {
301 let mut s = Self {
302 w: 1280,
303 h: 800,
304 dt: 1. / 60.,
305 time: 0.,
306 matrix: Mat4::IDENTITY,
307 depth: false,
308 frustum: None,
309 eye: None,
310 visibility: Default::default(),
311 visibility_stats: Default::default(),
312 clear: BLACK,
313 vertices: Vec::new(),
314 batches: Vec::new(),
315 retained: retained::Frame::default(),
316 lighting: None,
317 shader: None,
318 shader_params: [0.; 16],
319 quality: Default::default(),
320 quality_override: None,
321 points: Vec::new(),
322 sun: None,
323 keys: BTreeSet::new(),
324 pressed: BTreeSet::new(),
325 buttons: BTreeSet::new(),
326 clicks: BTreeSet::new(),
327 mouse: Vec2::ZERO,
328 delta: Vec2::ZERO,
329 wheel: 0.,
330 touches: Vec::new(),
331 grab: false,
332 visible: true,
333 touch_mouse: true,
334 #[cfg(not(target_arch = "wasm32"))]
335 screenshots: Vec::new(),
336 future: None,
337 finished: false,
338 autoplay: false,
339 frame: 0,
340 };
341 s.default_camera();
342 s
343 }
344}
345impl State {
346 fn refresh_environment(&mut self) {
347 self.retained
348 .environment
349 .update(&self.points, self.sun, self.quality, self.eye);
350 }
351 fn submit(
352 &mut self,
353 mesh: &retained::ResidentMesh,
354 model: Mat4,
355 bounds: ferravex_graphics::Bounds,
356 ) -> bool {
357 let visible = self.test_visibility(bounds, model);
358 let casts = self.depth
359 && mesh.is_opaque()
360 && self.shader.as_ref().is_none_or(|m| {
361 m.0.options.blend == material::BlendMode::Opaque && m.0.options.casts_shadows
362 })
363 && self
364 .retained
365 .environment
366 .frustum()
367 .is_some_and(|f| f.intersects_bounds(bounds, model));
368 if !visible && !casts {
369 return false;
370 }
371 let recorded = self.retained.record_style(
372 mesh,
373 model,
374 self.matrix,
375 self.depth,
376 retained::DrawStyle {
377 light: self.lighting,
378 material: self.shader.as_ref(),
379 params: self.shader_params,
380 shadow_only: !visible,
381 skip_shadow: !casts,
382 },
383 );
384 visible && recorded
385 }
386 fn test_visibility(&mut self, bounds: ferravex_graphics::Bounds, model: Mat4) -> bool {
387 use visibility::Visibility;
388 self.visibility_stats.tested += 1;
389 match visibility::classify(bounds, model, self.frustum, self.eye, self.visibility) {
390 Visibility::Visible => {
391 self.visibility_stats.visible += 1;
392 true
393 }
394 Visibility::OutsideFrustum => {
395 self.visibility_stats.frustum_culled += 1;
396 false
397 }
398 Visibility::TooDistant => {
399 self.visibility_stats.distance_culled += 1;
400 false
401 }
402 Visibility::InvalidTransform => false,
403 }
404 }
405 fn default_camera(&mut self) {
406 self.matrix = Mat4::orthographic_rh_gl(0., self.w as f32, self.h as f32, 0., -1., 1.);
407 self.depth = false;
408 self.frustum = None;
409 self.eye = None;
410 }
411 fn push(&mut self, vertices: impl IntoIterator<Item = (Vec3, Vec3, Color)>, model: Mat4) {
412 let start = self.vertices.len() as u32;
413 let normal_matrix = if self.lighting.is_some() {
414 model
415 .try_inverse()
416 .map(|m| m.transpose())
417 .unwrap_or(Mat4::IDENTITY)
418 } else {
419 Mat4::IDENTITY
420 };
421 for (p, n, mut c) in vertices {
422 let p = model.transform_point3(p);
423 if let Some(l) = self.lighting {
424 c = l.shade_vertex(p, normal_matrix.transform_vector3(n), c);
425 }
426 let v = self.matrix * p.extend(1.);
427 self.vertices.push([v.x, v.y, v.z, v.w, c.r, c.g, c.b, c.a]);
428 }
429 let count = self.vertices.len() as u32 - start;
430 if count == 0 {
431 return;
432 }
433 if let Some(last) = self.batches.last_mut().filter(|b| {
434 b.depth == self.depth && self.retained.commands.last().is_some_and(|c| c.kind == 0)
435 }) {
436 last.count += count;
437 } else {
438 self.retained.commands.push(retained::Command {
439 kind: 0,
440 index: self.batches.len() as u32,
441 });
442 self.batches.push(Batch {
443 start,
444 count,
445 depth: self.depth,
446 });
447 }
448 }
449}
450thread_local! {static STATE:RefCell<State>=RefCell::new(State::default());}
451fn read<T>(f: impl FnOnce(&State) -> T) -> T {
452 STATE.with(|s| f(&s.borrow()))
453}
454fn edit<T>(f: impl FnOnce(&mut State) -> T) -> T {
455 STATE.with(|s| f(&mut s.borrow_mut()))
456}
457pub fn set_camera(camera: &impl Camera) {
458 edit(|s| {
459 s.matrix = camera.matrix(s.w as f32, s.h as f32);
460 s.depth = camera.depth();
461 s.eye = camera.position();
462 if s.eye.is_some() {
463 s.refresh_environment();
464 }
465 s.frustum = if s.depth {
466 ferravex_graphics::Frustum::from_matrix(
467 s.matrix,
468 ferravex_graphics::ClipDepth::NegativeOneToOne,
469 )
470 } else {
471 None
472 };
473 });
474}
475pub fn set_default_camera() {
476 edit(State::default_camera)
477}
478pub fn set_lighting(settings: Option<shaders::LitSettings>) {
479 edit(|s| s.lighting = settings)
480}
481pub fn clear_background(c: Color) {
482 edit(|s| {
483 s.clear = c;
484 s.vertices.clear();
485 s.batches.clear();
486 s.retained.clear();
487 s.points.clear();
488 s.sun = None;
489 s.refresh_environment();
490 s.visibility_stats = Default::default();
491 });
492}
493pub fn draw_mesh(m: &Mesh) {
494 draw_mesh_transformed(m, Mat4::IDENTITY)
495}
496pub(crate) fn set_shader(shader: Option<material::ShaderMaterial>, params: [f32; 16]) {
499 edit(|s| {
500 s.shader = shader;
501 s.shader_params = params;
502 });
503}
504pub fn draw_resident(mesh: &retained::ResidentMesh, model: Mat4) -> bool {
505 edit(|s| {
506 let bounds = s
507 .shader
508 .as_ref()
509 .map_or(mesh.bounds(), |m| m.padded(mesh.bounds()));
510 s.submit(mesh, model, bounds)
511 })
512}
513pub fn draw_shadow(mesh: &retained::ResidentMesh, model: Mat4) -> bool {
515 edit(|s| {
516 let bounds = s
517 .shader
518 .as_ref()
519 .map_or(mesh.bounds(), |m| m.padded(mesh.bounds()));
520 if !s.depth
521 || !mesh.is_opaque()
522 || s.shader.as_ref().is_some_and(|m| {
523 !m.0.options.casts_shadows || m.0.options.blend != material::BlendMode::Opaque
524 })
525 || !s
526 .retained
527 .environment
528 .frustum()
529 .is_some_and(|f| f.intersects_bounds(bounds, model))
530 {
531 return false;
532 }
533 s.retained.record_style(
534 mesh,
535 model,
536 s.matrix,
537 s.depth,
538 retained::DrawStyle {
539 light: s.lighting,
540 material: s.shader.as_ref(),
541 params: s.shader_params,
542 shadow_only: true,
543 skip_shadow: false,
544 },
545 )
546 })
547}
548pub fn set_quality(quality: lighting::Quality) {
549 edit(|s| {
550 s.quality = s.quality_override.unwrap_or(quality);
551 s.refresh_environment();
552 });
553}
554pub fn quality() -> lighting::Quality {
555 read(|s| s.quality)
556}
557pub fn set_point_lights(lights: &[lighting::PointLight]) -> Result<(), &'static str> {
558 if lights.len() > lighting::MAX_POINT_LIGHTS || lights.iter().any(|l| !l.valid()) {
559 return Err("invalid point lights or capacity exceeded");
560 }
561 edit(|s| {
562 s.points.clear();
563 s.points.extend_from_slice(lights);
564 s.refresh_environment();
565 });
566 Ok(())
567}
568pub fn set_sun_shadow(sun: Option<lighting::SunShadow>) -> Result<(), &'static str> {
569 if sun.is_some_and(|s| !s.valid()) {
570 return Err("invalid sun shadow");
571 }
572 edit(|s| {
573 s.sun = sun;
574 s.refresh_environment();
575 });
576 Ok(())
577}
578fn quality_override(quality: lighting::Quality) {
579 edit(|s| {
580 s.quality_override = Some(quality);
581 s.quality = quality;
582 s.refresh_environment();
583 });
584}
585pub fn set_visibility(settings: visibility::VisibilitySettings) -> bool {
586 if !settings.valid() {
587 return false;
588 }
589 edit(|s| s.visibility = settings);
590 true
591}
592pub fn visibility_stats() -> visibility::VisibilityStats {
593 read(|s| s.visibility_stats)
594}
595pub fn draw_lod(
596 chain: &visibility::LodChain,
597 model: Mat4,
598 state: &mut visibility::LodState,
599) -> bool {
600 edit(|s| {
601 let distance = s.eye.map_or(0., |eye| {
602 visibility::distance_to_bounds(chain.bounds(), model, eye)
603 });
604 let mesh = chain.select(distance, state);
605 let bounds = s
606 .shader
607 .as_ref()
608 .map_or(chain.bounds(), |m| m.padded(chain.bounds()));
609 s.submit(mesh, model, bounds)
610 })
611}
612pub fn draw_mesh_transformed(m: &Mesh, model: Mat4) {
613 edit(|s| {
614 s.push(
615 m.indices
616 .as_chunks::<3>()
617 .0
618 .iter()
619 .filter_map(|[a, b, c]| {
620 let v = [
621 m.vertices.get(*a as usize)?,
622 m.vertices.get(*b as usize)?,
623 m.vertices.get(*c as usize)?,
624 ];
625 v.iter()
626 .all(|v| v.position.is_finite() && v.normal.is_finite())
627 .then_some(v)
628 })
629 .flatten()
630 .map(|v| {
631 (
632 v.position,
633 v.normal.truncate(),
634 Color::from_rgba(v.color[0], v.color[1], v.color[2], v.color[3]),
635 )
636 }),
637 model,
638 )
639 })
640}
641pub fn draw_triangle(a: Vec2, b: Vec2, c: Vec2, color: Color) {
642 edit(|s| {
643 s.push(
644 [a, b, c].map(|p| (p.extend(0.), Vec3::Z, color)),
645 Mat4::IDENTITY,
646 )
647 })
648}
649pub fn draw_rectangle(x: f32, y: f32, w: f32, h: f32, c: Color) {
650 let a = vec2(x, y);
651 let b = vec2(x + w, y);
652 let d = vec2(x, y + h);
653 let e = vec2(x + w, y + h);
654 draw_triangle(a, b, d, c);
655 draw_triangle(d, b, e, c);
656}
657pub fn draw_line(x1: f32, y1: f32, x2: f32, y2: f32, width: f32, c: Color) {
658 let a = vec2(x1, y1);
659 let b = vec2(x2, y2);
660 let d = (b - a).normalize_or_zero();
661 let n = vec2(-d.y, d.x) * width * 0.5;
662 draw_triangle(a + n, a - n, b + n, c);
663 draw_triangle(b + n, a - n, b - n, c);
664}
665pub fn draw_rectangle_lines(x: f32, y: f32, w: f32, h: f32, t: f32, c: Color) {
666 draw_line(x, y, x + w, y, t, c);
667 draw_line(x + w, y, x + w, y + h, t, c);
668 draw_line(x + w, y + h, x, y + h, t, c);
669 draw_line(x, y + h, x, y, t, c);
670}
671pub fn draw_poly(x: f32, y: f32, sides: u8, r: f32, rotation: f32, c: Color) {
672 let sides = sides.max(3);
673 for i in 0..sides {
674 let a = rotation.to_radians() + i as f32 * std::f32::consts::TAU / sides as f32;
675 let b = a + std::f32::consts::TAU / sides as f32;
676 draw_triangle(
677 vec2(x, y),
678 vec2(x + r * a.cos(), y + r * a.sin()),
679 vec2(x + r * b.cos(), y + r * b.sin()),
680 c,
681 );
682 }
683}
684pub fn draw_circle(x: f32, y: f32, r: f32, c: Color) {
685 draw_poly(x, y, 32, r, 0., c)
686}
687pub fn draw_circle_lines(x: f32, y: f32, r: f32, t: f32, c: Color) {
688 for i in 0..40 {
689 let a = i as f32 * std::f32::consts::TAU / 40.;
690 let b = (i + 1) as f32 * std::f32::consts::TAU / 40.;
691 draw_line(
692 x + r * a.cos(),
693 y + r * a.sin(),
694 x + r * b.cos(),
695 y + r * b.sin(),
696 t,
697 c,
698 );
699 }
700}
701pub fn draw_ellipse(x: f32, y: f32, rx: f32, ry: f32, rotation: f32, c: Color) {
702 let angle = rotation.to_radians();
703 let point = |a: f32| {
704 let p = vec2(rx * a.cos(), ry * a.sin());
705 vec2(
706 x + p.x * angle.cos() - p.y * angle.sin(),
707 y + p.x * angle.sin() + p.y * angle.cos(),
708 )
709 };
710 for i in 0..32 {
711 draw_triangle(
712 vec2(x, y),
713 point(i as f32 * std::f32::consts::TAU / 32.),
714 point((i + 1) as f32 * std::f32::consts::TAU / 32.),
715 c,
716 );
717 }
718}
719pub fn draw_line_3d(a: Vec3, b: Vec3, c: Color) {
720 let d = (b - a).normalize_or_zero();
721 let n = d
722 .cross(if d.y.abs() < 0.9 { Vec3::Y } else { Vec3::X })
723 .normalize_or_zero()
724 * 0.015;
725 edit(|s| {
726 s.push(
727 [a + n, a - n, b + n, b + n, a - n, b - n].map(|p| (p, Vec3::Y, c)),
728 Mat4::IDENTITY,
729 )
730 });
731}
732pub fn draw_cube(position: Vec3, size: Vec3, _texture: Option<()>, color: Color) {
733 mesh::Mesh3d::cuboid(size, color).draw(position, Quat::IDENTITY);
734}
735#[derive(Clone, Copy, Debug, Default)]
736pub struct Font;
737impl Font {
738 pub fn populate_font_cache(&self, _chars: &[char], _size: u16) { }
740}
741pub fn get_default_font() -> Font {
742 Font
743}
744#[derive(Clone, Copy, Debug)]
745pub struct TextParams {
746 pub font_size: u16,
747 pub font_scale: f32,
748 pub color: Color,
749}
750impl Default for TextParams {
751 fn default() -> Self {
752 Self {
753 font_size: 20,
754 font_scale: 1.,
755 color: WHITE,
756 }
757 }
758}
759#[derive(Clone, Copy, Debug, Default)]
760pub struct TextDimensions {
761 pub width: f32,
762 pub height: f32,
763 pub offset_y: f32,
764}
765pub fn measure_text(t: &str, _font: Option<&Font>, size: u16, scale: f32) -> TextDimensions {
766 let px = size as f32 * scale / 8.;
767 TextDimensions {
768 width: t.chars().count() as f32 * 6. * px,
769 height: 7. * px,
770 offset_y: 7. * px,
771 }
772}
773pub fn draw_text(t: &str, x: f32, y: f32, size: f32, c: Color) -> TextDimensions {
774 draw_text_ex(
775 t,
776 x,
777 y,
778 TextParams {
779 font_size: 8,
780 font_scale: size / 8.,
781 color: c,
782 },
783 )
784}
785pub fn draw_text_ex(t: &str, mut x: f32, y: f32, p: TextParams) -> TextDimensions {
786 let px = p.font_size as f32 * p.font_scale / 8.;
787 for c in t.chars() {
788 for (row, bits) in font::glyph(c.to_ascii_uppercase()).into_iter().enumerate() {
789 for col in 0..5 {
790 if bits & (1 << (4 - col)) != 0 {
791 draw_rectangle(
792 x + col as f32 * px,
793 y - 7. * px + row as f32 * px,
794 px,
795 px,
796 p.color,
797 );
798 }
799 }
800 }
801 x += 6. * px;
802 }
803 measure_text(t, None, p.font_size, p.font_scale)
804}
805pub fn screen_width() -> f32 {
806 read(|s| s.w as f32)
807}
808pub fn screen_height() -> f32 {
809 read(|s| s.h as f32)
810}
811pub fn screen_dpi_scale() -> f32 {
812 1.
813}
814pub fn get_frame_time() -> f32 {
815 read(|s| s.dt)
816}
817pub fn get_time() -> f64 {
818 read(|s| s.time)
819}
820pub fn is_key_down(k: KeyCode) -> bool {
821 read(|s| s.keys.contains(&(k as u32)))
822}
823pub fn is_key_pressed(k: KeyCode) -> bool {
824 read(|s| s.pressed.contains(&(k as u32)))
825}
826pub fn is_mouse_button_down(k: MouseButton) -> bool {
827 read(|s| s.buttons.contains(&(k as u32)))
828}
829pub fn is_mouse_button_pressed(k: MouseButton) -> bool {
830 read(|s| s.clicks.contains(&(k as u32)))
831}
832pub fn mouse_position() -> (f32, f32) {
833 read(|s| s.mouse.into())
834}
835pub fn mouse_delta_position() -> Vec2 {
836 read(|s| -s.delta / vec2(s.w as f32, s.h as f32))
837}
838pub fn mouse_wheel() -> (f32, f32) {
839 read(|s| (0., s.wheel))
840}
841pub fn touches() -> Vec<Touch> {
842 read(|s| s.touches.clone())
843}
844pub fn set_cursor_grab(grab: bool) {
845 edit(|s| s.grab = grab)
846}
847pub fn simulate_mouse_with_touch(enabled: bool) {
848 edit(|s| s.touch_mouse = enabled)
849}
850pub fn show_mouse(visible: bool) {
851 edit(|s| s.visible = visible)
852}
853pub struct ScreenImage;
854pub fn get_screen_data() -> ScreenImage {
855 ScreenImage
856}
857impl ScreenImage {
858 #[cfg(not(target_arch = "wasm32"))]
860 pub fn export_png(&self, path: &str) {
861 edit(|s| {
862 if s.screenshots.len() < 8 {
863 s.screenshots.push(path.into());
864 }
865 });
866 }
867}
868pub async fn next_frame() {
869 struct Yield(bool);
870 impl Future for Yield {
871 type Output = ();
872 fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<()> {
873 if self.0 {
874 Poll::Ready(())
875 } else {
876 self.0 = true;
877 Poll::Pending
878 }
879 }
880 }
881 Yield(false).await
882}
883pub fn start(conf: Conf, future: impl Future<Output = ()> + 'static) {
884 edit(|s| {
885 *s = State::default();
886 s.w = conf.window_width.max(1) as u32;
887 s.h = conf.window_height.max(1) as u32;
888 s.default_camera();
889 s.future = Some(Box::pin(future));
890 });
891}
892fn frame(dt: f32, w: u32, h: u32) -> bool {
893 edit(|s| {
894 s.dt = if dt.is_finite() {
895 dt.clamp(0., 0.1)
896 } else {
897 0.
898 };
899 s.time += s.dt as f64;
900 s.w = w.max(1);
901 s.h = h.max(1);
902 s.vertices.clear();
903 s.batches.clear();
904 s.retained.clear();
905 s.points.clear();
906 s.sun = None;
907 s.refresh_environment();
908 s.visibility_stats = Default::default();
909 s.default_camera();
910 });
911 let mut future = edit(|s| s.future.take());
912 let finished = future.as_mut().is_none_or(|f| {
913 f.as_mut()
914 .poll(&mut Context::from_waker(Waker::noop()))
915 .is_ready()
916 });
917 edit(|s| {
918 s.future = future;
919 s.retained.finalize();
920 s.finished = finished;
921 s.frame += 1;
922 s.pressed.clear();
923 s.clicks.clear();
924 s.delta = Vec2::ZERO;
925 s.wheel = 0.;
926 let ended: Vec<_> = s
927 .touches
928 .iter()
929 .filter(|t| matches!(t.phase, TouchPhase::Ended | TouchPhase::Cancelled))
930 .map(|t| t.id)
931 .collect();
932 s.touches.retain(|t| !ended.contains(&t.id));
933 for t in &mut s.touches {
934 t.phase = TouchPhase::Stationary;
935 }
936 });
937 !finished
938}
939fn key(code: u32, down: bool) {
940 edit(|s| {
941 if down {
942 if s.keys.insert(code) {
943 s.pressed.insert(code);
944 }
945 } else {
946 s.keys.remove(&code);
947 }
948 })
949}
950fn button(code: u32, down: bool) {
951 edit(|s| {
952 if down {
953 if s.buttons.insert(code) {
954 s.clicks.insert(code);
955 }
956 } else {
957 s.buttons.remove(&code);
958 }
959 })
960}
961fn pointer(x: f32, y: f32, dx: f32, dy: f32) {
962 if ![x, y, dx, dy].into_iter().all(f32::is_finite) {
963 return;
964 }
965 edit(|s| {
966 s.mouse = vec2(x, y);
967 s.delta += vec2(dx, dy);
968 })
969}
970#[cfg(any(target_arch = "wasm32", test))]
971fn touch(id: u64, phase: u32, x: f32, y: f32) {
972 if !x.is_finite() || !y.is_finite() {
973 return;
974 }
975 edit(|s| {
976 let position = vec2(x, y);
977 if phase == 1
978 && let Some(t) = s
979 .touches
980 .iter_mut()
981 .find(|t| t.id == id && t.phase == TouchPhase::Started)
982 {
983 t.position = position;
984 return;
985 }
986 let preserve_start = phase == 2
987 && s.touches
988 .iter()
989 .any(|t| t.id == id && t.phase == TouchPhase::Started);
990 if !preserve_start {
991 s.touches.retain(|t| t.id != id);
992 }
993 if s.touches.len() < 64 {
994 s.touches.push(Touch {
995 id,
996 position,
997 phase: match phase {
998 0 => TouchPhase::Started,
999 1 => TouchPhase::Moved,
1000 2 => TouchPhase::Ended,
1001 _ => TouchPhase::Cancelled,
1002 },
1003 });
1004 }
1005 });
1006}
1007#[cfg(not(target_arch = "wasm32"))]
1008pub fn run(conf: Conf, future: impl Future<Output = ()> + 'static) {
1009 let mut window = native::NativeWindow::new(&conf)
1010 .unwrap_or_else(|e| panic!("Ferravex native platform: {e}"));
1011 start(conf, future);
1012 if let Ok(value) = std::env::var("FERRAVEX_QUALITY") {
1013 quality_override(
1014 lighting::Quality::from_name(&value)
1015 .expect("FERRAVEX_QUALITY must be low, medium or high"),
1016 );
1017 }
1018 let fixed = std::env::args().any(|s| s == "--smoke-frames");
1019 let mut performance = telemetry::Session::from_env();
1020 if let Some(session) = &mut performance {
1021 window.set_swap_interval(0);
1022 if window.enable_gpu_timing() {
1023 session.gpu_source("OpenGL TIME_ELAPSED");
1024 }
1025 }
1026 let mut last = std::time::Instant::now();
1027 loop {
1028 let now = std::time::Instant::now();
1029 let dt = if fixed {
1030 1. / 60.
1031 } else {
1032 (now - last).as_secs_f32()
1033 };
1034 last = now;
1035 for e in window.poll() {
1036 use native::Event;
1037 match e {
1038 Event::Key(c, d) => key(c, d),
1039 Event::MouseButton(c, d) => button(c, d),
1040 Event::MouseMove(x, y) => {
1041 let old = read(|s| s.mouse);
1042 pointer(x, y, x - old.x, y - old.y);
1043 }
1044 Event::MouseDelta(dx, dy) => {
1045 let old = read(|s| s.mouse);
1046 pointer(old.x, old.y, dx, dy);
1047 }
1048 Event::Wheel(v) => edit(|s| s.wheel += v),
1049 Event::Resize(_, _) => {}
1050 Event::Quit => return,
1051 }
1052 }
1053 let (w, h) = window.size();
1054 let token = performance.as_ref().map(|_| telemetry::FrameToken::begin());
1055 let active = frame(dt, w, h);
1056 read(|s| window.set_cursor(s.grab, s.visible));
1057 let cpu_us = token.as_ref().map_or(0., |t| t.elapsed_us());
1058 let submit_start = std::time::Instant::now();
1059 read(|s| window.present_scene(&s.vertices, &s.batches, s.clear, Some(&s.retained)));
1060 if let (Some(session), Some(token)) = (&mut performance, token) {
1061 session.record(token.finish(read(|s| telemetry::FrameSample {
1062 cpu_us,
1063 submit_us: Some(submit_start.elapsed().as_secs_f64() * 1e6),
1064 draw_calls: Some(window.render_counts().0),
1065 vertices: Some(window.render_counts().1),
1066 upload_bytes: Some(
1067 (s.vertices.len() * 32 + s.retained.instances.len() * 112) as u64
1068 + window.last_mesh_upload_bytes(),
1069 ),
1070 ..Default::default()
1071 })));
1072 }
1073 if let Some(session) = &mut performance {
1074 window.drain_gpu_results(|frame, time| session.gpu_result(frame, time));
1075 }
1076 let paths = edit(|s| std::mem::take(&mut s.screenshots));
1077 if !paths.is_empty() {
1078 let pixels = window.pixels();
1079 match ferravex_graphics::Framebuffer::new(w, h) {
1080 Ok(mut fb) if pixels.len() == w as usize * h as usize * 4 => {
1081 for (dst, src) in fb.pixels_mut().iter_mut().zip(pixels.as_chunks::<4>().0) {
1082 *dst = *src;
1083 }
1084 for path in paths {
1085 if let Err(error) = fb.save_png(&path) {
1086 eprintln!("Screenshot {path}: {error}");
1087 }
1088 }
1089 }
1090 _ => eprintln!("Screenshot unavailable: invalid dimensions or GPU readback"),
1091 }
1092 }
1093 if !active {
1094 break;
1095 }
1096 if !fixed {
1097 let elapsed = now.elapsed();
1098 if elapsed < std::time::Duration::from_millis(16) {
1099 std::thread::sleep(std::time::Duration::from_millis(16) - elapsed);
1100 }
1101 }
1102 }
1103 if let Some(mut session) = performance {
1104 session.render_quality(quality() as u32);
1105 window.finish_gpu_timing();
1106 window.drain_gpu_results(|frame, time| session.gpu_result(frame, time));
1107 let (w, h) = window.size();
1108 session
1109 .write(&window.driver_info(), w, h)
1110 .expect("write performance report");
1111 }
1112}
1113#[macro_export]
1114macro_rules! game {
1115 ($conf:path,$game:path) => {
1116 #[cfg(not(target_arch = "wasm32"))]
1117 #[global_allocator]
1118 static FERRAVEX_ALLOCATOR: $crate::telemetry::TrackingAllocator =
1119 $crate::telemetry::TrackingAllocator;
1120 #[cfg(not(target_arch = "wasm32"))]
1121 pub fn run() {
1122 $crate::run($conf(), $game());
1123 }
1124 #[cfg(target_arch = "wasm32")]
1125 #[unsafe(no_mangle)]
1126 pub extern "C" fn ferravex_init() {
1127 $crate::start($conf(), $game());
1128 }
1129 };
1130}
1131#[cfg(target_arch = "wasm32")]
1132mod wasm {
1133 use super::*;
1134 #[unsafe(no_mangle)]
1135 pub extern "C" fn ferravex_light_environment() -> *const lighting::Environment {
1136 read(|s| &s.retained.environment as *const lighting::Environment)
1137 }
1138 #[unsafe(no_mangle)]
1139 pub extern "C" fn ferravex_quality_override(value: u32) -> u32 {
1140 let q = match value {
1141 0 => lighting::Quality::Low,
1142 1 => lighting::Quality::Medium,
1143 2 => lighting::Quality::High,
1144 _ => return 0,
1145 };
1146 quality_override(q);
1147 1
1148 }
1149
1150 #[unsafe(no_mangle)]
1151 pub extern "C" fn ferravex_retained_batch_count() -> u32 {
1152 read(|s| s.retained.batches.len() as u32)
1153 }
1154 #[unsafe(no_mangle)]
1155 pub extern "C" fn ferravex_culled_count() -> u32 {
1156 let s = visibility_stats();
1157 s.frustum_culled + s.distance_culled
1158 }
1159 #[unsafe(no_mangle)]
1160 pub extern "C" fn ferravex_visible_count() -> u32 {
1161 visibility_stats().visible
1162 }
1163 #[unsafe(no_mangle)]
1164 pub extern "C" fn ferravex_commands() -> *const retained::Command {
1165 read(|s| s.retained.commands.as_ptr())
1166 }
1167 #[unsafe(no_mangle)]
1168 pub extern "C" fn ferravex_command_count() -> u32 {
1169 read(|s| s.retained.commands.len() as u32)
1170 }
1171 #[unsafe(no_mangle)]
1172 pub extern "C" fn ferravex_retained_batches() -> *const retained::RetainedBatch {
1173 read(|s| s.retained.batches.as_ptr())
1174 }
1175 #[unsafe(no_mangle)]
1176 pub extern "C" fn ferravex_retained_instances() -> *const [f32; 28] {
1177 read(|s| s.retained.instances.as_ptr())
1178 }
1179 #[unsafe(no_mangle)]
1180 pub extern "C" fn ferravex_retained_instance_count() -> u32 {
1181 read(|s| s.retained.instances.len() as u32)
1182 }
1183 #[unsafe(no_mangle)]
1184 pub extern "C" fn ferravex_mesh_vertices(id: u32) -> *const [f32; 10] {
1185 retained::geometry(id).map_or(std::ptr::null(), |m| m.vertices.as_ptr())
1186 }
1187 #[unsafe(no_mangle)]
1188 pub extern "C" fn ferravex_mesh_vertex_count(id: u32) -> u32 {
1189 retained::geometry(id).map_or(0, |m| m.vertices.len() as u32)
1190 }
1191 #[unsafe(no_mangle)]
1192 pub extern "C" fn ferravex_frame(dt: f32, w: u32, h: u32) -> u32 {
1193 frame(dt, w, h) as u32
1194 }
1195 #[unsafe(no_mangle)]
1196 pub extern "C" fn ferravex_vertices() -> *const [f32; 8] {
1197 read(|s| s.vertices.as_ptr())
1198 }
1199 #[unsafe(no_mangle)]
1200 pub extern "C" fn ferravex_vertex_count() -> u32 {
1201 read(|s| s.vertices.len() as u32)
1202 }
1203 #[unsafe(no_mangle)]
1204 pub extern "C" fn ferravex_batches() -> *const Batch {
1205 read(|s| s.batches.as_ptr())
1206 }
1207 #[unsafe(no_mangle)]
1208 pub extern "C" fn ferravex_batch_count() -> u32 {
1209 read(|s| s.batches.len() as u32)
1210 }
1211 #[unsafe(no_mangle)]
1212 pub extern "C" fn ferravex_clear(channel: u32) -> f32 {
1213 read(|s| [s.clear.r, s.clear.g, s.clear.b, s.clear.a][channel.min(3) as usize])
1214 }
1215 #[unsafe(no_mangle)]
1216 pub extern "C" fn ferravex_key(code: u32, down: u32) {
1217 key(code, down != 0)
1218 }
1219 #[unsafe(no_mangle)]
1220 pub extern "C" fn ferravex_button(code: u32, down: u32) {
1221 button(code, down != 0)
1222 }
1223 #[unsafe(no_mangle)]
1224 pub extern "C" fn ferravex_pointer(x: f32, y: f32, dx: f32, dy: f32) {
1225 pointer(x, y, dx, dy)
1226 }
1227 #[unsafe(no_mangle)]
1228 pub extern "C" fn ferravex_wheel(delta: f32) {
1229 if delta.is_finite() {
1230 edit(|s| s.wheel += delta);
1231 }
1232 }
1233 #[unsafe(no_mangle)]
1234 pub extern "C" fn ferravex_blur() {
1235 edit(|s| {
1236 s.keys.clear();
1237 s.pressed.clear();
1238 s.buttons.clear();
1239 s.clicks.clear();
1240 s.touches.clear();
1241 s.delta = Vec2::ZERO;
1242 s.wheel = 0.;
1243 })
1244 }
1245 #[unsafe(no_mangle)]
1246 pub extern "C" fn ferravex_touch_mouse() -> u32 {
1247 read(|s| s.touch_mouse as u32)
1248 }
1249 #[unsafe(no_mangle)]
1250 pub extern "C" fn ferravex_grab() -> u32 {
1251 read(|s| s.grab as u32)
1252 }
1253 #[unsafe(no_mangle)]
1254 pub extern "C" fn ferravex_touch(id: u32, phase: u32, x: f32, y: f32) {
1255 touch(id as u64, phase, x, y);
1256 }
1257 #[unsafe(no_mangle)]
1258 pub extern "C" fn ferravex_autoplay(enabled: u32) {
1259 edit(|s| s.autoplay = enabled != 0)
1260 }
1261}
1262pub fn autoplay_enabled() -> bool {
1263 read(|s| s.autoplay)
1264}
1265
1266#[cfg(test)]
1267mod tests {
1268 use super::*;
1269 fn reset() {
1270 edit(|s| *s = State::default());
1271 }
1272 #[test]
1273 fn camera_culled_meshes_remain_available_to_the_shadow_pass() {
1274 reset();
1275 set_quality(lighting::Quality::High);
1276 set_sun_shadow(Some(lighting::SunShadow {
1277 extent: 30.,
1278 ..Default::default()
1279 }))
1280 .unwrap();
1281 set_camera(&Camera3D {
1282 position: vec3(0., 0., 5.),
1283 target: Vec3::ZERO,
1284 z_far: 20.,
1285 ..Default::default()
1286 });
1287 let mesh = mesh::Mesh3d::cuboid(Vec3::ONE, WHITE);
1288 assert!(!draw_resident(
1289 mesh.resident(),
1290 Mat4::from_translation(Vec3::X * 10.)
1291 ));
1292 read(|s| {
1293 assert_eq!(s.retained.batches.len(), 1);
1294 assert_ne!(s.retained.batches[0].depth & 32, 0);
1295 assert_eq!(s.retained.batches[0].depth & 8, 0);
1296 });
1297 }
1298 #[test]
1299 fn shared_camera_culls_residents_and_default_ui_camera_does_not() {
1300 reset();
1301 let mesh = mesh::Mesh3d::cuboid(Vec3::ONE, WHITE);
1302 set_camera(&Camera3D {
1303 position: vec3(0., 0., 5.),
1304 target: Vec3::ZERO,
1305 z_far: 20.,
1306 ..Default::default()
1307 });
1308 assert!(draw_resident(mesh.resident(), Mat4::IDENTITY));
1309 assert!(!draw_resident(
1310 mesh.resident(),
1311 Mat4::from_translation(Vec3::X * 100.)
1312 ));
1313 let stats = visibility_stats();
1314 assert_eq!(
1315 (stats.tested, stats.visible, stats.frustum_culled),
1316 (2, 1, 1)
1317 );
1318 assert!(!set_visibility(visibility::VisibilitySettings {
1319 max_distance: Some(-1.),
1320 ..Default::default()
1321 }));
1322 set_default_camera();
1323 assert!(draw_resident(
1324 mesh.resident(),
1325 Mat4::from_translation(Vec3::X * 100.)
1326 ));
1327 }
1328 #[test]
1329 fn frame_yields_once_and_keeps_game_local_state() {
1330 reset();
1331 let ticks = std::rc::Rc::new(std::cell::Cell::new(0));
1332 let count = ticks.clone();
1333 start(Conf::default(), async move {
1334 for _ in 0..3 {
1335 count.set(count.get() + 1);
1336 next_frame().await;
1337 }
1338 });
1339 for expected in 1..=3 {
1340 assert!(frame(1. / 60., 640, 480));
1341 assert_eq!(ticks.get(), expected);
1342 }
1343 assert!(!frame(1. / 60., 640, 480));
1344 assert_eq!(ticks.get(), 3);
1345 }
1346 #[test]
1347 fn key_edges_survive_until_poll_and_release_on_blur() {
1348 reset();
1349 key(97, true);
1350 key(97, true);
1351 assert!(is_key_down(KeyCode::A));
1352 assert!(is_key_pressed(KeyCode::A));
1353 start(Conf::default(), async {
1354 assert!(is_key_pressed(KeyCode::D));
1355 next_frame().await;
1356 assert!(!is_key_pressed(KeyCode::D));
1357 assert!(is_key_down(KeyCode::D));
1358 });
1359 key(100, true);
1360 assert!(frame(0.01, 1280, 800));
1361 assert!(!frame(0.01, 1280, 800));
1362 key(100, false);
1363 assert!(!is_key_down(KeyCode::D));
1364 }
1365 #[test]
1366 fn screen_camera_projects_top_left_and_bottom_right() {
1367 reset();
1368 edit(|s| {
1369 s.w = 640;
1370 s.h = 480;
1371 s.default_camera()
1372 });
1373 read(|s| {
1374 assert!(
1375 s.matrix
1376 .project_point3(Vec3::ZERO)
1377 .abs_diff_eq(vec3(-1., 1., 0.), 1e-6)
1378 );
1379 assert!(
1380 s.matrix
1381 .project_point3(vec3(640., 480., 0.))
1382 .abs_diff_eq(vec3(1., -1., 0.), 1e-6)
1383 );
1384 });
1385 let c = Camera2D::from_display_rect(Rect::new(10., 20., 100., 50.));
1386 let m = c.matrix(640., 480.);
1387 assert!(
1388 m.project_point3(vec3(10., 20., 0.))
1389 .abs_diff_eq(vec3(-1., 1., 0.), 1e-6)
1390 );
1391 }
1392 #[test]
1393 fn draws_batch_only_adjacent_depth_modes_and_retain_their_camera() {
1394 reset();
1395 draw_rectangle(0., 0., 20., 20., WHITE);
1396 set_camera(&Camera3D::default());
1397 draw_triangle(Vec2::ZERO, Vec2::X, Vec2::Y, RED);
1398 set_default_camera();
1399 draw_triangle(Vec2::ZERO, Vec2::X, Vec2::Y, BLUE);
1400 read(|s| {
1401 assert_eq!(s.vertices.len(), 12);
1402 assert_eq!(
1403 s.batches
1404 .iter()
1405 .map(|b| (b.start, b.count, b.depth))
1406 .collect::<Vec<_>>(),
1407 [(0, 6, false), (6, 3, true), (9, 3, false)]
1408 );
1409 assert_eq!(std::mem::size_of::<Batch>(), 12);
1410 assert_eq!(std::mem::offset_of!(Batch, depth), 8);
1411 });
1412 }
1413 #[test]
1414 fn fast_touch_taps_preserve_start_and_end_without_sticking() {
1415 reset();
1416 start(Conf::default(), async {
1417 next_frame().await;
1418 });
1419 touch(7, 0, 10., 20.);
1420 touch(7, 1, 12., 22.);
1421 assert_eq!(touches()[0].phase, TouchPhase::Started);
1422 touch(7, 2, 12., 22.);
1423 assert_eq!(touches().len(), 2);
1424 frame(f32::NAN, 640, 480);
1425 assert!(touches().is_empty());
1426 assert_eq!(get_frame_time(), 0.);
1427 assert!(get_time().is_finite());
1428 touch(8, 0, 10., 20.);
1429 touch(8, 3, 10., 20.);
1430 assert_eq!(touches().len(), 1);
1431 assert_eq!(touches()[0].phase, TouchPhase::Cancelled);
1432 pointer(f32::NAN, 0., 0., 0.);
1433 assert!(read(|s| s.mouse.is_finite()));
1434 }
1435 #[test]
1436 fn invalid_mesh_triangles_cannot_corrupt_adjacent_batches() {
1437 reset();
1438 let v = Vertex::new2(Vec3::ZERO, Vec2::ZERO, WHITE);
1439 draw_mesh(&Mesh {
1440 vertices: vec![v; 3],
1441 indices: vec![0, 1, 9, 0, 1, 2, 0],
1442 texture: None,
1443 });
1444 read(|s| {
1445 assert_eq!(s.vertices.len(), 3);
1446 assert_eq!(s.batches[0].count, 3);
1447 });
1448 }
1449 #[test]
1450 fn nonuniform_scale_uses_inverse_transpose_for_normals() {
1451 reset();
1452 set_lighting(Some(shaders::LitSettings {
1453 ambient: 0.,
1454 light_direction: Vec3::Y,
1455 fog_start: 1e5,
1456 fog_end: 2e5,
1457 ..Default::default()
1458 }));
1459 let n = vec3(1., 1., 0.).normalize();
1460 let model = Mat4::from_scale(vec3(4., 0.5, 1.));
1461 edit(|s| s.push([(Vec3::ZERO, n, WHITE); 3], model));
1462 read(|s| {
1463 assert!(s.vertices[0][4] > 0.99);
1464 });
1465 }
1466}