~starkingdoms/starkingdoms

5ae8d33508c5272d49012190e709523775ff79d6 — ghostly_zsh 11 months ago 0144592
bring back client
A crates/client/Cargo.toml => crates/client/Cargo.toml +35 -0
@@ 0,0 1,35 @@
[package]
name = "starkingdoms-client"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

# Everywhere dependencies
[dependencies]
tracing = "0.1" # Log system
tracing-subscriber = "0.3" # Log layers
bevy_ecs = "0.15"
egui = "0.30"
wgpu = { version = "23", features = ["webgl"] }
winit = "0.30"
thiserror = "2"
image = "0.25"
egui-winit = { version = "0.30", default-features = false, features = ["links", "wayland", "x11"] }
egui-wgpu = "0.30"
web-time = "1"
futures = "0.3"
nalgebra = "0.33"

# WASM dependencies
[target.'cfg(target_arch = "wasm32")'.dependencies]
tracing-web = "0.1" # Log output
console_error_panic_hook = "0.1" # Give useful information in the panic response, other than the useless "entered unreachable code"
wasm-bindgen = "0.2"
web-sys = "0.3"
wasm-bindgen-futures = "0.4"

# Native dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
pollster = "0.4"
\ No newline at end of file

A crates/client/index.html => crates/client/index.html +39 -0
@@ 0,0 1,39 @@
<!DOCTYPE html>
<html>
  <head>
    <title>StarKingdoms.TK</title>

    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />

    <style>
        html, body {
            overflow: hidden;
            margin: 0;
            padding: 0;
            height: 100%;
            width: 100%;
        }

        canvas {
            margin-right: auto;
            margin-left: auto;
            display: block;
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
        }
    </style>
  </head>
  <body>
    <script type="module">
      import init from './pkg/starkingdoms_client.js';
      async function run() {
        await init();
      }
      window.addEventListener('load', run);
    </script>
  </body>
</html>
\ No newline at end of file

A crates/client/src/ecs.rs => crates/client/src/ecs.rs +87 -0
@@ 0,0 1,87 @@
use bevy_ecs::bundle::Bundle;
use bevy_ecs::component::Component;
use bevy_ecs::system::Resource;
use nalgebra::Matrix3;

#[derive(Component, Debug, Clone, Copy)]
pub struct Translation {
    pub x: f32,
    pub y: f32,
}
impl Translation {
    pub fn as_matrix(&self) -> Matrix3<f32> {
        Matrix3::from_iterator([
            1.0, 0.0, self.x,
            0.0, 1.0, self.y,
            0.0, 0.0, 1.0,
        ])
    }
}

#[derive(Component, Debug, Clone, Copy)]
pub struct Shear {
    pub x: f32,
    pub y: f32,
}
impl Shear {
    pub fn as_matrix(&self) -> Matrix3<f32> {
        Matrix3::from_iterator([
            1.0, self.x, 0.0,
            self.y, 1.0, 0.0,
            0.0, 0.0, 1.0,
        ])
    }
}

#[derive(Component, Debug, Clone, Copy)]
pub struct Scale {
    pub width: f32,
    pub height: f32,
}
impl Scale {
    pub fn as_matrix(&self) -> Matrix3<f32> {
        Matrix3::from_iterator([
            self.width, 0.0, 0.0,
            0.0, self.height, 0.0,
            0.0, 0.0, 1.0
        ])
    }
}
#[derive(Component, Debug, Clone, Copy)]
pub struct Rotation {
    pub radians: f32
}
impl Rotation {
    pub fn as_matrix(&self) -> Matrix3<f32> {
        let x = self.radians.cos();
        let y = self.radians.sin();
        Matrix3::from_iterator([
            x, y, 0.0,
            -y, x, 0.0,
            0.0, 0.0, 1.0
        ])
    }
}

#[derive(Component, Debug, Clone)]
pub struct SpriteTexture {
    pub texture: String,
}

#[derive(Bundle)]
pub struct SpriteBundle {
    pub position: Translation,
    pub shear: Shear,
    pub scale: Scale,
    pub texture: SpriteTexture,
    pub rotation: Rotation
}

#[derive(Resource, Debug)]
pub struct Camera {
    pub x: f32,
    pub y: f32,
    pub shear_x: f32,
    pub shear_y: f32,
    pub zoom: f32,
}

A crates/client/src/input.rs => crates/client/src/input.rs +7 -0
@@ 0,0 1,7 @@
use bevy_ecs::event::Event;

#[derive(Event, Debug, Copy, Clone)]
pub enum MouseWheelEvent {
    Line { x: f64, y: f64 },
    Pixel { x: f64, y: f64 },
}

A crates/client/src/lib.rs => crates/client/src/lib.rs +182 -0
@@ 0,0 1,182 @@
use crate::ecs::{Camera, Translation, Rotation, Scale, SpriteBundle, SpriteTexture};
use crate::input::MouseWheelEvent;
use crate::rendering::ui::UiRenderable;
use crate::rendering::App;
use bevy_ecs::event::{EventReader, Events};
use bevy_ecs::schedule::Schedule;
use bevy_ecs::system::ResMut;
use bevy_ecs::world::World;
use ecs::Shear;
use egui::{Context, DragValue};
use tracing::info;
use winit::event_loop::{ControlFlow, EventLoop};

#[cfg(target_arch = "wasm32")]
#[path = "wasm/mod.rs"]
pub mod platform;
#[cfg(not(target_arch = "wasm32"))]
#[path = "native/mod.rs"]
pub mod platform;

