ferravex_render/
lib.rs

1//! Original immediate/retained renderer and platform ABI. Game code, input, geometry and
2//! scheduling live in Rust; hosts only bind OS events and GPU submission.
3pub 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}
496/// Submit an immutable mesh snapshot. Compatible adjacent draws share one instanced call.
497/// Returns false when culled, for invalid transforms or a full frame instance budget.
498pub(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}
513/// Submit a hidden opaque caster, for example a first-person character body.
514pub 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) { /* Static glyphs need no atlas allocation. */
739    }
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    /// Captures this frame after GPU submission, including draws queued before this call.
859    #[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}