~starkingdoms/starkingdoms

efdb9b1995399003b2f5cba85c1513ce3ec32504 — core 11 months ago ea95cf2
add things
M Cargo.lock => Cargo.lock +30 -0
@@ 2612,6 2612,21 @@ dependencies = [
]

[[package]]
name = "futures"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
 "futures-channel",
 "futures-core",
 "futures-executor",
 "futures-io",
 "futures-sink",
 "futures-task",
 "futures-util",
]

[[package]]
name = "futures-channel"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 2628,6 2643,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"

[[package]]
name = "futures-executor"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
dependencies = [
 "futures-core",
 "futures-task",
 "futures-util",
]

[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 2675,10 2701,13 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
 "futures-channel",
 "futures-core",
 "futures-io",
 "futures-macro",
 "futures-sink",
 "futures-task",
 "memchr",
 "pin-project-lite",
 "pin-utils",
 "slab",


@@ 5196,6 5225,7 @@ dependencies = [
 "egui",
 "egui-wgpu",
 "egui-winit",
 "futures",
 "image 0.25.5",
 "pollster",
 "thiserror 2.0.9",

M starkingdoms-client/Cargo.toml => starkingdoms-client/Cargo.toml +1 -0
@@ 19,6 19,7 @@ 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"

# WASM dependencies
[target.'cfg(target_arch = "wasm32")'.dependencies]

M starkingdoms-client/src/ecs.rs => starkingdoms-client/src/ecs.rs +7 -7
@@ 4,13 4,13 @@ use bevy_ecs::system::Resource;

#[derive(Component, Debug, Clone, Copy)]
pub struct Position {
    pub x: f64,
    pub y: f64
    pub x: f32,
    pub y: f32
}
#[derive(Component, Debug, Clone, Copy)]
pub struct Scale {
    pub width: f64,
    pub height: f64,
    pub width: f32,
    pub height: f32,
}

#[derive(Component, Debug, Clone)]


@@ 27,7 27,7 @@ pub struct SpriteBundle {

#[derive(Resource, Debug)]
pub struct Camera {
    pub x: f64,
    pub y: f64,
    pub zoom: f64
    pub x: f32,
    pub y: f32,
    pub zoom: f32
}
\ No newline at end of file

M starkingdoms-client/src/lib.rs => starkingdoms-client/src/lib.rs +34 -5
@@ 1,11 1,15 @@
use std::ops::Add;
use std::time::Duration;
use bevy_ecs::event::Events;
use bevy_ecs::schedule::Schedule;
use bevy_ecs::world::World;
use egui::Context;
use tracing::info;
use crate::ecs::Camera;
use web_time::Instant;
use winit::event_loop::{ControlFlow, EventLoop};
use crate::ecs::{Camera, Position, Scale, SpriteBundle, SpriteTexture};
use crate::input::MouseWheelEvent;
use crate::rendering::Renderer;
use crate::rendering::App;
use crate::rendering::ui::UiRenderable;

#[cfg(target_arch = "wasm32")]


@@ 26,21 30,46 @@ pub fn start() {

    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,
        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

    world.spawn(SpriteBundle {
        position: Position { x: 0.0, y: 0.0 },
        scale: Scale { width: 50.0, height: 50.0 },
        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();
}

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");
            });
    }
}
\ No newline at end of file

M starkingdoms-client/src/native/mod.rs => starkingdoms-client/src/native/mod.rs +2 -0
@@ 1,3 1,5 @@
use wgpu::{Backends, Limits};

/// --- IMPORTANT: THIS IS A DUAL TARGET CRATE ---
/// THIS WILL ONLY EXECUTE ON NATIVE
/// DO ONLY PLATFORM SPECIFIC INITIALIZATION HERE

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

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));
    }
}
\ No newline at end of file

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

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

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::new(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::new(window.clone(), world, update_schedule, ui_renderable).await;
                    tx.send(renderer.into()).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) {
        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::new(window.clone(), w, u, t).await;
                                    tx.send(renderer.into()).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::new(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))));
        }
    }
}
\ No newline at end of file

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

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 uniform_buffer: Buffer,
    pub scale_factor: f64,

    pub window: Arc<Window>,

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

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 new(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 (device, queue) = adapter.request_device(&DeviceDescriptor {
            label: Some("Basic render device"),
            required_features: Features::default(),
            required_limits: Limits::downlevel_webgl2_defaults(),
            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 uniform_buffer = device.create_buffer(&BufferDescriptor {
            label: Some("quad uniforms"),
            size: 16,
            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,
            uniform_buffer,
            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<(Position, Scale, SpriteTexture)> = vec![];

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

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

        for (pos, scale, tex) 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,
                )
            });

            // need to calculate width, height, x, and y, using logical size & aspect

            // calculate "viewport position" w/ the camera
            let viewport_x = pos.x - cam.x;
            let viewport_y = pos.y - cam.y;
            let scaled_w = scale.width * cam.zoom;
            let scaled_h = scale.height * cam.zoom;

            let x_screen = viewport_x / self.logical_size.width as f32;
            let y_screen = viewport_y / self.logical_size.height as f32;
            let w_screen = scaled_w / self.logical_size.width as f32;
            let h_screen = scaled_h / self.logical_size.height as f32;

            let mut uniform_buffer = vec![];
            let uniform_buffer_data = [w_screen, h_screen, x_screen, y_screen];
            for i in uniform_buffer_data {
                let mut bytes = i.to_ne_bytes().to_vec();
                uniform_buffer.append(&mut bytes);
            }

            self.queue
                .write_buffer(&self.uniform_buffer, 0, &uniform_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.uniform_buffer.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();
    }
}
\ No newline at end of file

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

#[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
    }
}
\ No newline at end of file

A starkingdoms-client/src/rendering/ui.rs => starkingdoms-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);
}
\ No newline at end of file

M starkingdoms-client/src/wasm/mod.rs => starkingdoms-client/src/wasm/mod.rs +1 -0
@@ 1,6 1,7 @@
use tracing::Level;
use tracing_subscriber::fmt::format::Pretty;
use tracing_subscriber::prelude::*;
use wgpu::{Backends, Limits};
use wasm_bindgen::prelude::wasm_bindgen;
use tracing_web::{MakeWebConsoleWriter, performance_layer};