pub mod ecs;
pub mod input;
pub mod rendering;

// Hi, you've found the real main function! This is called AFTER platform-specific initialization code.
pub fn start() {
    info!(
        "Hello, world! StarKingdoms.TK v{} says hello, running on {}",
        env!("CARGO_PKG_VERSION"),
        if cfg!(target_arch = "wasm32") {
            "wasm"
        } else {
            "native"
        }
    );

    info!("Creating the ECS world...");
    let mut world = World::new();

    world.insert_resource::<Events<MouseWheelEvent>>(Events::default());
    world.insert_resource(Camera {
        x: 0.0,
        y: 0.0,
        shear_x: 0.0,
        shear_y: 0.0,
        zoom: 1.0,
    });

    let mut start_schedule = Schedule::default();
    // Add startup things here
    // Caution: This will run before there are things on-screen
    start_schedule.run(&mut world);

    let mut update_schedule = Schedule::default();
    // Add things to run every frame here
    // Caution: This will run once before there are things on screen
    update_schedule.add_systems(zoom_camera_on_mouse_events);

    world.spawn(SpriteBundle {
        position: Translation {
            x: 100.0,
            y: 100.0
        },
        shear: Shear {
            x: 0.0,
            y: 0.0,
        },
        scale: Scale {
            width: 100.0,
            height: 100.0,
        },
        rotation: Rotation {
            radians: 45.0_f32.to_radians()
        },
        texture: SpriteTexture {
            texture: "happy-tree".to_string(),
        },
    });

    let event_loop = EventLoop::new().unwrap();
    event_loop.set_control_flow(ControlFlow::Poll);
    event_loop
        .run_app(&mut App::new(world, update_schedule, Gui {}))
        .unwrap();
}

fn zoom_camera_on_mouse_events(mut events: EventReader<MouseWheelEvent>, mut camera: ResMut<Camera>) {
    for event in events.read() {
        let raw_delta = match event {
            MouseWheelEvent::Line { y, .. } => *y,
            MouseWheelEvent::Pixel { y, ..} => *y,
        } as f32;

        let delta = if raw_delta < 0.0 {
            raw_delta * -0.9
        } else {
            raw_delta * 1.1
        };

        if delta < 0.0 {
            camera.zoom *= 1.0 / delta;
        } else {
            camera.zoom *= delta;
        }
    }
}

pub struct Gui {}
impl UiRenderable for Gui {
    fn render(&mut self, ctx: &Context, world: &mut World) {
        egui::Window::new("Main Menu")
            .resizable(false)
            .show(ctx, |ui| {
                ui.heading("StarKingdoms.TK");
                ui.label("A game about floating through space");
                ui.separator();

                let mut sprites = world.query::<(&mut Translation, &mut Shear, &mut Scale, &SpriteTexture, &mut Rotation)>();
                for (mut pos, mut shear, mut scale, tex, mut rot) in sprites.iter_mut(world) {
                    ui.heading(&tex.texture);
                    
                    egui::Grid::new("sprite_grid")
                        .num_columns(2)
                        .spacing([40.0, 4.0])
                        .striped(true)
                        .show(ui, |ui| {
                            ui.label("X");
                            ui.add(DragValue::new(&mut pos.x).speed(0.1));
                            ui.end_row();

                            ui.label("Y");
                            ui.add(DragValue::new(&mut pos.y).speed(0.1));
                            ui.end_row();

                            ui.label("Shear X");
                            ui.add(DragValue::new(&mut shear.x).speed(0.1));
                            ui.end_row();

                            ui.label("Shear Y");
                            ui.add(DragValue::new(&mut shear.y).speed(0.1));
                            ui.end_row();

                            ui.label("Width");
                            ui.add(DragValue::new(&mut scale.width).speed(0.1));
                            ui.end_row();

                            ui.label("Height");
                            ui.add(DragValue::new(&mut scale.height).speed(0.1));
                            ui.end_row();

                            ui.label("Rotation");
                            ui.add(DragValue::new(&mut rot.radians).speed(0.1));
                            ui.end_row();
                        });
                }
                let mut camera = world.resource_mut::<Camera>();
                egui::Grid::new("camera_grid")
                    .num_columns(2)
                    .spacing([40.0, 4.0])
                    .show(ui, |ui| {
                        ui.label("Camera X");
                        ui.add(DragValue::new(&mut camera.x).speed(0.1));
                        ui.end_row();

                        ui.label("Camera Y");
                        ui.add(DragValue::new(&mut camera.y).speed(0.1));
                        ui.end_row();

                        ui.label("Shear X");
                        ui.add(DragValue::new(&mut camera.shear_x).speed(0.1));
                        ui.end_row();
                        ui.label("Shear Y");
                        ui.add(DragValue::new(&mut camera.shear_y).speed(0.1));
                        ui.end_row();

                        ui.label("Camera Zoom");
                        ui.add(DragValue::new(&mut camera.zoom).speed(0.1));
                        ui.end_row();
                    });
            });
    }
}

A crates/client/src/main.rs => crates/client/src/main.rs +5 -0
@@ 0,0 1,5 @@
/// --- IMPORTANT: DO NOT EDIT - EDIT THE APPROPRIATE PLATFORM SPECIFIC ENTRYPOINT ---
/// `platform::start` IS WHAT YOU WANT
fn main() {
    starkingdoms_client::platform::entrypoint();
}

A crates/client/src/native/mod.rs => crates/client/src/native/mod.rs +11 -0
@@ 0,0 1,11 @@
/// --- IMPORTANT: THIS IS A DUAL TARGET CRATE ---
/// THIS WILL ONLY EXECUTE ON NATIVE
/// DO ONLY PLATFORM SPECIFIC INITIALIZATION HERE
/// FOR ACTUAL PROGRAM LOGIC, EDIT `crate::start`
/// DO NOT RENAME
pub fn entrypoint() {
    tracing_subscriber::fmt::init();

    // All done with platform-specific initialization, call back into the common code path
    crate::start();
}

A crates/client/src/rendering/mipmap.rs => crates/client/src/rendering/mipmap.rs +134 -0
@@ 0,0 1,134 @@
use crate::rendering::texture::Texture;
use std::collections::HashMap;
use tracing::debug;
use wgpu::{
    include_wgsl, BindGroupDescriptor, BindGroupEntry, BindingResource, Color, ColorTargetState,
    ColorWrites, CommandEncoderDescriptor, Device, FilterMode, FragmentState, LoadOp, Operations,
    Queue, RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline,
    RenderPipelineDescriptor, Sampler, SamplerDescriptor, ShaderModule, StoreOp, TextureFormat,
    TextureViewDescriptor, VertexState,
};

pub struct MipGenerator {
    shader: ShaderModule,
    sampler: Sampler,
    pipelines: HashMap<TextureFormat, RenderPipeline>,
}
impl MipGenerator {
    pub fn new(device: &Device) -> Self {
        debug!("initializing MipGenerator, compiling shader module");
        let shader = device.create_shader_module(include_wgsl!("../shaders/text_quad_mips.wgsl"));
        Self {
            shader,
            sampler: device.create_sampler(&SamplerDescriptor {
                min_filter: FilterMode::Linear,
                label: Some("MipGenerator sampler"),
                ..Default::default()
            }),
            pipelines: HashMap::new(),
        }
    }

    pub fn generate_mips(&mut self, texture: &Texture, device: &Device, queue: &Queue) {
        let pipeline = self
            .pipelines
            .entry(texture.texture.format())
            .or_insert_with(|| {
                device.create_render_pipeline(&RenderPipelineDescriptor {
                    label: Some("MipGenerator format pipeline"),
                    layout: None,
                    vertex: VertexState {
                        module: &self.shader,
                        entry_point: Some("vs"),
                        compilation_options: Default::default(),
                        buffers: &[],
                    },
                    primitive: Default::default(),
                    depth_stencil: None,
                    multisample: Default::default(),
                    fragment: Some(FragmentState {
                        module: &self.shader,
                        entry_point: Some("fs"),
                        compilation_options: Default::default(),
                        targets: &[Some(ColorTargetState {
                            format: texture.texture.format(),
                            blend: None,
                            write_mask: ColorWrites::default(),
                        })],
                    }),
                    multiview: None,
                    cache: None,
                })
            });

        let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
            label: Some("MipGenerator command encoder"),
        });

        let mut width = texture.texture.width();
        let mut height = texture.texture.height();
        let mut base_mip_level = 0;

        while width > 1 || height > 1 {
            width = 1.max(width / 2);
            height = 1.max(height / 2);

            let bind_group = device.create_bind_group(&BindGroupDescriptor {
                label: Some("MipGenerator bind group"),
                layout: &pipeline.get_bind_group_layout(0),
                entries: &[
                    BindGroupEntry {
                        binding: 0,
                        resource: BindingResource::Sampler(&self.sampler),
                    },
                    BindGroupEntry {
                        binding: 1,
                        resource: BindingResource::TextureView(&texture.texture.create_view(
                            &TextureViewDescriptor {
                                base_mip_level,
                                mip_level_count: Some(1),
                                ..Default::default()
                            },
                        )),
                    },
                ],
            });

            base_mip_level += 1;

            let texture_view = texture.texture.create_view(&TextureViewDescriptor {
                base_mip_level,
                mip_level_count: Some(1),
                ..Default::default()
            });

            let render_pass_descriptor = RenderPassDescriptor {
                label: Some("MipGenerator render pass"),
                color_attachments: &[Some(RenderPassColorAttachment {
                    view: &texture_view,
                    resolve_target: None,
                    ops: Operations {
                        load: LoadOp::Clear(Color {
                            r: 0.0,
                            g: 0.0,
                            b: 0.0,
                            a: 1.0,
                        }),
                        store: StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
            };

            let mut pass = encoder.begin_render_pass(&render_pass_descriptor);
            pass.set_pipeline(pipeline);
            pass.set_bind_group(0, Some(&bind_group), &[]);
            pass.draw(0..6, 0..1);
        }

        let command_buffer = encoder.finish();
        queue.submit(std::iter::once(command_buffer));
    }
}

A crates/client/src/rendering/mod.rs => crates/client/src/rendering/mod.rs +230 -0
@@ 0,0 1,230 @@
mod mipmap;
mod renderer;
mod texture;
pub mod ui;

use crate::input::MouseWheelEvent;
use crate::rendering::renderer::RenderInitRes::{Initialized, NotReadyYet};
#[allow(unused_imports)]
use crate::rendering::renderer::{Renderer, RenderInitRes};
use crate::rendering::ui::UiRenderable;
use bevy_ecs::schedule::Schedule;
use bevy_ecs::world::World;
use std::ops::Add;
use std::process::exit;
use std::sync::Arc;
use std::time::Duration;
use tracing::info;
use web_time::Instant;
use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize;
use winit::event::{MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::window::{Window, WindowId};

pub struct App<T: UiRenderable> {
    window: Option<Arc<Window>>,
    renderer: Option<Renderer<T>>,

    #[cfg(target_arch = "wasm32")]
    renderer_rx: Option<futures::channel::oneshot::Receiver<RenderInitRes<T>>>,

    world: Option<World>,
    update_schedule: Option<Schedule>,
    ui_renderable: Option<T>,
}

impl<T: UiRenderable> App<T> {
    pub fn new(world: World, update_schedule: Schedule, ui_renderable: T) -> Self {
        Self {
            window: None,
            renderer: None,
            #[cfg(target_arch = "wasm32")]
            renderer_rx: None,
            world: Some(world),
            update_schedule: Some(update_schedule),
            ui_renderable: Some(ui_renderable),
        }
    }
}
impl<T: UiRenderable + 'static> ApplicationHandler for App<T> {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.window.is_none() {
            let attributes = Window::default_attributes().with_title("StarKingdoms.TK");

            let window = Arc::new(event_loop.create_window(attributes).unwrap());
            self.window = Some(window.clone());

            let world = self.world.take().unwrap();
            let update_schedule = self.update_schedule.take().unwrap();
            let ui_renderable = self.ui_renderable.take().unwrap();

            #[cfg(not(target_arch = "wasm32"))]
            {
                let renderer = pollster::block_on(async move {
                    Renderer::try_init(window.clone(), world, update_schedule, ui_renderable).await
                });
                match renderer {
                    Initialized(r) => {
                        self.renderer = Some(r);
                    }
                    NotReadyYet(w, u, t) => {
                        self.world = Some(w);
                        self.update_schedule = Some(u);
                        self.ui_renderable = Some(t);
                    }
                }
            }
            #[cfg(target_arch = "wasm32")]
            {
                use winit::platform::web::WindowExtWebSys;
                // Add it to the DOM
                web_sys::window()
                    .unwrap()
                    .document()
                    .unwrap()
                    .body()
                    .unwrap()
                    .append_child(&window.canvas().unwrap())
                    .unwrap();

                let (tx, rx) = futures::channel::oneshot::channel();
                self.renderer_rx = Some(rx);
                wasm_bindgen_futures::spawn_local(async move {
                    let renderer =
                        Renderer::try_init(window.clone(), world, update_schedule, ui_renderable).await;
                    tx.send(renderer).unwrap();
                });
            }
        }
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        _window_id: WindowId,
        event: WindowEvent,
    ) {
        if event == WindowEvent::CloseRequested {
            info!("Close requested by underlying event system");
            event_loop.exit();
            exit(1);
        }
        if let Some(renderer) = &mut self.renderer {
            let egui_response = renderer.gui_winit.on_window_event(&renderer.window, &event);
            if egui_response.consumed {
                return;
            }
        }

        match event {
            WindowEvent::Resized(new) => {
                if let Some(renderer) = &mut self.renderer {
                    if new.width > 0 && new.height > 0 {
                        renderer.size = new;
                        renderer.scale_factor = renderer.window.scale_factor();
                        renderer.logical_size =
                            LogicalSize::from_physical(renderer.size, renderer.scale_factor);

                        renderer.surface_configuration.width = new.width;
                        renderer.surface_configuration.height = new.height;
                        renderer
                            .surface
                            .configure(&renderer.device, &renderer.surface_configuration);
                    }
                }
            }
            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
                if let Some(renderer) = &mut self.renderer {
                    renderer.scale_factor = scale_factor;
                    renderer.logical_size =
                        LogicalSize::from_physical(renderer.size, renderer.scale_factor);
                }
            }
            WindowEvent::MouseWheel { delta, .. } => {
                if let Some(renderer) = &mut self.renderer {
                    renderer.world.send_event(match delta {
                        MouseScrollDelta::PixelDelta(pos) => {
                            MouseWheelEvent::Pixel { x: pos.x, y: pos.y }
                        }
                        MouseScrollDelta::LineDelta(x, y) => MouseWheelEvent::Line {
                            x: x as f64,
                            y: y as f64,
                        },
                    });
                }
            }
            _ => {}
        }
    }

    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
        #[allow(unused_variables)]
        if let Some(window) = self.window.clone() {
            #[cfg(target_arch = "wasm32")]
            {
                let mut renderer_rxd = false;
                if let Some(rx) = self.renderer_rx.as_mut() {
                    if let Ok(Some(renderer)) = rx.try_recv() {
                        match renderer {
                            Initialized(r) => {
                                self.renderer = Some(r);
                                renderer_rxd = true;
                            }
                            NotReadyYet(w, u, t) => {
                                let (tx, rx) = futures::channel::oneshot::channel();
                                self.renderer_rx = Some(rx);
                                wasm_bindgen_futures::spawn_local(async move {
                                    let renderer = Renderer::try_init(window.clone(), w, u, t).await;
                                    tx.send(renderer).unwrap();
                                });
                            }
                        }
                    }
                }
                if renderer_rxd {
                    self.renderer_rx = None;
                }
            }

            #[cfg(not(target_arch = "wasm32"))]
            {
                if self.renderer.is_none() {
                    if let Some(window) = self.window.clone() {
                        let world = self.world.take().unwrap();
                        let update_schedule = self.update_schedule.take().unwrap();
                        let ui_renderable = self.ui_renderable.take().unwrap();

                        let renderer = pollster::block_on(async move {
                            Renderer::try_init(window.clone(), world, update_schedule, ui_renderable)
                                .await
                        });

                        match renderer {
                            Initialized(r) => {
                                self.renderer = Some(r);
                            }
                            NotReadyYet(w, u, t) => {
                                self.world = Some(w);
                                self.update_schedule = Some(u);
                                self.ui_renderable = Some(t);
                            }
                        }

                        return;
                    }
                }
            }

            let Some(renderer) = &mut self.renderer else {
                return;
            };

            renderer.render();

            event_loop.set_control_flow(ControlFlow::WaitUntil(
                Instant::now().add(Duration::from_secs_f64(1.0 / 60.0)),
            ));
        }
    }
}

A crates/client/src/rendering/renderer.rs => crates/client/src/rendering/renderer.rs +389 -0
@@ 0,0 1,389 @@
use crate::ecs::{Camera, Rotation, Scale, Shear, SpriteTexture, Translation};
use crate::rendering::mipmap::MipGenerator;
use crate::rendering::renderer::RenderInitRes::{Initialized, NotReadyYet};
use crate::rendering::texture;
use crate::rendering::ui::UiRenderable;
use bevy_ecs::schedule::Schedule;
use bevy_ecs::world::World;
use egui::ViewportId;
use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
use std::sync::Arc;
use tracing::info;
use web_time::Instant;
use wgpu::SurfaceConfiguration;
use wgpu::{
    include_wgsl, Adapter, Backends, BindGroupDescriptor, BindGroupEntry, BindingResource, Buffer,
    BufferDescriptor, BufferUsages, Color, ColorTargetState, CommandEncoderDescriptor, Device,
    DeviceDescriptor, Features, FragmentState, Instance, InstanceDescriptor, Limits, LoadOp,
    Operations, Queue, RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline,
    RenderPipelineDescriptor, RequestAdapterOptions, ShaderModule, StoreOp, Surface,
    TextureViewDescriptor, VertexState,
};
use winit::dpi::{LogicalSize, PhysicalSize};
use winit::window::Window;

#[allow(unused_attributes, dead_code)]
pub struct Renderer<T: UiRenderable> {
    pub last_frame_time: Instant,

    pub surface: Surface<'static>,
    pub surface_configuration: SurfaceConfiguration,
    pub device: Device,
    pub queue: Queue,
    pub adapter: Adapter,

    pub sprite_shader_module: ShaderModule,
    pub sprite_pipeline: RenderPipeline,

    pub world: World,
    pub update_schedule: Schedule,

    pub gui_ctx: egui::Context,
    pub gui_winit: egui_winit::State,
    pub gui_renderer: egui_wgpu::Renderer,
    pub gui_renderable: T,

    pub textures: HashMap<String, texture::Texture>,
    pub mip_generator: MipGenerator,

    pub frame_uniform: Buffer,
    pub local_uniform: Buffer,
    pub scale_factor: f64,

    pub window: Arc<Window>,

    pub size: PhysicalSize<u32>,
    pub logical_size: LogicalSize<u32>,
}

#[allow(clippy::large_enum_variant)]
pub enum RenderInitRes<T: UiRenderable> {
    Initialized(Renderer<T>),
    NotReadyYet(World, Schedule, T),
}
impl<T: UiRenderable> Debug for RenderInitRes<T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Initialized(..) => write!(f, "[initialized renderer]"),
            NotReadyYet(..) => write!(f, "[pending initialization]"),
        }
    }
}

impl<T: UiRenderable> Renderer<T> {
    pub async fn try_init(
        window: Arc<Window>,
        world: World,
        update_schedule: Schedule,
        gui_renderable: T,
    ) -> RenderInitRes<T> {
        let size = window.inner_size();
        if size.width == 0 || size.height == 0 {
            return NotReadyYet(world, update_schedule, gui_renderable);
        }

        // First, create an instance. This is our handle to wgpu, and is the equivalent of navigator.gpu in WebGPU
        let instance = Instance::new(InstanceDescriptor {
            backends: Backends::all(), // Select the appropriate backend for the HAL to use. Defined in the platform module, as it's platform-specific
            ..Default::default()       // Other fields aren't relevant here
        });

        // Next, get our render surface
        let surface = instance.create_surface(window.clone()).unwrap();

        // Next, request out adapter
        let adapter = instance
            .request_adapter(&RequestAdapterOptions {
                power_preference: Default::default(), // Don't care
                force_fallback_adapter: false,        // We want a real GPU
                compatible_surface: Some(&surface), // Find an adapter that is able to render to our window
            })
            .await
            .unwrap();

        let mut limits = Limits::downlevel_webgl2_defaults();
        limits.max_texture_dimension_1d = 8192;
        limits.max_texture_dimension_2d = 8192;
        limits.max_texture_dimension_3d = 2048;
        let (device, queue) = adapter
            .request_device(
                &DeviceDescriptor {
                    label: Some("Basic render device"),
                    required_features: Features::default(),
                    required_limits: limits,
                    memory_hints: Default::default(),
                },
                None,
            )
            .await
            .unwrap();

        let format = surface
            .get_default_config(&adapter, size.width, size.height)
            .unwrap();

        surface.configure(&device, &format);

        let sprite_shader_module =
            device.create_shader_module(include_wgsl!("../shaders/sprite.wgsl"));
        let sprite_pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
            label: Some("Sprite pipeline"),
            layout: None,
            vertex: VertexState {
                module: &sprite_shader_module,
                entry_point: Some("vs"),
                compilation_options: Default::default(),
                buffers: &[],
            },
            primitive: Default::default(),
            depth_stencil: None,
            multisample: Default::default(),
            fragment: Some(FragmentState {
                module: &sprite_shader_module,
                entry_point: Some("fs"),
                compilation_options: Default::default(),
                targets: &[Some(ColorTargetState {
                    format: format.format,
                    blend: None,
                    write_mask: Default::default(),
                })],
            }),
            multiview: None,
            cache: None,
        });

        let gui_context = egui::Context::default();
        let gui_winit = egui_winit::State::new(
            gui_context.clone(),
            ViewportId::ROOT,
            &window,
            Some(window.scale_factor() as f32),
            None,
            Some(device.limits().max_texture_dimension_2d as usize),
        );
        let gui_renderer = egui_wgpu::Renderer::new(&device, format.format, None, 1, false);

        let frame_uniform = device.create_buffer(&BufferDescriptor {
            label: Some("frame uniforms"),
            size: 48 + 16, // mat3x3f, vec2f
            usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        let local_uniform = device.create_buffer(&BufferDescriptor {
            label: Some("local uniforms"),
            size: 48, // mat3x3f
            usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        Initialized(Self {
            mip_generator: MipGenerator::new(&device),
            textures: HashMap::new(),
            last_frame_time: Instant::now(),
            surface,
            device,
            queue,
            adapter,
            sprite_shader_module,
            sprite_pipeline,
            world,
            update_schedule,
            gui_ctx: gui_context,
            gui_winit,
            gui_renderer,
            gui_renderable,
            logical_size: LogicalSize::from_physical(size, window.scale_factor()),
            scale_factor: window.scale_factor(),
            window,
            frame_uniform,
            local_uniform,
            size,
            surface_configuration: format,
        })
    }

    pub fn render(&mut self) {
        // update the world
        self.update_schedule.run(&mut self.world);
        // update the UI
        let egui_output = self
            .gui_ctx
            .run(self.gui_winit.take_egui_input(&self.window), |ctx| {
                self.gui_renderable.render(ctx, &mut self.world)
            });
        self.gui_winit
            .handle_platform_output(&self.window, egui_output.platform_output);

        let output = self.surface.get_current_texture().unwrap();
        let view = output
            .texture
            .create_view(&TextureViewDescriptor::default());

        let render_pass_descriptor = RenderPassDescriptor {
            label: Some("basic render pass"),
            color_attachments: &[Some(RenderPassColorAttachment {
                view: &view,
                resolve_target: None,
                ops: Operations {
                    load: LoadOp::Clear(Color {
                        r: 0.3,
                        g: 0.3,
                        b: 0.3,
                        a: 1.0,
                    }),
                    store: StoreOp::Store,
                },
            })],
            ..Default::default()
        };
        let mut encoder = self
            .device
            .create_command_encoder(&CommandEncoderDescriptor {
                label: Some("command encoder"),
            });

        let mut sprites_to_render: Vec<(Translation, Shear, Scale, SpriteTexture, Rotation)> = vec![];

        let mut things_to_render = self.world.query::<(&Translation, &Shear, &Scale, &SpriteTexture, &Rotation)>();
        for thing in things_to_render.iter_mut(&mut self.world) {
            sprites_to_render.push((*thing.0, *thing.1, *thing.2, thing.3.clone(), *thing.4));
        }

        let cam = self.world.resource::<Camera>();

        let mut frame_uniform = vec![];
        let frame_uniform_values = [
            cam.zoom,   cam.shear_y,0.0,  0.0,
            cam.shear_x,cam.zoom,   0.0,  0.0,
            cam.x,      cam.y,      1.0,  0.0,
            self.logical_size.width as f32, self.logical_size.height as f32, 0.0, 0.0];
        for i in frame_uniform_values {
            let mut bytes = i.to_ne_bytes().to_vec();
            frame_uniform.append(&mut bytes);
        }

        self.queue.write_buffer(&self.frame_uniform, 0, &frame_uniform);

        for (pos, shear, scale, tex, rot) in sprites_to_render {
            let tex = self.textures.entry(tex.texture.clone()).or_insert_with(|| {
                info!("loading texture {}", &tex.texture);
                let b: &[u8] = match tex.texture.as_str() {
                    "f" => include_bytes!("../textures/f.png"),
                    "happy-tree" => include_bytes!("../textures/happy-tree.png"),
                    "uv" => include_bytes!("../textures/uv.png"),
                    u => panic!("unknown texture {u}, has it been added in rendering::renderer::<impl Renderer>::render()?")
                };
                texture::Texture::new(
                    b,
                    &tex.texture,
                    &self.device,
                    &self.queue,
                    &mut self.mip_generator,
                )
            });

            let xy_matrix = pos.as_matrix();
            let shear_matrix = shear.as_matrix();
            let rot_matrix = rot.as_matrix();
            let scale_matrix = scale.as_matrix();

            let transform_matrix = scale_matrix * shear_matrix * rot_matrix * xy_matrix;


            let mut local_buffer = vec![];
            let local_buffer_data = [
                transform_matrix.m11, transform_matrix.m12, transform_matrix.m13, 0.0,
                transform_matrix.m21, transform_matrix.m22, transform_matrix.m23, 0.0,
                transform_matrix.m31, transform_matrix.m32, transform_matrix.m33, 0.0,
            ];
            for i in local_buffer_data {
                let mut bytes = i.to_ne_bytes().to_vec();
                local_buffer.append(&mut bytes);
            }

            self.queue
                .write_buffer(&self.local_uniform, 0, &local_buffer);

            let bind_group = self.device.create_bind_group(&BindGroupDescriptor {
                label: Some("test_bind_group"),
                layout: &self.sprite_pipeline.get_bind_group_layout(0),
                entries: &[
                    BindGroupEntry {
                        binding: 0,
                        resource: BindingResource::Sampler(&tex.sampler),
                    },
                    BindGroupEntry {
                        binding: 1,
                        resource: BindingResource::TextureView(
                            &tex.texture.create_view(&TextureViewDescriptor::default()),
                        ),
                    },
                    BindGroupEntry {
                        binding: 2,
                        resource: BindingResource::Buffer(
                            self.frame_uniform.as_entire_buffer_binding(),
                        ),
                    },
                    BindGroupEntry {
                        binding: 3,
                        resource: BindingResource::Buffer(
                            self.local_uniform.as_entire_buffer_binding(),
                        ),
                    },
                ],
            });
            let mut pass = encoder.begin_render_pass(&render_pass_descriptor);
            pass.set_pipeline(&self.sprite_pipeline);
            pass.set_bind_group(0, Some(&bind_group), &[]);
            pass.draw(0..6, 0..1);
        }
        // main game rendering done
        // next up: egui UI rendering
        for (id, image_delta) in &egui_output.textures_delta.set {
            self.gui_renderer
                .update_texture(&self.device, &self.queue, *id, image_delta);
        }
        {
            let paint_jobs = self
                .gui_ctx
                .tessellate(egui_output.shapes, self.scale_factor as f32);
            let screen_descriptor = egui_wgpu::ScreenDescriptor {
                size_in_pixels: [self.size.width, self.size.height],
                pixels_per_point: self.scale_factor as f32,
            };
            self.gui_renderer.update_buffers(
                &self.device,
                &self.queue,
                &mut encoder,
                &paint_jobs,
                &screen_descriptor,
            );
            let render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
                label: Some("ui render pass"),
                color_attachments: &[Some(RenderPassColorAttachment {
                    view: &view,
                    resolve_target: None,
                    ops: Operations {
                        load: LoadOp::Load,
                        store: StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
            });
            let mut forgotten_render_pass = render_pass.forget_lifetime();
            self.gui_renderer
                .render(&mut forgotten_render_pass, &paint_jobs, &screen_descriptor);
            for id in egui_output.textures_delta.free {
                self.gui_renderer.free_texture(&id);
            }
        }

        let buffer = encoder.finish();
        self.queue.submit(std::iter::once(buffer));

        output.present();
    }
}

A crates/client/src/rendering/texture.rs => crates/client/src/rendering/texture.rs +77 -0
@@ 0,0 1,77 @@
use crate::rendering::mipmap::MipGenerator;
use image::EncodableLayout;
use wgpu::{
    Device, Extent3d, FilterMode, ImageCopyTexture, ImageDataLayout, Origin3d, Queue,
    SamplerDescriptor, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
};

#[derive(Debug)]
pub struct Texture {
    pub texture: wgpu::Texture,
    pub sampler: wgpu::Sampler,
}
impl Texture {
    pub fn new(
        bytes: &[u8],
        label: &str,
        device: &Device,
        queue: &Queue,
        mip_generator: &mut MipGenerator,
    ) -> Self {
        let img = image::load_from_memory(bytes).unwrap();
        let rgba = img.to_rgba8();

        let max_size = rgba.width().max(rgba.height());
        let log_factor = if max_size == 0 { 0 } else { max_size.ilog2() };
        let optimal_mip_levels = 1 + log_factor;

        let texture = device.create_texture(&TextureDescriptor {
            label: Some(label),
            size: Extent3d {
                width: rgba.width(),
                height: rgba.height(),
                depth_or_array_layers: 1,
            },
            mip_level_count: optimal_mip_levels,
            sample_count: 1,
            dimension: TextureDimension::D2,
            format: TextureFormat::Rgba8UnormSrgb,
            usage: TextureUsages::TEXTURE_BINDING
                | TextureUsages::COPY_DST
                | TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[TextureFormat::Rgba8UnormSrgb],
        });
        queue.write_texture(
            ImageCopyTexture {
                texture: &texture,
                mip_level: 0,
                origin: Origin3d::ZERO,
                aspect: Default::default(),
            },
            rgba.as_bytes(),
            ImageDataLayout {
                offset: 0,
                bytes_per_row: Some(rgba.width() * 4),
                rows_per_image: Some(rgba.height()),
            },
            Extent3d {
                width: rgba.width(),
                height: rgba.height(),
                depth_or_array_layers: 1,
            },
        );

        let sampler = device.create_sampler(&SamplerDescriptor {
            label: Some("test_sampler"),
            mag_filter: FilterMode::Linear,
            min_filter: FilterMode::Linear,
            ..Default::default()
        });

        let tex = Self { texture, sampler };

        mip_generator.generate_mips(&tex, device, queue);

        tex
    }
}

A crates/client/src/rendering/ui.rs => crates/client/src/rendering/ui.rs +5 -0
@@ 0,0 1,5 @@
use bevy_ecs::world::World;

pub trait UiRenderable {
    fn render(&mut self, ctx: &egui::Context, world: &mut World);
}

A crates/client/src/shaders/sprite.wgsl => crates/client/src/shaders/sprite.wgsl +52 -0
@@ 0,0 1,52 @@
struct VertexShaderOut {
    @builtin(position) position: vec4<f32>,
    @location(0) texcoord: vec2<f32>
}

struct FrameUniforms {
    camera_transform: mat3x3f,
    viewport_size: vec2f,
}
struct LocalUniforms {
    transform: mat3x3f,
}
@group(0) @binding(2) var<uniform> frame_uni: FrameUniforms;
@group(0) @binding(3) var<uniform> local_uni: LocalUniforms;

@vertex fn vs(
    @builtin(vertex_index) vertexIndex : u32
) -> VertexShaderOut {
    let pos = array(
            vec2<f32>(-0.5, -0.5),
            vec2<f32>(0.5, -0.5),
            vec2<f32>(-0.5, 0.5),
            vec2<f32>(-0.5, 0.5),
            vec2<f32>(0.5, -0.5),
            vec2<f32>(0.5, 0.5)
        );

    var vsOutput: VertexShaderOut;

    let homogeneous_position = frame_uni.camera_transform * local_uni.transform * vec3f(pos[vertexIndex], 1);
    let position = homogeneous_position.xy / homogeneous_position.z;
    // convert from pixels to 0.0 to 1.0
    let zeroToOne = position / frame_uni.viewport_size;
    // convert from 0 - 1 to 0 - 2
    let zeroToTwo = zeroToOne * 2.0;
    // convert from 0 - 2 to -1 - +1 (clip space)
    let flippedClipSpace = zeroToTwo - 1.0;
    // flip Y
    let clipSpace = flippedClipSpace * vec2f(1, -1);

    vsOutput.position = vec4f(clipSpace, 0.0, 1.0);
    vsOutput.texcoord = pos[vertexIndex] + vec2f(0.5, 0.5);

    return vsOutput;
}

@group(0) @binding(0) var tex_sampler: sampler;
@group(0) @binding(1) var tex: texture_2d<f32>;

@fragment fn fs(fsInput: VertexShaderOut) -> @location(0) vec4<f32> {
    return textureSample(tex, tex_sampler, fsInput.texcoord);
}

A crates/client/src/shaders/text_quad_mips.wgsl => crates/client/src/shaders/text_quad_mips.wgsl +26 -0
@@ 0,0 1,26 @@
struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) texcoord: vec2<f32>
}
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
    let pos = array(
        vec2<f32>(0.0, 0.0),
        vec2<f32>(1.0, 0.0),
        vec2<f32>(0.0, 1.0),
        vec2<f32>(0.0, 1.0),
        vec2<f32>(1.0, 0.0),
        vec2<f32>(1.0, 1.0)
    );
    var out: VertexOutput;
    let xy = pos[vertexIndex];
    out.position = vec4<f32>(xy * 2.0 - 1.0, 0.0, 1.0);
    out.texcoord = vec2<f32>(xy.x, 1.0 - xy.y);
    return out;
}

@group(0) @binding(0) var texSampler: sampler;
@group(0) @binding(1) var tex: texture_2d<f32>;

@fragment fn fs(inp: VertexOutput) -> @location(0) vec4<f32> {
    return textureSample(tex, texSampler, inp.texcoord);
}
\ No newline at end of file

A crates/client/src/textures/f.png => crates/client/src/textures/f.png +0 -0
A crates/client/src/textures/happy-tree.png => crates/client/src/textures/happy-tree.png +0 -0
A crates/client/src/textures/uv.png => crates/client/src/textures/uv.png +0 -0
A crates/client/src/wasm/mod.rs => crates/client/src/wasm/mod.rs +33 -0
@@ 0,0 1,33 @@
use tracing::Level;
use tracing_subscriber::fmt::format::Pretty;
use tracing_subscriber::prelude::*;
use tracing_web::{performance_layer, MakeWebConsoleWriter};
use wasm_bindgen::prelude::wasm_bindgen;

/// --- IMPORTANT: THIS IS A DUAL TARGET CRATE ---
/// THIS WILL ONLY EXECUTE ON WEBASSEMBLY
/// DO ONLY PLATFORM SPECIFIC INITIALIZATION HERE
/// FOR ACTUAL PROGRAM LOGIC, EDIT `crate::start`
/// DO NOT RENAME
#[wasm_bindgen(start)]
pub fn entrypoint() {
    // Panic handler
    console_error_panic_hook::set_once();

    /* ----- Logging setup ----- */
    let fmt_layer = tracing_subscriber::fmt::layer()
        .with_ansi(false) // not supported in browsers
        .without_time() // std::time doesn't exist in wasm
        .with_writer(MakeWebConsoleWriter::new().with_max_level(Level::DEBUG)); // wgpu spams the console, and this is slow as hell

    let perf_layer = performance_layer() // enable performance tracing
        .with_details_from_fields(Pretty::default()); // ... with pretty fields

    tracing_subscriber::registry()
        .with(fmt_layer)
        .with(perf_layer)
        .init(); // register the logger

    // All done with platform-specific initialization, call back into the common code path
    crate::start();
}