1#![allow(
3 clippy::cast_precision_loss,
4 clippy::cast_possible_truncation,
5 clippy::cast_sign_loss
6)]
7pub mod atlas;
46mod camera_follow;
47mod screen_shake;
48mod text;
49pub mod textbox;
50
51pub use camera_follow::CameraFollow2d;
52pub use screen_shake::ScreenShake;
53
54use rustc_hash::FxHashMap as HashMap;
55use std::sync::Arc;
56
57use bevy_ecs::prelude::*;
58use bevy_ecs::schedule::IntoScheduleConfigs;
59use lunar_assets::{AssetServer, Font, Handle, Texture};
60use lunar_core::{App, GamePlugin, Time};
61use lunar_math::{Color, Transform, Vec2};
62
63#[allow(dead_code)]
65#[derive(Clone, Copy)]
66struct SpriteDrawParams {
67 position: Vec2,
68 rotation: f32,
69 scale: Vec2,
70 tint: Color,
71 uv_rect: Option<(Vec2, Vec2)>,
72 origin: Vec2,
73}
74
75#[derive(Debug, Clone, Copy)]
79pub struct SpriteParams {
80 pub position: Vec2,
82 pub scale: Vec2,
84 pub rotation: f32,
86 pub origin: Vec2,
88 pub tint: Color,
90}
91
92#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Resource)]
97pub struct RenderTargetId(pub u32);
98
99#[derive(Resource, Default)]
102pub struct RenderTargetStore {
103 entries: rustc_hash::FxHashMap<RenderTargetId, lunar_assets::Handle<lunar_assets::Texture>>,
104}
105
106impl RenderTargetStore {
107 #[must_use]
109 pub fn get_texture(
110 &self,
111 id: RenderTargetId,
112 ) -> Option<lunar_assets::Handle<lunar_assets::Texture>> {
113 self.entries.get(&id).copied()
114 }
115}
116
117#[derive(Resource, Clone)]
141pub struct Camera {
142 pub position: Vec2,
144 pub zoom: f32,
146 pub rotation: f32,
148 pub viewport: Option<(u32, u32)>,
150 pub layer_parallax: HashMap<i32, Vec2>,
152 pub target: Option<RenderTargetId>,
154}
155
156impl Camera {
157 #[must_use]
159 pub fn new() -> Self {
160 Self {
161 position: Vec2::ZERO,
162 zoom: 1.0,
163 rotation: 0.0,
164 viewport: None,
165 layer_parallax: HashMap::default(),
166 target: None,
167 }
168 }
169
170 #[must_use]
172 pub fn at_position(x: f32, y: f32) -> Self {
173 Self {
174 position: Vec2::new(x, y),
175 zoom: 1.0,
176 rotation: 0.0,
177 viewport: None,
178 layer_parallax: HashMap::default(),
179 target: None,
180 }
181 }
182
183 #[must_use]
197 pub fn projection_matrix(&self, window_width: u32, window_height: u32) -> [f32; 16] {
198 self.projection_matrix_for_layer(0, window_width, window_height)
199 }
200
201 #[must_use]
206 pub fn projection_matrix_for_layer(
207 &self,
208 layer: i32,
209 window_width: u32,
210 window_height: u32,
211 ) -> [f32; 16] {
212 let parallax_offset = self
213 .layer_parallax
214 .get(&layer)
215 .copied()
216 .unwrap_or(Vec2::ZERO);
217 let effective_pos = self.position - parallax_offset;
218 self.projection_matrix_at(effective_pos, window_width, window_height)
219 }
220
221 fn projection_matrix_at(&self, pos: Vec2, window_width: u32, window_height: u32) -> [f32; 16] {
223 #[allow(clippy::cast_precision_loss)]
224 let w = window_width as f32;
225 #[allow(clippy::cast_precision_loss)]
226 let h = window_height as f32;
227 let zoom = self.zoom.max(0.001);
228 let cos = self.rotation.cos();
229 let sin = self.rotation.sin();
230
231 let sx = 2.0 / w * zoom;
233 let sy = -2.0 / h * zoom;
234
235 let tx = -pos.y.mul_add(-sin, pos.x * cos);
237 let ty = -pos.y.mul_add(cos, pos.x * sin);
238
239 [
241 sx,
242 0.0,
243 0.0,
244 0.0,
245 0.0,
246 sy,
247 0.0,
248 0.0,
249 0.0,
250 0.0,
251 1.0,
252 0.0,
253 sx * tx,
254 sy * ty,
255 0.0,
256 1.0,
257 ]
258 }
259
260 pub fn set_layer_parallax(&mut self, layer: i32, offset: Vec2) {
265 self.layer_parallax.insert(layer, offset);
266 }
267
268 pub fn clear_layer_parallax(&mut self, layer: i32) {
270 self.layer_parallax.remove(&layer);
271 }
272
273 #[must_use]
288 pub fn screen_to_world(&self, screen: Vec2, window_width: u32, window_height: u32) -> Vec2 {
289 let (vw, vh) = self.viewport.unwrap_or((window_width, window_height));
290 #[allow(clippy::cast_precision_loss)]
291 let (vw_f, vh_f) = (vw as f32, vh as f32);
292
293 let zoom = self.zoom.max(0.001);
294 let cos = self.rotation.cos();
295 let sin = self.rotation.sin();
296
297 let nx = screen.x / vw_f - 0.5;
299 let ny = screen.y / vh_f - 0.5;
300 let world_dx = nx * vw_f / zoom;
301 let world_dy = ny * vh_f / zoom;
302
303 let unrot_x = world_dx * cos + world_dy * sin;
305 let unrot_y = -world_dx * sin + world_dy * cos;
306
307 Vec2::new(self.position.x + unrot_x, self.position.y + unrot_y)
308 }
309
310 #[must_use]
315 pub fn world_to_screen(&self, world: Vec2, window_width: u32, window_height: u32) -> Vec2 {
316 let (vw, vh) = self.viewport.unwrap_or((window_width, window_height));
317 #[allow(clippy::cast_precision_loss)]
318 let (vw_f, vh_f) = (vw as f32, vh as f32);
319
320 let zoom = self.zoom.max(0.001);
321 let cos = self.rotation.cos();
322 let sin = self.rotation.sin();
323
324 let dx = world.x - self.position.x;
326 let dy = world.y - self.position.y;
327 let rx = dx * cos - dy * sin;
328 let ry = dx * sin + dy * cos;
329
330 let sx = rx * zoom / vw_f;
332 let sy = -ry * zoom / vh_f;
333
334 Vec2::new((sx + 0.5) * vw_f, (0.5 - sy) * vh_f)
335 }
336
337 pub fn set_target_aspect(&mut self, width: u32, height: u32) -> &mut Self {
345 self.viewport = Some((width, height));
346 self
347 }
348}
349
350impl Default for Camera {
351 fn default() -> Self {
352 Self::new()
353 }
354}
355
356#[derive(Debug, Clone)]
361pub struct RenderConfig {
362 pub width: u32,
364 pub height: u32,
366 pub vsync: bool,
368 pub frame_cap: u32,
370 pub tick_rate: lunar_core::TickRate,
373 pub title: String,
375 pub target_aspect: Option<f32>,
378 pub allow_resize: bool,
380}
381
382impl Default for RenderConfig {
383 fn default() -> Self {
384 Self {
385 width: 1280,
386 height: 720,
387 vsync: true,
388 frame_cap: 0,
389 tick_rate: lunar_core::TickRate::Hz60,
390 title: "Lunar".to_string(),
391 target_aspect: None,
392 allow_resize: true,
393 }
394 }
395}
396
397impl RenderConfig {
398 #[must_use]
402 pub fn loop_config(&self) -> lunar_core::LoopConfig {
403 lunar_core::LoopConfig {
404 frame_cap: self.frame_cap,
405 tick_rate: self.tick_rate,
406 }
407 }
408}
409
410const INITIAL_VERTEX_CAPACITY: usize = 65536;
415
416const GLYPH_ATLAS_BIND_ID: u32 = u32::MAX - 1;
419
420const VERTEX_BUFFER_COUNT: usize = 2;
422
423const VERTEX_STRIDE: usize = 20;
425
426#[cfg_attr(not(target_arch = "wasm32"), derive(Resource))]
431pub struct RenderEngine {
432 surface: wgpu::Surface<'static>,
433 device: wgpu::Device,
434 queue: wgpu::Queue,
435 config: wgpu::SurfaceConfiguration,
436 render_config: RenderConfig,
437 sprite_pipeline: wgpu::RenderPipeline,
438 uniform_buf: wgpu::Buffer,
439 globals_bg: wgpu::BindGroup,
441 material_bgl: wgpu::BindGroupLayout,
443 sampler: wgpu::Sampler,
444 textures: HashMap<u32, GpuTexture>,
445 material_bgs: HashMap<u32, wgpu::BindGroup>,
447 vertex_bufs: [wgpu::Buffer; VERTEX_BUFFER_COUNT],
449 vertex_capacity: usize,
452 overflow_flag: bool,
455 frame_index: usize,
457 vertex_offset: usize,
459 glyph_atlas: text::GlyphAtlas,
460 #[allow(dead_code)]
461 glyph_atlas_texture: Option<GpuTexture>,
462 render_passes: Vec<Box<dyn RenderPass>>,
463 #[cfg(not(target_arch = "wasm32"))]
465 pipeline_cache: Option<wgpu::PipelineCache>,
466 sorted_indices: Vec<usize>,
469 text_quads: HashMap<usize, Vec<text::TextGlyphQuad>>,
472 text_layout_cache: text::TextLayoutCache,
475 render_target_views: HashMap<u32, wgpu::TextureView>,
478 render_target_counter: u32,
480}
481
482#[allow(dead_code)]
484struct GpuTexture {
485 texture: wgpu::Texture,
486 view: wgpu::TextureView,
487}
488
489impl RenderEngine {
490 #[cfg(not(target_arch = "wasm32"))]
496 pub fn from_surface(
497 instance: &wgpu::Instance,
498 surface: wgpu::Surface<'static>,
499 config: RenderConfig,
500 ) -> Self {
501 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
502 power_preference: wgpu::PowerPreference::HighPerformance,
503 force_fallback_adapter: false,
504 compatible_surface: Some(&surface),
505 }))
506 .expect("failed to request adapter");
507
508 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
509 label: Some("lunar render device"),
510 required_features: wgpu::Features::empty(),
511 required_limits: wgpu::Limits::default(),
512 memory_hints: wgpu::MemoryHints::Performance,
513 trace: wgpu::Trace::default(),
514 experimental_features: wgpu::ExperimentalFeatures::disabled(),
515 }))
516 .expect("failed to request device");
517
518 Self::init_inner(&adapter, &device, queue, surface, config)
519 }
520
521 #[cfg(target_arch = "wasm32")]
523 pub async fn from_surface(
524 instance: &wgpu::Instance,
525 surface: wgpu::Surface<'static>,
526 config: RenderConfig,
527 ) -> Self {
528 let adapter = instance
529 .request_adapter(&wgpu::RequestAdapterOptions {
530 power_preference: wgpu::PowerPreference::HighPerformance,
531 force_fallback_adapter: false,
532 compatible_surface: Some(&surface),
533 })
534 .await
535 .expect("no WebGPU adapter found — in Firefox enable dom.webgpu.enabled in about:config, Chrome 113+ required");
536
537 let (device, queue) = adapter
538 .request_device(&wgpu::DeviceDescriptor {
539 label: Some("lunar render device"),
540 required_features: wgpu::Features::empty(),
541 required_limits: wgpu::Limits::default(),
542 memory_hints: wgpu::MemoryHints::Performance,
543 trace: wgpu::Trace::default(),
544 experimental_features: wgpu::ExperimentalFeatures::disabled(),
545 })
546 .await
547 .expect("failed to request device");
548
549 Self::init_inner(&adapter, &device, queue, surface, config)
550 }
551
552 #[cfg(target_arch = "wasm32")]
559 pub fn create_canvas_surface(
560 instance: &wgpu::Instance,
561 canvas: &web_sys::HtmlCanvasElement,
562 ) -> Result<wgpu::Surface<'static>, String> {
563 let surface = instance
564 .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
565 .map_err(|e| format!("failed to create surface: {e:?}"))?;
566 Ok(surface)
567 }
568
569 #[cfg(target_arch = "wasm32")]
576 pub fn find_canvas(id: &str) -> Result<web_sys::HtmlCanvasElement, String> {
577 use wasm_bindgen::JsCast;
578 let window = web_sys::window().ok_or("no window")?;
579 let document = window.document().ok_or("no document")?;
580 let element = document
581 .get_element_by_id(id)
582 .ok_or_else(|| format!("no element with id '{id}'"))?;
583 element
584 .dyn_into::<web_sys::HtmlCanvasElement>()
585 .map_err(|_| format!("element '{id}' is not a canvas"))
586 }
587
588 #[allow(clippy::too_many_lines)]
589 fn init_inner(
590 adapter: &wgpu::Adapter,
591 device: &wgpu::Device,
592 queue: wgpu::Queue,
593 surface: wgpu::Surface<'static>,
594 config: RenderConfig,
595 ) -> Self {
596 let caps = surface.get_capabilities(adapter);
597 let format = caps
598 .formats
599 .first()
600 .copied()
601 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb);
602
603 let surface_config = wgpu::SurfaceConfiguration {
604 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
605 format,
606 width: config.width,
607 height: config.height,
608 present_mode: if config.vsync {
609 wgpu::PresentMode::AutoVsync
610 } else {
611 wgpu::PresentMode::AutoNoVsync
612 },
613 alpha_mode: caps.alpha_modes.first().copied().unwrap_or_default(),
614 view_formats: vec![],
615 desired_maximum_frame_latency: 2,
616 };
617
618 surface.configure(device, &surface_config);
619
620 let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
622 label: Some("uniform buffer"),
623 size: 64,
624 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
625 mapped_at_creation: false,
626 });
627
628 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
629 label: Some("sprite sampler"),
630 address_mode_u: wgpu::AddressMode::ClampToEdge,
631 address_mode_v: wgpu::AddressMode::ClampToEdge,
632 mag_filter: wgpu::FilterMode::Nearest,
633 min_filter: wgpu::FilterMode::Nearest,
634 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
635 ..Default::default()
636 });
637
638 let globals_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
640 label: Some("[globals] bgl"),
641 entries: &[wgpu::BindGroupLayoutEntry {
642 binding: 0,
643 visibility: wgpu::ShaderStages::VERTEX,
644 ty: wgpu::BindingType::Buffer {
645 ty: wgpu::BufferBindingType::Uniform,
646 has_dynamic_offset: false,
647 min_binding_size: None,
648 },
649 count: None,
650 }],
651 });
652
653 let material_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
655 label: Some("[material] bgl"),
656 entries: &[
657 wgpu::BindGroupLayoutEntry {
658 binding: 0,
659 visibility: wgpu::ShaderStages::FRAGMENT,
660 ty: wgpu::BindingType::Texture {
661 sample_type: wgpu::TextureSampleType::Float { filterable: true },
662 view_dimension: wgpu::TextureViewDimension::D2,
663 multisampled: false,
664 },
665 count: None,
666 },
667 wgpu::BindGroupLayoutEntry {
668 binding: 1,
669 visibility: wgpu::ShaderStages::FRAGMENT,
670 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
671 count: None,
672 },
673 ],
674 });
675
676 let globals_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
677 label: Some("[globals] bg"),
678 layout: &globals_bgl,
679 entries: &[wgpu::BindGroupEntry {
680 binding: 0,
681 resource: uniform_buf.as_entire_binding(),
682 }],
683 });
684
685 let placeholder_texture = device.create_texture(&wgpu::TextureDescriptor {
687 label: Some("white 1x1"),
688 size: wgpu::Extent3d {
689 width: 1,
690 height: 1,
691 depth_or_array_layers: 1,
692 },
693 mip_level_count: 1,
694 sample_count: 1,
695 dimension: wgpu::TextureDimension::D2,
696 format: wgpu::TextureFormat::Rgba8UnormSrgb,
697 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
698 view_formats: &[],
699 });
700 queue.write_texture(
701 wgpu::TexelCopyTextureInfo {
702 texture: &placeholder_texture,
703 mip_level: 0,
704 origin: wgpu::Origin3d::ZERO,
705 aspect: wgpu::TextureAspect::All,
706 },
707 &[255u8, 255, 255, 255],
708 wgpu::TexelCopyBufferLayout {
709 offset: 0,
710 bytes_per_row: Some(4),
711 rows_per_image: Some(1),
712 },
713 wgpu::Extent3d {
714 width: 1,
715 height: 1,
716 depth_or_array_layers: 1,
717 },
718 );
719 let placeholder_view =
720 placeholder_texture.create_view(&wgpu::TextureViewDescriptor::default());
721
722 let placeholder_material_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
723 label: Some("[material] placeholder bg"),
724 layout: &material_bgl,
725 entries: &[
726 wgpu::BindGroupEntry {
727 binding: 0,
728 resource: wgpu::BindingResource::TextureView(&placeholder_view),
729 },
730 wgpu::BindGroupEntry {
731 binding: 1,
732 resource: wgpu::BindingResource::Sampler(&sampler),
733 },
734 ],
735 });
736
737 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
738 label: Some("sprite shader"),
739 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(SHADER_SOURCE)),
740 });
741
742 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
743 label: Some("sprite pipeline layout"),
744 bind_group_layouts: &[Some(&globals_bgl), Some(&material_bgl)],
745 immediate_size: 0,
746 });
747
748 let sprite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
750 label: Some("sprite pipeline"),
751 layout: Some(&pipeline_layout),
752 vertex: wgpu::VertexState {
753 module: &shader,
754 entry_point: Some("vs_main"),
755 buffers: &[wgpu::VertexBufferLayout {
756 array_stride: VERTEX_STRIDE as u64,
757 step_mode: wgpu::VertexStepMode::Vertex,
758 attributes: &[
759 wgpu::VertexAttribute {
760 format: wgpu::VertexFormat::Float32x2,
761 offset: 0,
762 shader_location: 0,
763 },
764 wgpu::VertexAttribute {
765 format: wgpu::VertexFormat::Float32x2,
766 offset: 8,
767 shader_location: 1,
768 },
769 wgpu::VertexAttribute {
770 format: wgpu::VertexFormat::Unorm8x4,
771 offset: 16,
772 shader_location: 2,
773 },
774 ],
775 }],
776 compilation_options: wgpu::PipelineCompilationOptions::default(),
777 },
778 fragment: Some(wgpu::FragmentState {
779 module: &shader,
780 entry_point: Some("fs_main"),
781 targets: &[Some(wgpu::ColorTargetState {
782 format: surface_config.format,
783 blend: Some(wgpu::BlendState {
784 color: wgpu::BlendComponent {
785 src_factor: wgpu::BlendFactor::SrcAlpha,
786 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
787 operation: wgpu::BlendOperation::Add,
788 },
789 alpha: wgpu::BlendComponent {
790 src_factor: wgpu::BlendFactor::One,
791 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
792 operation: wgpu::BlendOperation::Add,
793 },
794 }),
795 write_mask: wgpu::ColorWrites::ALL,
796 })],
797 compilation_options: wgpu::PipelineCompilationOptions::default(),
798 }),
799 primitive: wgpu::PrimitiveState {
800 topology: wgpu::PrimitiveTopology::TriangleList,
801 strip_index_format: None,
802 front_face: wgpu::FrontFace::Ccw,
803 cull_mode: None,
804 polygon_mode: wgpu::PolygonMode::Fill,
805 unclipped_depth: false,
806 conservative: false,
807 },
808 depth_stencil: None,
809 multisample: wgpu::MultisampleState::default(),
810 cache: None, multiview_mask: None,
812 });
813
814 let frame_cap_str = if config.frame_cap == 0 {
815 "uncapped".to_string()
816 } else {
817 config.frame_cap.to_string()
818 };
819 log::info!(
820 "render engine initialized: {}x{}, frame_cap={}",
821 config.width,
822 config.height,
823 frame_cap_str
824 );
825
826 let vertex_bufs: [wgpu::Buffer; VERTEX_BUFFER_COUNT] = std::array::from_fn(|i| {
829 device.create_buffer(&wgpu::BufferDescriptor {
830 label: Some(&format!("persistent vertex buffer {i}")),
831 size: (INITIAL_VERTEX_CAPACITY * VERTEX_STRIDE) as u64,
832 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
833 mapped_at_creation: false,
834 })
835 });
836
837 Self {
838 surface,
839 device: device.clone(),
840 queue,
841 config: surface_config,
842 render_config: config,
843 sprite_pipeline,
844 uniform_buf,
845 globals_bg,
846 material_bgl,
847 sampler,
848 textures: HashMap::default(),
849 material_bgs: {
850 let mut map = HashMap::default();
851 map.insert(u32::MAX, placeholder_material_bg);
852 map
853 },
854 vertex_bufs,
855 vertex_capacity: INITIAL_VERTEX_CAPACITY,
856 overflow_flag: false,
857 frame_index: 0,
858 vertex_offset: 0,
859 glyph_atlas: text::GlyphAtlas::new(2048, 1024),
860 glyph_atlas_texture: None,
861 render_passes: Vec::new(),
862 #[cfg(not(target_arch = "wasm32"))]
863 pipeline_cache: Self::load_pipeline_cache(device),
864 sorted_indices: Vec::new(),
865 text_quads: HashMap::default(),
866 text_layout_cache: text::TextLayoutCache::new(256),
867 render_target_views: HashMap::default(),
868 render_target_counter: 0,
869 }
870 }
871
872 fn update_projection_for_layer(&mut self, layer: i32, camera: Option<&Camera>) {
875 let (surface_w, surface_h) = (self.config.width as f32, self.config.height as f32);
876
877 if layer >= layers::POST_PROCESS {
879 let projection: [f32; 16] = [
880 2.0 / surface_w,
881 0.0,
882 0.0,
883 0.0,
884 0.0,
885 -2.0 / surface_h,
886 0.0,
887 0.0,
888 0.0,
889 0.0,
890 1.0,
891 0.0,
892 -1.0,
893 1.0,
894 0.0,
895 1.0,
896 ];
897 self.queue
898 .write_buffer(&self.uniform_buf, 0, bytemuck::cast_slice(&projection));
899 return;
900 }
901
902 let projection = if let Some(cam) = camera
905 && let Some((vp_w, vp_h)) = cam.viewport
906 {
907 let (vp_w, vp_h) = (vp_w as f32, vp_h as f32);
908 let scale = (surface_w / vp_w).min(surface_h / vp_h);
910 let vp_w_scaled = vp_w * scale;
911 let vp_h_scaled = vp_h * scale;
912
913 let offset_x = (surface_w - vp_w_scaled) / surface_w;
915 let offset_y = (surface_h - vp_h_scaled) / surface_h;
916
917 let _sx = (vp_w_scaled / surface_w) * 2.0 / vp_w;
921 let _sy = -(vp_h_scaled / surface_h) * 2.0 / vp_h;
922 let pos = cam.position;
923 let tx = pos.x;
924 let ty = pos.y;
925
926 let left = -1.0 + offset_x;
932 let right = 1.0 - offset_x;
933 let bottom = -1.0 + offset_y;
934 let top = 1.0 - offset_y;
935
936 let sx2 = (right - left) / vp_w;
942 let sy2 = (bottom - top) / vp_h;
943 let tx2 = (right + left) / 2.0;
944 let ty2 = (top + bottom) / 2.0;
945
946 [
947 sx2,
948 0.0,
949 0.0,
950 0.0,
951 0.0,
952 sy2,
953 0.0,
954 0.0,
955 0.0,
956 0.0,
957 1.0,
958 0.0,
959 sx2 * (-tx) + tx2,
960 sy2 * (-ty) + ty2,
961 0.0,
962 1.0,
963 ]
964 } else if let Some(cam) = camera {
965 cam.projection_matrix_for_layer(layer, self.config.width, self.config.height)
966 } else {
967 [
968 2.0 / surface_w,
969 0.0,
970 0.0,
971 0.0,
972 0.0,
973 -2.0 / surface_h,
974 0.0,
975 0.0,
976 0.0,
977 0.0,
978 1.0,
979 0.0,
980 -1.0,
981 1.0,
982 0.0,
983 1.0,
984 ]
985 };
986 self.queue
987 .write_buffer(&self.uniform_buf, 0, bytemuck::cast_slice(&projection));
988 }
989
990 #[cfg(not(target_arch = "wasm32"))]
992 fn load_pipeline_cache(device: &wgpu::Device) -> Option<wgpu::PipelineCache> {
993 let cache_path = std::path::Path::new(".pipeline_cache.bin");
994 if cache_path.exists() {
995 match std::fs::read(cache_path) {
996 Ok(data) => {
997 log::info!("loaded pipeline cache ({} bytes)", data.len());
998 Some(unsafe {
1005 device.create_pipeline_cache(&wgpu::PipelineCacheDescriptor {
1006 label: Some("loaded pipeline cache"),
1007 data: Some(&data),
1008 fallback: true,
1009 })
1010 })
1011 }
1012 Err(e) => {
1013 log::warn!("failed to load pipeline cache: {e}");
1014 None
1015 }
1016 }
1017 } else {
1018 None
1019 }
1020 }
1021
1022 #[cfg(not(target_arch = "wasm32"))]
1025 pub fn save_pipeline_cache(&self) {
1026 if let Some(ref cache) = self.pipeline_cache
1027 && let Some(data) = cache.get_data()
1028 {
1029 let cache_path = std::path::Path::new(".pipeline_cache.bin");
1030 if let Err(e) = std::fs::write(cache_path, &data) {
1031 log::warn!("failed to save pipeline cache: {e}");
1032 } else {
1033 log::info!("saved pipeline cache ({} bytes)", data.len());
1034 }
1035 }
1036 }
1037
1038 pub fn add_render_pass<P: RenderPass>(&mut self, pass: P) {
1041 self.render_passes.push(Box::new(pass));
1042 }
1043
1044 pub const fn config(&self) -> &RenderConfig {
1046 &self.render_config
1047 }
1048
1049 pub const fn device(&self) -> &wgpu::Device {
1051 &self.device
1052 }
1053
1054 pub const fn queue(&self) -> &wgpu::Queue {
1056 &self.queue
1057 }
1058
1059 pub fn resize(&mut self, width: u32, height: u32) {
1061 self.config.width = width;
1062 self.config.height = height;
1063 self.surface.configure(&self.device, &self.config);
1064 self.render_config.width = width;
1065 self.render_config.height = height;
1066 }
1067
1068 pub fn remove_texture(&mut self, tex_id: u32) {
1071 self.textures.remove(&tex_id);
1072 self.material_bgs.remove(&tex_id);
1073 self.render_target_views.remove(&tex_id);
1074 }
1075
1076 pub fn create_render_target(
1085 &mut self,
1086 store: &mut RenderTargetStore,
1087 width: u32,
1088 height: u32,
1089 ) -> (RenderTargetId, Handle<Texture>) {
1090 let id = self.render_target_counter;
1091 self.render_target_counter += 1;
1092
1093 let tex_id = u32::MAX / 2 + id;
1095
1096 let gpu_texture = self.device.create_texture(&wgpu::TextureDescriptor {
1097 label: Some(&format!("[rt:{tex_id}]")),
1098 size: wgpu::Extent3d {
1099 width,
1100 height,
1101 depth_or_array_layers: 1,
1102 },
1103 mip_level_count: 1,
1104 sample_count: 1,
1105 dimension: wgpu::TextureDimension::D2,
1106 format: self.config.format,
1107 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1108 | wgpu::TextureUsages::TEXTURE_BINDING
1109 | wgpu::TextureUsages::COPY_SRC,
1110 view_formats: &[],
1111 });
1112 let render_view = gpu_texture.create_view(&wgpu::TextureViewDescriptor::default());
1116 let sample_view = gpu_texture.create_view(&wgpu::TextureViewDescriptor::default());
1117 self.render_target_views.insert(tex_id, render_view);
1118 self.textures.insert(
1119 tex_id,
1120 GpuTexture {
1121 texture: gpu_texture,
1122 view: sample_view,
1123 },
1124 );
1125
1126 let rt_id = RenderTargetId(tex_id);
1127 let handle = Handle::<Texture>::new(tex_id, 0);
1128 store.entries.insert(rt_id, handle);
1129 (rt_id, handle)
1130 }
1131
1132 pub fn upload_font(&mut self, font_id: u32, data: &[u8]) {
1135 self.glyph_atlas.register_font(font_id, data);
1136 }
1137
1138 pub fn upload_texture(&mut self, handle: &Handle<Texture>, texture: &Texture) {
1141 if self.textures.contains_key(&handle.id()) {
1142 return;
1143 }
1144
1145 let mip_count = texture.mip_level_count();
1146 let gpu_texture = self.device.create_texture(&wgpu::TextureDescriptor {
1147 label: Some("sprite texture"),
1148 size: wgpu::Extent3d {
1149 width: texture.width,
1150 height: texture.height,
1151 depth_or_array_layers: 1,
1152 },
1153 mip_level_count: mip_count,
1154 sample_count: 1,
1155 dimension: wgpu::TextureDimension::D2,
1156 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1157 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1158 view_formats: &[],
1159 });
1160
1161 self.queue.write_texture(
1163 wgpu::TexelCopyTextureInfo {
1164 texture: &gpu_texture,
1165 mip_level: 0,
1166 origin: wgpu::Origin3d::ZERO,
1167 aspect: wgpu::TextureAspect::All,
1168 },
1169 &texture.pixels,
1170 wgpu::TexelCopyBufferLayout {
1171 offset: 0,
1172 bytes_per_row: Some(4 * texture.width),
1173 rows_per_image: Some(texture.height),
1174 },
1175 wgpu::Extent3d {
1176 width: texture.width,
1177 height: texture.height,
1178 depth_or_array_layers: 1,
1179 },
1180 );
1181 let mut mip_w = texture.width;
1183 let mut mip_h = texture.height;
1184 for (i, mip_data) in texture.mips.iter().enumerate() {
1185 mip_w = (mip_w / 2).max(1);
1186 mip_h = (mip_h / 2).max(1);
1187 self.queue.write_texture(
1188 wgpu::TexelCopyTextureInfo {
1189 texture: &gpu_texture,
1190 mip_level: i as u32 + 1,
1191 origin: wgpu::Origin3d::ZERO,
1192 aspect: wgpu::TextureAspect::All,
1193 },
1194 mip_data,
1195 wgpu::TexelCopyBufferLayout {
1196 offset: 0,
1197 bytes_per_row: Some(4 * mip_w),
1198 rows_per_image: Some(mip_h),
1199 },
1200 wgpu::Extent3d {
1201 width: mip_w,
1202 height: mip_h,
1203 depth_or_array_layers: 1,
1204 },
1205 );
1206 }
1207
1208 let view = gpu_texture.create_view(&wgpu::TextureViewDescriptor::default());
1209 let tex_id = handle.id();
1210
1211 let material_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1213 label: Some("[material] texture bg"),
1214 layout: &self.material_bgl,
1215 entries: &[
1216 wgpu::BindGroupEntry {
1217 binding: 0,
1218 resource: wgpu::BindingResource::TextureView(&view),
1219 },
1220 wgpu::BindGroupEntry {
1221 binding: 1,
1222 resource: wgpu::BindingResource::Sampler(&self.sampler),
1223 },
1224 ],
1225 });
1226 self.material_bgs.insert(tex_id, material_bg);
1227
1228 self.textures.insert(
1229 tex_id,
1230 GpuTexture {
1231 texture: gpu_texture,
1232 view,
1233 },
1234 );
1235 }
1236
1237 #[allow(dead_code)]
1239 fn upload_glyph_atlas(&mut self) {
1240 let atlas = &self.glyph_atlas;
1241 let gpu_texture = self.device.create_texture(&wgpu::TextureDescriptor {
1242 label: Some("glyph atlas texture"),
1243 size: wgpu::Extent3d {
1244 width: atlas.width,
1245 height: atlas.height,
1246 depth_or_array_layers: 1,
1247 },
1248 mip_level_count: 1,
1249 sample_count: 1,
1250 dimension: wgpu::TextureDimension::D2,
1251 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1252 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1253 view_formats: &[],
1254 });
1255
1256 self.queue.write_texture(
1257 wgpu::TexelCopyTextureInfo {
1258 texture: &gpu_texture,
1259 mip_level: 0,
1260 origin: wgpu::Origin3d::ZERO,
1261 aspect: wgpu::TextureAspect::All,
1262 },
1263 atlas.pixels(),
1264 wgpu::TexelCopyBufferLayout {
1265 offset: 0,
1266 bytes_per_row: Some(4 * atlas.width),
1267 rows_per_image: Some(atlas.height),
1268 },
1269 wgpu::Extent3d {
1270 width: atlas.width,
1271 height: atlas.height,
1272 depth_or_array_layers: 1,
1273 },
1274 );
1275
1276 let view = gpu_texture.create_view(&wgpu::TextureViewDescriptor::default());
1277 self.glyph_atlas_texture = Some(GpuTexture {
1278 texture: gpu_texture,
1279 view,
1280 });
1281 }
1282
1283 pub fn surface_size(&self) -> (u32, u32) {
1285 (self.config.width, self.config.height)
1286 }
1287
1288 fn grow_vertex_buffers(&mut self) {
1291 let new_capacity = self.vertex_capacity.saturating_mul(2);
1292 log::warn!(
1293 "render: vertex buffer overflow detected; growing capacity {} → {} vertices",
1294 self.vertex_capacity,
1295 new_capacity
1296 );
1297 self.vertex_capacity = new_capacity;
1298 self.vertex_bufs = std::array::from_fn(|i| {
1299 self.device.create_buffer(&wgpu::BufferDescriptor {
1300 label: Some(&format!("persistent vertex buffer {i}")),
1301 size: (self.vertex_capacity * VERTEX_STRIDE) as u64,
1302 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1303 mapped_at_creation: false,
1304 })
1305 });
1306 }
1307
1308 #[allow(clippy::too_many_lines)]
1312 pub fn render(
1313 &mut self,
1314 commands: &[DrawCommand],
1315 camera: Option<&Camera>,
1316 render_info: &mut RenderInfo,
1317 ) {
1318 if self.overflow_flag {
1320 self.grow_vertex_buffers();
1321 self.overflow_flag = false;
1322 }
1323
1324 let mut sprite_count: u32 = 0;
1328 let mut draw_calls: u32 = 0;
1329
1330 let rt_tex_id = camera.and_then(|c| c.target).map(|rt| rt.0);
1332
1333 let surface_frame = if rt_tex_id.is_none() {
1335 match self.surface.get_current_texture() {
1336 wgpu::CurrentSurfaceTexture::Success(f)
1337 | wgpu::CurrentSurfaceTexture::Suboptimal(f) => Some(f),
1338 _ => return,
1339 }
1340 } else {
1341 None
1342 };
1343
1344 let view: wgpu::TextureView = if let Some(id) = rt_tex_id {
1348 let Some(rt_view) = self.render_target_views.get(&id) else {
1349 return;
1350 };
1351 rt_view.clone()
1352 } else {
1353 surface_frame
1354 .as_ref()
1355 .unwrap()
1356 .texture
1357 .create_view(&wgpu::TextureViewDescriptor::default())
1358 };
1359
1360 let text_scale = if let Some(cam) = camera
1363 && let Some((vp_w, vp_h)) = cam.viewport
1364 {
1365 let sx = self.config.width as f32 / vp_w as f32;
1366 let sy = self.config.height as f32 / vp_h as f32;
1367 sx.min(sy)
1368 } else {
1369 1.0
1370 };
1371 if self.glyph_atlas.set_scale(text_scale) {
1372 self.text_layout_cache.clear();
1374 }
1375
1376 let cmd_count = commands.len();
1381 for (i, cmd) in commands.iter().enumerate() {
1382 let DrawKind::Text {
1383 font,
1384 content,
1385 position,
1386 font_size,
1387 wrap_width,
1388 line_height,
1389 ..
1390 } = &cmd.kind
1391 else {
1392 continue;
1393 };
1394 let font_id = u32::try_from(font.unwrap_or(0)).unwrap_or(u32::MAX);
1395 let slot = self.text_quads.entry(i).or_default();
1396
1397 if let Some(cached) =
1398 self.text_layout_cache
1399 .get(font_id, content, *font_size, *wrap_width)
1400 {
1401 slot.clear();
1403 slot.extend(cached.iter().map(|q| text::TextGlyphQuad {
1404 position: Vec2::new(q.position.x + position.x, q.position.y + position.y),
1405 size: q.size,
1406 uv_min: q.uv_min,
1407 uv_max: q.uv_max,
1408 }));
1409 } else {
1410 let mut origin_quads: Vec<text::TextGlyphQuad> = Vec::new();
1412 if let Some(max_w) = wrap_width {
1413 text::layout_text_wrapped_into(
1414 &mut self.glyph_atlas,
1415 font_id,
1416 content,
1417 *font_size,
1418 Vec2::ZERO,
1419 *max_w,
1420 *line_height,
1421 &mut origin_quads,
1422 );
1423 } else {
1424 text::layout_text_into(
1425 &mut self.glyph_atlas,
1426 font_id,
1427 content,
1428 *font_size,
1429 Vec2::ZERO,
1430 &mut origin_quads,
1431 );
1432 }
1433 self.text_layout_cache.insert(
1434 font_id,
1435 content,
1436 *font_size,
1437 *wrap_width,
1438 origin_quads.clone(),
1439 );
1440 slot.clear();
1441 slot.extend(origin_quads.into_iter().map(|q| text::TextGlyphQuad {
1442 position: Vec2::new(q.position.x + position.x, q.position.y + position.y),
1443 size: q.size,
1444 uv_min: q.uv_min,
1445 uv_max: q.uv_max,
1446 }));
1447 }
1448 }
1449 self.text_quads.retain(|k, _| *k < cmd_count);
1450 if std::mem::take(&mut self.glyph_atlas.dirty) {
1451 self.upload_glyph_atlas();
1452 if let Some(atlas) = &self.glyph_atlas_texture {
1453 let material_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1454 label: Some("[material] glyph atlas bg"),
1455 layout: &self.material_bgl,
1456 entries: &[
1457 wgpu::BindGroupEntry {
1458 binding: 0,
1459 resource: wgpu::BindingResource::TextureView(&atlas.view),
1460 },
1461 wgpu::BindGroupEntry {
1462 binding: 1,
1463 resource: wgpu::BindingResource::Sampler(&self.sampler),
1464 },
1465 ],
1466 });
1467 self.material_bgs.insert(GLYPH_ATLAS_BIND_ID, material_bg);
1468 }
1469 }
1470
1471 let mut current_layer: Option<i32> = None;
1473
1474 self.sorted_indices.clear();
1477 self.sorted_indices.extend(0..commands.len());
1478 self.sorted_indices.sort_unstable_by_key(|&i| {
1479 let cmd = &commands[i];
1480 let layer = match &cmd.kind {
1481 DrawKind::Sprite { layer, .. }
1482 | DrawKind::Rect { layer, .. }
1483 | DrawKind::Line { layer, .. }
1484 | DrawKind::Text { layer, .. } => *layer,
1485 };
1486 let secondary: i64 = match &cmd.kind {
1487 DrawKind::Sprite {
1488 sort_key: Some(k), ..
1489 } => i64::from(*k),
1490 DrawKind::Sprite {
1491 texture: Some(id), ..
1492 } => i64::try_from(*id).unwrap_or(i64::MAX),
1493 DrawKind::Text { .. } => i64::from(GLYPH_ATLAS_BIND_ID),
1494 _ => i64::MAX,
1495 };
1496 (layer, secondary)
1497 });
1498
1499 let mut encoder = self
1500 .device
1501 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1502 label: Some("render encoder"),
1503 });
1504
1505 {
1506 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1507 label: Some("render pass"),
1508 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1509 view: &view,
1510 resolve_target: None,
1511 ops: wgpu::Operations {
1512 load: wgpu::LoadOp::Clear(wgpu::Color {
1513 r: 0.07,
1514 g: 0.07,
1515 b: 0.07,
1516 a: 1.0,
1517 }),
1518 store: wgpu::StoreOp::Store,
1519 },
1520 depth_slice: None,
1521 })],
1522 depth_stencil_attachment: None,
1523 timestamp_writes: None,
1524 occlusion_query_set: None,
1525 multiview_mask: None,
1526 });
1527
1528 pass.set_pipeline(&self.sprite_pipeline);
1529
1530 self.frame_index = (self.frame_index + 1) % VERTEX_BUFFER_COUNT;
1532 self.vertex_offset = 0;
1534
1535 let mut current_tex: Option<u32> = None;
1539 let mut batch_start = 0;
1540
1541 for i in 0..self.sorted_indices.len() {
1542 let orig_idx = self.sorted_indices[i];
1543 let command = &commands[orig_idx];
1544 let layer = match &command.kind {
1545 DrawKind::Sprite { layer, .. }
1546 | DrawKind::Rect { layer, .. }
1547 | DrawKind::Line { layer, .. }
1548 | DrawKind::Text { layer, .. } => *layer,
1549 };
1550
1551 let tex_id = match &command.kind {
1552 DrawKind::Sprite {
1553 texture: Some(id), ..
1554 } => u32::try_from(*id).unwrap_or(u32::MAX),
1555 DrawKind::Text { .. } => GLYPH_ATLAS_BIND_ID,
1556 _ => u32::MAX,
1557 };
1558
1559 if current_layer != Some(layer) {
1561 if self.vertex_offset > batch_start
1562 && let Some(prev_tex) = current_tex
1563 {
1564 let vertex_count = (self.vertex_offset - batch_start) / VERTEX_STRIDE;
1565 self.draw_vertex_batch(&mut pass, prev_tex, batch_start, vertex_count);
1566 draw_calls += 1;
1567 }
1568 batch_start = self.vertex_offset;
1569 self.update_projection_for_layer(layer, camera);
1570 current_layer = Some(layer);
1571 }
1572
1573 if current_tex != Some(tex_id) {
1575 if self.vertex_offset > batch_start
1576 && let Some(prev_tex) = current_tex
1577 {
1578 let vertex_count = (self.vertex_offset - batch_start) / VERTEX_STRIDE;
1579 self.draw_vertex_batch(&mut pass, prev_tex, batch_start, vertex_count);
1580 draw_calls += 1;
1581 }
1582 batch_start = self.vertex_offset;
1583 current_tex = Some(tex_id);
1584 }
1585
1586 if self.vertex_offset + 6 * VERTEX_STRIDE > self.vertex_capacity * VERTEX_STRIDE {
1587 self.overflow_flag = true;
1588 continue;
1589 }
1590
1591 match &command.kind {
1592 DrawKind::Sprite {
1593 texture: Some(_),
1594 position,
1595 rotation,
1596 scale,
1597 tint,
1598 uv_rect,
1599 origin,
1600 ..
1601 } => {
1602 self.write_sprite_vertices(&SpriteDrawParams {
1603 position: *position,
1604 rotation: *rotation,
1605 scale: *scale,
1606 tint: *tint,
1607 uv_rect: *uv_rect,
1608 origin: *origin,
1609 });
1610 sprite_count += 1;
1611 }
1612 DrawKind::Sprite { texture: None, .. } => {}
1613 DrawKind::Rect {
1614 position,
1615 size,
1616 color,
1617 ..
1618 } => {
1619 self.write_rect_vertices(*position, *size, *color);
1620 }
1621 DrawKind::Line {
1622 start,
1623 end,
1624 color,
1625 thickness,
1626 ..
1627 } => {
1628 self.write_line_vertices(*start, *end, *color, *thickness);
1629 }
1630 DrawKind::Text { color, .. } => {
1631 let color = *color;
1632 let count = self.text_quads.get(&orig_idx).map(|q| q.len()).unwrap_or(0);
1633 for qi in 0..count {
1634 let quad = self.text_quads[&orig_idx][qi]; self.write_text_quad(&quad, color);
1636 }
1637 }
1638 }
1639 }
1640
1641 if self.vertex_offset > batch_start
1643 && let Some(tex_id) = current_tex
1644 {
1645 let vertex_count = (self.vertex_offset - batch_start) / VERTEX_STRIDE;
1646 self.draw_vertex_batch(&mut pass, tex_id, batch_start, vertex_count);
1647 draw_calls += 1;
1648 }
1649 }
1650
1651 for pass in &self.render_passes {
1653 let mut custom_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1654 label: Some(pass.name()),
1655 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1656 view: &view,
1657 resolve_target: None,
1658 ops: wgpu::Operations {
1659 load: wgpu::LoadOp::Load,
1660 store: wgpu::StoreOp::Store,
1661 },
1662 depth_slice: None,
1663 })],
1664 depth_stencil_attachment: None,
1665 timestamp_writes: None,
1666 occlusion_query_set: None,
1667 multiview_mask: None,
1668 });
1669 pass.execute(&self.device, &self.queue, &mut custom_pass);
1670 }
1671
1672 self.queue.submit(Some(encoder.finish()));
1673 if let Some(frame) = surface_frame {
1674 frame.present();
1675 }
1676
1677 render_info.window_width = self.config.width;
1678 render_info.window_height = self.config.height;
1679 render_info.sprite_count = sprite_count;
1680 render_info.draw_calls = draw_calls;
1681 }
1682
1683 fn draw_vertex_batch(
1685 &self,
1686 pass: &mut wgpu::RenderPass<'_>,
1687 tex_id: u32,
1688 offset: usize,
1689 vertex_count: usize,
1690 ) {
1691 let Some(material_bg) = self.material_bgs.get(&tex_id) else {
1692 return;
1693 };
1694 pass.set_bind_group(0, &self.globals_bg, &[]);
1695 pass.set_bind_group(1, material_bg, &[]);
1696 let buf = &self.vertex_bufs[self.frame_index];
1697 pass.set_vertex_buffer(
1698 0,
1699 buf.slice(offset as u64..(offset + vertex_count * VERTEX_STRIDE) as u64),
1700 );
1701 pass.draw(0..u32::try_from(vertex_count).unwrap_or(0), 0..1);
1702 }
1703
1704 fn write_sprite_vertices(&mut self, params: &SpriteDrawParams) {
1708 let &SpriteDrawParams {
1709 position,
1710 rotation,
1711 scale,
1712 tint,
1713 uv_rect,
1714 origin,
1715 } = params;
1716 let cos = rotation.cos();
1717 let sin = rotation.sin();
1718
1719 let corners = [
1721 [-origin.x, -origin.y],
1722 [scale.x - origin.x, -origin.y],
1723 [-origin.x, scale.y - origin.y],
1724 [-origin.x, scale.y - origin.y],
1725 [scale.x - origin.x, -origin.y],
1726 [scale.x - origin.x, scale.y - origin.y],
1727 ];
1728
1729 let (uv_min, uv_max) = uv_rect.unwrap_or((Vec2::ZERO, Vec2::new(1.0, 1.0)));
1730 let uvs = [
1731 [uv_min.x, uv_min.y],
1732 [uv_max.x, uv_min.y],
1733 [uv_min.x, uv_max.y],
1734 [uv_min.x, uv_max.y],
1735 [uv_max.x, uv_min.y],
1736 [uv_max.x, uv_max.y],
1737 ];
1738
1739 let packed_color = pack_color(tint);
1740 let mut verts: [u32; 30] = [0; 30];
1742 for (i, [lx, ly]) in corners.iter().enumerate() {
1743 let rx = lx * cos - ly * sin;
1744 let ry = lx * sin + ly * cos;
1745 let px = position.x + rx;
1746 let py = position.y + ry;
1747 let [u, v] = uvs[i];
1748 let base = i * 5;
1749 verts[base] = f32_to_u32(px);
1750 verts[base + 1] = f32_to_u32(py);
1751 verts[base + 2] = f32_to_u32(u);
1752 verts[base + 3] = f32_to_u32(v);
1753 verts[base + 4] = packed_color;
1754 }
1755
1756 let bytes = bytemuck::cast_slice(&verts);
1757 let buf = &self.vertex_bufs[self.frame_index];
1758 self.queue
1759 .write_buffer(buf, self.vertex_offset as u64, bytes);
1760 self.vertex_offset += 6 * VERTEX_STRIDE;
1761 }
1762
1763 fn write_rect_vertices(&mut self, position: Vec2, size: Vec2, color: Color) {
1766 let (x, y, w, h) = (position.x, position.y, size.x, size.y);
1767 let packed_color = pack_color(color);
1768 let mut verts: [u32; 30] = [0; 30];
1770 let positions = [
1771 (x, y),
1772 (x + w, y),
1773 (x, y + h),
1774 (x, y + h),
1775 (x + w, y),
1776 (x + w, y + h),
1777 ];
1778 for (i, (px, py)) in positions.iter().enumerate() {
1779 let base = i * 5;
1780 verts[base] = f32_to_u32(*px);
1781 verts[base + 1] = f32_to_u32(*py);
1782 verts[base + 2] = 0; verts[base + 3] = 0; verts[base + 4] = packed_color;
1785 }
1786 let bytes = bytemuck::cast_slice(&verts);
1787 let buf = &self.vertex_bufs[self.frame_index];
1788 self.queue
1789 .write_buffer(buf, self.vertex_offset as u64, bytes);
1790 self.vertex_offset += 6 * VERTEX_STRIDE;
1791 }
1792
1793 fn write_line_vertices(&mut self, start: Vec2, end: Vec2, color: Color, thickness: f32) {
1796 let dx = end.x - start.x;
1797 let dy = end.y - start.y;
1798 let len = dx.hypot(dy);
1799 if len < 0.001 {
1800 return;
1801 }
1802 let nx = -dy / len;
1804 let ny = dx / len;
1805 let half_t = thickness * 0.5;
1806 let corners = [
1808 (nx.mul_add(half_t, start.x), ny.mul_add(half_t, start.y)),
1809 (nx.mul_add(-half_t, start.x), ny.mul_add(-half_t, start.y)),
1810 (nx.mul_add(half_t, end.x), ny.mul_add(half_t, end.y)),
1811 (nx.mul_add(-half_t, end.x), ny.mul_add(-half_t, end.y)),
1812 ];
1813 let packed_color = pack_color(color);
1814 let mut verts: [u32; 30] = [0; 30];
1816 let indices = [0, 1, 2, 2, 1, 3];
1817 for (i, &idx) in indices.iter().enumerate() {
1818 let base = i * 5;
1819 let (px, py) = corners[idx];
1820 verts[base] = f32_to_u32(px);
1821 verts[base + 1] = f32_to_u32(py);
1822 verts[base + 2] = 0; verts[base + 3] = 0; verts[base + 4] = packed_color;
1825 }
1826 let bytes = bytemuck::cast_slice(&verts);
1827 let buf = &self.vertex_bufs[self.frame_index];
1828 self.queue
1829 .write_buffer(buf, self.vertex_offset as u64, bytes);
1830 self.vertex_offset += 6 * VERTEX_STRIDE;
1831 }
1832
1833 fn write_text_quad(&mut self, quad: &text::TextGlyphQuad, color: Color) {
1836 let x = quad.position.x;
1837 let y = quad.position.y;
1838 let w = quad.size.x;
1839 let h = quad.size.y;
1840 let u0 = quad.uv_min.x;
1841 let v0 = quad.uv_min.y;
1842 let u1 = quad.uv_max.x;
1843 let v1 = quad.uv_max.y;
1844 let packed_color = pack_color(color);
1845 let mut verts: [u32; 30] = [0; 30];
1847 let positions_uvs = [
1848 (x, y, u0, v0),
1849 (x + w, y, u1, v0),
1850 (x, y + h, u0, v1),
1851 (x, y + h, u0, v1),
1852 (x + w, y, u1, v0),
1853 (x + w, y + h, u1, v1),
1854 ];
1855 for (i, (px, py, u, v)) in positions_uvs.iter().enumerate() {
1856 let base = i * 5;
1857 verts[base] = f32_to_u32(*px);
1858 verts[base + 1] = f32_to_u32(*py);
1859 verts[base + 2] = f32_to_u32(*u);
1860 verts[base + 3] = f32_to_u32(*v);
1861 verts[base + 4] = packed_color;
1862 }
1863 let bytes = bytemuck::cast_slice(&verts);
1864 let buf = &self.vertex_bufs[self.frame_index];
1865 self.queue
1866 .write_buffer(buf, self.vertex_offset as u64, bytes);
1867 self.vertex_offset += 6 * VERTEX_STRIDE;
1868 }
1869}
1870
1871#[cfg(not(target_arch = "wasm32"))]
1873impl Drop for RenderEngine {
1874 fn drop(&mut self) {
1875 self.save_pipeline_cache();
1876 }
1877}
1878
1879fn pack_color(color: Color) -> u32 {
1881 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
1882 let r = (color.r * 255.0).clamp(0.0, 255.0) as u32;
1883 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
1884 let g = (color.g * 255.0).clamp(0.0, 255.0) as u32;
1885 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
1886 let b = (color.b * 255.0).clamp(0.0, 255.0) as u32;
1887 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
1888 let a = (color.a * 255.0).clamp(0.0, 255.0) as u32;
1889 (a << 24) | (b << 16) | (g << 8) | r
1890}
1891
1892fn f32_to_u32(value: f32) -> u32 {
1894 bytemuck::cast(value)
1895}
1896
1897const SHADER_SOURCE: &str = r"
1898struct Uniforms { projection: mat4x4<f32> }
1899
1900struct VertexOut {
1901 @builtin(position) clip_position: vec4<f32>,
1902 @location(0) uv: vec2<f32>,
1903 @location(1) color: vec4<f32>,
1904}
1905
1906@group(0) @binding(0) var<uniform> uniforms: Uniforms;
1907@group(1) @binding(0) var sprite_texture: texture_2d<f32>;
1908@group(1) @binding(1) var sprite_sampler: sampler;
1909
1910@vertex
1911fn vs_main(
1912 @location(0) pos: vec2<f32>,
1913 @location(1) uv: vec2<f32>,
1914 @location(2) color: vec4<f32>,
1915) -> VertexOut {
1916 var out: VertexOut;
1917 out.clip_position = uniforms.projection * vec4<f32>(pos, 0.0, 1.0);
1918 out.uv = uv;
1919 out.color = color;
1920 return out;
1921}
1922
1923@fragment
1924fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
1925 let tex_color = textureSample(sprite_texture, sprite_sampler, in.uv);
1926 return tex_color * in.color;
1927}
1928";
1929
1930#[derive(Resource)]
1940pub struct RenderQueue {
1941 commands: Vec<DrawCommand>,
1942 target: Option<u32>,
1944}
1945
1946#[doc(hidden)]
1953#[derive(Debug, Clone)]
1954pub struct DrawCommand {
1955 pub kind: DrawKind,
1957}
1958
1959#[doc(hidden)]
1963#[derive(Debug, Clone)]
1964pub enum DrawKind {
1965 Sprite {
1969 texture: Option<u64>,
1970 position: Vec2,
1971 rotation: f32,
1972 scale: Vec2,
1973 tint: Color,
1974 layer: i32,
1975 uv_rect: Option<(Vec2, Vec2)>,
1976 origin: Vec2,
1977 sort_key: Option<i32>,
1979 },
1980 Rect {
1982 position: Vec2,
1983 size: Vec2,
1984 color: Color,
1985 layer: i32,
1986 },
1987 Line {
1989 start: Vec2,
1990 end: Vec2,
1991 color: Color,
1992 thickness: f32,
1993 layer: i32,
1994 },
1995 Text {
1997 font: Option<u64>,
1998 content: Arc<str>,
1999 position: Vec2,
2000 font_size: f32,
2001 color: Color,
2002 layer: i32,
2003 wrap_width: Option<f32>,
2005 line_height: f32,
2007 },
2008}
2009
2010pub mod layers {
2013 pub const BACKGROUND: i32 = 0;
2015 pub const GAME: i32 = 100;
2017 pub const FOREGROUND: i32 = 200;
2019 pub const UI: i32 = 300;
2021 pub const POST_PROCESS: i32 = 1000;
2024}
2025
2026#[derive(Debug, Clone, Component)]
2050pub struct Sprite {
2051 pub texture: Handle<Texture>,
2053 pub size: Option<Vec2>,
2056 pub color: Color,
2058 pub source_rect: Option<(Vec2, Vec2)>,
2060 pub origin: Option<Vec2>,
2063 pub layer: i32,
2065}
2066
2067impl Sprite {
2068 #[must_use]
2070 pub const fn new(texture: Handle<Texture>) -> Self {
2071 Self {
2072 texture,
2073 size: None,
2074 color: Color::WHITE,
2075 source_rect: None,
2076 origin: None,
2077 layer: layers::GAME,
2078 }
2079 }
2080
2081 #[must_use]
2083 pub const fn with_size(mut self, size: Vec2) -> Self {
2084 self.size = Some(size);
2085 self
2086 }
2087
2088 #[must_use]
2090 pub const fn with_color(mut self, color: Color) -> Self {
2091 self.color = color;
2092 self
2093 }
2094
2095 #[must_use]
2097 pub const fn with_layer(mut self, layer: i32) -> Self {
2098 self.layer = layer;
2099 self
2100 }
2101
2102 #[must_use]
2104 pub const fn with_source_rect(mut self, uv_min: Vec2, uv_max: Vec2) -> Self {
2105 self.source_rect = Some((uv_min, uv_max));
2106 self
2107 }
2108
2109 #[must_use]
2111 pub const fn with_origin(mut self, origin: Vec2) -> Self {
2112 self.origin = Some(origin);
2113 self
2114 }
2115}
2116
2117#[derive(Debug, Clone, Component)]
2136pub struct Text {
2137 pub content: Arc<str>,
2139 pub font: Handle<Font>,
2141 pub font_size: f32,
2143 pub color: Color,
2145 pub layer: i32,
2147}
2148
2149impl Text {
2150 #[must_use]
2152 pub fn new(content: impl Into<Arc<str>>, font: Handle<Font>) -> Self {
2153 Self {
2154 content: content.into(),
2155 font,
2156 font_size: 16.0,
2157 color: Color::WHITE,
2158 layer: layers::UI,
2159 }
2160 }
2161
2162 #[must_use]
2164 pub const fn with_size(mut self, font_size: f32) -> Self {
2165 self.font_size = font_size;
2166 self
2167 }
2168
2169 #[must_use]
2171 pub const fn with_color(mut self, color: Color) -> Self {
2172 self.color = color;
2173 self
2174 }
2175
2176 #[must_use]
2178 pub const fn with_layer(mut self, layer: i32) -> Self {
2179 self.layer = layer;
2180 self
2181 }
2182}
2183
2184impl RenderQueue {
2185 #[must_use]
2187 pub fn new() -> Self {
2188 Self {
2189 commands: Vec::with_capacity(1024),
2190 target: None,
2191 }
2192 }
2193
2194 pub fn clear(&mut self) {
2196 self.commands.clear();
2197 self.target = None;
2198 }
2199
2200 pub const fn set_target(&mut self, target: Option<u32>) {
2203 self.target = target;
2204 }
2205
2206 #[must_use]
2208 pub const fn target(&self) -> Option<u32> {
2209 self.target
2210 }
2211
2212 #[doc(hidden)]
2215 pub fn push(&mut self, command: DrawCommand) {
2216 self.commands.push(command);
2217 }
2218
2219 #[doc(hidden)]
2221 #[must_use]
2222 pub fn commands(&self) -> &[DrawCommand] {
2223 &self.commands
2224 }
2225
2226 pub fn draw_sprite(&mut self, texture: &Handle<Texture>, position: Vec2, size: Vec2) {
2228 self.draw_sprite_on_layer(texture, position, size, layers::GAME);
2229 }
2230
2231 pub fn draw_sprite_on_layer(
2233 &mut self,
2234 texture: &Handle<Texture>,
2235 position: Vec2,
2236 size: Vec2,
2237 layer: i32,
2238 ) {
2239 self.push(DrawCommand {
2240 kind: DrawKind::Sprite {
2241 texture: Some(u64::from(texture.id())),
2242 position,
2243 rotation: 0.0,
2244 scale: size,
2245 tint: Color::WHITE,
2246 layer,
2247 uv_rect: None,
2248 origin: Vec2::new(size.x * 0.5, size.y * 0.5),
2249 sort_key: None,
2250 },
2251 });
2252 }
2253
2254 pub fn draw_sprite_atlas(
2257 &mut self,
2258 texture: &Handle<Texture>,
2259 position: Vec2,
2260 size: Vec2,
2261 region: (Vec2, Vec2),
2262 ) {
2263 self.draw_sprite_atlas_on_layer(texture, position, size, region, layers::GAME);
2264 }
2265
2266 pub fn draw_sprite_atlas_on_layer(
2268 &mut self,
2269 texture: &Handle<Texture>,
2270 position: Vec2,
2271 size: Vec2,
2272 region: (Vec2, Vec2),
2273 layer: i32,
2274 ) {
2275 self.push(DrawCommand {
2276 kind: DrawKind::Sprite {
2277 texture: Some(u64::from(texture.id())),
2278 position,
2279 rotation: 0.0,
2280 scale: size,
2281 tint: Color::WHITE,
2282 layer,
2283 uv_rect: Some(region),
2284 origin: Vec2::new(size.x * 0.5, size.y * 0.5),
2285 sort_key: None,
2286 },
2287 });
2288 }
2289
2290 pub fn draw_sprite_transformed(&mut self, texture: &Handle<Texture>, params: SpriteParams) {
2292 self.draw_sprite_transformed_on_layer(texture, params, layers::GAME);
2293 }
2294
2295 pub fn draw_sprite_transformed_on_layer(
2297 &mut self,
2298 texture: &Handle<Texture>,
2299 params: SpriteParams,
2300 layer: i32,
2301 ) {
2302 self.push(DrawCommand {
2303 kind: DrawKind::Sprite {
2304 texture: Some(u64::from(texture.id())),
2305 position: params.position,
2306 rotation: params.rotation,
2307 scale: params.scale,
2308 tint: params.tint,
2309 layer,
2310 uv_rect: None,
2311 origin: params.origin,
2312 sort_key: None,
2313 },
2314 });
2315 }
2316
2317 pub fn draw_rect(&mut self, position: Vec2, size: Vec2, color: Color) {
2319 self.draw_rect_on_layer(position, size, color, layers::GAME);
2320 }
2321
2322 pub fn draw_rect_on_layer(&mut self, position: Vec2, size: Vec2, color: Color, layer: i32) {
2324 self.push(DrawCommand {
2325 kind: DrawKind::Rect {
2326 position,
2327 size,
2328 color,
2329 layer,
2330 },
2331 });
2332 }
2333
2334 pub fn draw_screen_rect(&mut self, position: Vec2, size: Vec2, color: Color) {
2338 self.draw_rect_on_layer(position, size, color, layers::POST_PROCESS);
2339 }
2340
2341 pub fn draw_line(&mut self, start: Vec2, end: Vec2, color: Color, thickness: f32) {
2344 self.draw_line_on_layer(start, end, color, thickness, layers::GAME);
2345 }
2346
2347 pub fn draw_line_on_layer(
2349 &mut self,
2350 start: Vec2,
2351 end: Vec2,
2352 color: Color,
2353 thickness: f32,
2354 layer: i32,
2355 ) {
2356 self.push(DrawCommand {
2357 kind: DrawKind::Line {
2358 start,
2359 end,
2360 color,
2361 thickness,
2362 layer,
2363 },
2364 });
2365 }
2366
2367 pub fn clear_color(&mut self, color: Color) {
2371 self.push(DrawCommand {
2372 kind: DrawKind::Rect {
2373 position: Vec2::ZERO,
2374 size: Vec2::new(10000.0, 10000.0),
2375 color,
2376 layer: layers::BACKGROUND,
2377 },
2378 });
2379 }
2380
2381 pub fn draw_text(
2383 &mut self,
2384 font: &Handle<Font>,
2385 content: &str,
2386 position: Vec2,
2387 font_size: f32,
2388 color: Color,
2389 ) {
2390 self.draw_text_on_layer(font, content, position, font_size, color, layers::GAME);
2391 }
2392
2393 pub fn draw_text_on_layer(
2395 &mut self,
2396 font: &Handle<Font>,
2397 content: &str,
2398 position: Vec2,
2399 font_size: f32,
2400 color: Color,
2401 layer: i32,
2402 ) {
2403 self.push(DrawCommand {
2404 kind: DrawKind::Text {
2405 font: Some(u64::from(font.id())),
2406 content: Arc::from(content),
2407 position,
2408 font_size,
2409 color,
2410 layer,
2411 wrap_width: None,
2412 line_height: 0.0,
2413 },
2414 });
2415 }
2416
2417 #[allow(clippy::too_many_arguments)]
2421 pub fn draw_ui_text(
2422 &mut self,
2423 font: &Handle<Font>,
2424 text: &str,
2425 screen_pos: Vec2,
2426 font_size: f32,
2427 color: Color,
2428 camera: &Camera,
2429 window_width: u32,
2430 window_height: u32,
2431 ) {
2432 let world = camera.screen_to_world(screen_pos, window_width, window_height);
2433 self.draw_text_on_layer(font, text, world, font_size, color, layers::UI);
2434 }
2435
2436 pub fn draw_ui_rect(
2439 &mut self,
2440 screen_pos: Vec2,
2441 size: Vec2,
2442 color: Color,
2443 camera: &Camera,
2444 window_width: u32,
2445 window_height: u32,
2446 ) {
2447 let world = camera.screen_to_world(screen_pos, window_width, window_height);
2448 self.draw_rect_on_layer(world, size, color, layers::UI);
2449 }
2450
2451 #[allow(clippy::too_many_arguments)]
2455 pub fn draw_text_wrapped(
2456 &mut self,
2457 font: &Handle<Font>,
2458 content: &str,
2459 position: Vec2,
2460 font_size: f32,
2461 color: Color,
2462 max_width: f32,
2463 line_height: f32,
2464 layer: i32,
2465 ) {
2466 self.push(DrawCommand {
2467 kind: DrawKind::Text {
2468 font: Some(u64::from(font.id())),
2469 content: Arc::from(content),
2470 position,
2471 font_size,
2472 color,
2473 layer,
2474 wrap_width: Some(max_width),
2475 line_height,
2476 },
2477 });
2478 }
2479
2480 #[allow(clippy::too_many_arguments)]
2483 pub fn draw_ui_sprite(
2484 &mut self,
2485 texture: &Handle<Texture>,
2486 screen_pos: Vec2,
2487 size: Vec2,
2488 camera: &Camera,
2489 window_width: u32,
2490 window_height: u32,
2491 ) {
2492 let world = camera.screen_to_world(screen_pos, window_width, window_height);
2493 self.draw_sprite_on_layer(texture, world, size, layers::UI);
2494 }
2495
2496 pub fn draw_immediate(&mut self, f: impl FnOnce(&mut DrawContext<'_>)) {
2512 let mut ctx = DrawContext { queue: self };
2513 f(&mut ctx);
2514 }
2515}
2516
2517pub struct DrawContext<'a> {
2522 queue: &'a mut RenderQueue,
2523}
2524
2525impl DrawContext<'_> {
2526 pub fn line(&mut self, start: Vec2, end: Vec2, color: Color, thickness: f32) {
2528 self.queue.draw_line(start, end, color, thickness);
2529 }
2530
2531 pub fn rect(&mut self, position: Vec2, size: Vec2, color: Color) {
2533 self.queue.draw_rect(position, size, color);
2534 }
2535
2536 pub fn rect_stroke(&mut self, position: Vec2, size: Vec2, color: Color, thickness: f32) {
2538 let Vec2 { x, y } = position;
2539 let Vec2 { x: w, y: h } = size;
2540 self.line(Vec2::new(x, y), Vec2::new(x + w, y), color, thickness);
2542 self.line(
2544 Vec2::new(x, y + h),
2545 Vec2::new(x + w, y + h),
2546 color,
2547 thickness,
2548 );
2549 self.line(Vec2::new(x, y), Vec2::new(x, y + h), color, thickness);
2551 self.line(
2553 Vec2::new(x + w, y),
2554 Vec2::new(x + w, y + h),
2555 color,
2556 thickness,
2557 );
2558 }
2559
2560 pub fn circle(&mut self, center: Vec2, radius: f32, color: Color, thickness: f32) {
2562 let segments = 32;
2563 #[allow(clippy::cast_precision_loss)]
2564 for i in 0..segments {
2565 let a1 = (i as f32 / segments as f32) * 2.0 * std::f32::consts::PI;
2566 let a2 = ((i + 1) as f32 / segments as f32) * 2.0 * std::f32::consts::PI;
2567 let x1 = center.x + a1.cos() * radius;
2568 let y1 = center.y + a1.sin() * radius;
2569 let x2 = center.x + a2.cos() * radius;
2570 let y2 = center.y + a2.sin() * radius;
2571 self.line(Vec2::new(x1, y1), Vec2::new(x2, y2), color, thickness);
2572 }
2573 }
2574
2575 pub fn circle_filled(&mut self, center: Vec2, radius: f32, color: Color) {
2577 let r = radius.ceil() as i32;
2578 #[allow(clippy::cast_precision_loss)]
2579 for dy in -r..=r {
2580 let dy_f = dy as f32 + 0.5; let half_w = (radius * radius - dy_f * dy_f).sqrt();
2582 if half_w <= 0.0 {
2583 continue;
2584 }
2585 self.queue.push(DrawCommand {
2586 kind: DrawKind::Rect {
2587 position: Vec2::new(center.x - half_w, center.y + dy as f32),
2588 size: Vec2::new(half_w * 2.0, 1.0),
2589 color,
2590 layer: layers::FOREGROUND,
2591 },
2592 });
2593 }
2594 }
2595
2596 pub fn text(&mut self, content: &str, position: Vec2, font_size: f32, color: Color) {
2598 self.queue.push(DrawCommand {
2600 kind: DrawKind::Text {
2601 font: Some(0),
2602 content: Arc::from(content),
2603 position,
2604 font_size,
2605 color,
2606 layer: layers::FOREGROUND,
2607 wrap_width: None,
2608 line_height: 0.0,
2609 },
2610 });
2611 }
2612
2613 pub fn point(&mut self, position: Vec2, color: Color) {
2615 self.circle(position, 3.0, color, 1.0);
2616 }
2617
2618 pub fn aabb(&mut self, min: Vec2, max: Vec2, color: Color, thickness: f32) {
2620 self.rect_stroke(min, max - min, color, thickness);
2621 }
2622}
2623
2624pub trait RenderPass: Send + Sync + 'static {
2629 fn name(&self) -> &str;
2631
2632 fn execute(
2636 &self,
2637 _device: &wgpu::Device,
2638 _queue: &wgpu::Queue,
2639 _pass: &mut wgpu::RenderPass<'_>,
2640 ) {
2641 }
2642}
2643
2644impl Default for RenderQueue {
2645 fn default() -> Self {
2646 Self::new()
2647 }
2648}
2649
2650pub trait PostEffect: Send + Sync + 'static {
2674 fn apply(&self, queue: &mut RenderQueue, window_w: f32, window_h: f32);
2677}
2678
2679#[derive(Resource, Default)]
2692pub struct PostProcessStack {
2693 effects: Vec<Box<dyn PostEffect>>,
2694}
2695
2696impl PostProcessStack {
2697 pub fn push(&mut self, effect: impl PostEffect + 'static) {
2699 self.effects.push(Box::new(effect));
2700 }
2701
2702 pub fn clear(&mut self) {
2704 self.effects.clear();
2705 }
2706
2707 #[must_use]
2709 pub fn is_empty(&self) -> bool {
2710 self.effects.is_empty()
2711 }
2712}
2713
2714#[derive(Resource, Clone, Copy)]
2732pub struct ScreenFlash {
2733 pub color: Color,
2734 pub intensity: f32,
2736 pub decay: f32,
2738}
2739
2740#[derive(Resource, Clone, Copy)]
2751pub struct ColorTint {
2752 pub color: Color,
2753 pub intensity: f32,
2755}
2756
2757fn apply_post_process_system(
2758 mut queue: ResMut<RenderQueue>,
2759 flash: Option<Res<ScreenFlash>>,
2760 tint: Option<Res<ColorTint>>,
2761 stack: Res<PostProcessStack>,
2762 info: Res<RenderInfo>,
2763) {
2764 let w = info.window_width as f32;
2765 let h = info.window_height as f32;
2766 if w == 0.0 || h == 0.0 {
2767 return;
2768 }
2769 let size = Vec2::new(w, h);
2770 for effect in &stack.effects {
2771 effect.apply(&mut queue, w, h);
2772 }
2773 if let Some(tint) = tint
2774 && tint.intensity > 0.0
2775 {
2776 queue.draw_screen_rect(
2777 Vec2::ZERO,
2778 size,
2779 Color::rgba(tint.color.r, tint.color.g, tint.color.b, tint.intensity),
2780 );
2781 }
2782 if let Some(flash) = flash
2783 && flash.intensity > 0.0
2784 {
2785 queue.draw_screen_rect(
2786 Vec2::ZERO,
2787 size,
2788 Color::rgba(flash.color.r, flash.color.g, flash.color.b, flash.intensity),
2789 );
2790 }
2791}
2792
2793fn decay_screen_flash_system(
2794 mut commands: Commands,
2795 flash: Option<ResMut<ScreenFlash>>,
2796 time: Res<lunar_core::Time>,
2797) {
2798 if let Some(mut flash) = flash
2799 && flash.decay > 0.0
2800 {
2801 flash.intensity -= flash.decay * time.delta_seconds();
2802 if flash.intensity <= 0.0 {
2803 commands.remove_resource::<ScreenFlash>();
2804 }
2805 }
2806}
2807
2808#[derive(Resource)]
2813pub struct RenderInfo {
2814 pub window_width: u32,
2816 pub window_height: u32,
2818 pub fps: f32,
2820 pub frame_time_ms: f32,
2822 pub draw_calls: u32,
2824 pub sprite_count: u32,
2826}
2827
2828impl RenderInfo {
2829 #[must_use]
2831 pub const fn new() -> Self {
2832 Self {
2833 window_width: 0,
2834 window_height: 0,
2835 fps: 0.0,
2836 frame_time_ms: 0.0,
2837 draw_calls: 0,
2838 sprite_count: 0,
2839 }
2840 }
2841}
2842
2843impl Default for RenderInfo {
2844 fn default() -> Self {
2845 Self::new()
2846 }
2847}
2848
2849#[derive(Resource)]
2854pub struct DebugOverlay {
2855 pub enabled: bool,
2857 pub position: Vec2,
2859 pub font_size: f32,
2861 pub color: Color,
2863 scratch: String,
2865}
2866
2867impl DebugOverlay {
2868 #[must_use]
2870 pub fn new() -> Self {
2871 Self {
2872 enabled: false,
2873 position: Vec2::new(10.0, 10.0),
2874 font_size: 14.0,
2875 color: Color::WHITE,
2876 scratch: String::new(),
2877 }
2878 }
2879
2880 pub fn draw(
2883 &mut self,
2884 queue: &mut RenderQueue,
2885 fps: f32,
2886 frame_time_ms: f32,
2887 sprite_count: u32,
2888 entity_count: u32,
2889 ) {
2890 if !self.enabled {
2891 return;
2892 }
2893 use std::fmt::Write;
2894 let x = self.position.x;
2895 let y = self.position.y;
2896 let fs = self.font_size;
2897 let color = self.color;
2898 let spacing = fs + 2.0;
2899
2900 self.scratch.clear();
2903 let _ = write!(self.scratch, "FPS: {fps:.1}");
2904 push_debug_text(queue, &self.scratch, Vec2::new(x, y), fs, color);
2905
2906 self.scratch.clear();
2907 let _ = write!(self.scratch, "Frame: {frame_time_ms:.1}ms");
2908 push_debug_text(queue, &self.scratch, Vec2::new(x, y + spacing), fs, color);
2909
2910 self.scratch.clear();
2911 let _ = write!(self.scratch, "Sprites: {sprite_count}");
2912 push_debug_text(
2913 queue,
2914 &self.scratch,
2915 Vec2::new(x, y + spacing * 2.0),
2916 fs,
2917 color,
2918 );
2919
2920 self.scratch.clear();
2921 let _ = write!(self.scratch, "Entities: {entity_count}");
2922 push_debug_text(
2923 queue,
2924 &self.scratch,
2925 Vec2::new(x, y + spacing * 3.0),
2926 fs,
2927 color,
2928 );
2929 }
2930}
2931
2932fn push_debug_text(
2933 queue: &mut RenderQueue,
2934 content: &str,
2935 position: Vec2,
2936 font_size: f32,
2937 color: Color,
2938) {
2939 queue.push(DrawCommand {
2940 kind: DrawKind::Text {
2941 font: Some(0),
2942 content: Arc::from(content),
2943 position,
2944 font_size,
2945 color,
2946 layer: layers::FOREGROUND,
2947 wrap_width: None,
2948 line_height: 0.0,
2949 },
2950 });
2951}
2952
2953impl Default for DebugOverlay {
2954 fn default() -> Self {
2955 Self::new()
2956 }
2957}
2958
2959pub struct RenderPlugin;
2964
2965impl Default for RenderPlugin {
2966 fn default() -> Self {
2967 Self
2968 }
2969}
2970
2971impl GamePlugin for RenderPlugin {
2972 fn name(&self) -> &'static str {
2973 "RenderPlugin"
2974 }
2975
2976 fn build(&mut self, app: &mut App) {
2977 app.insert_resource(RenderQueue::new());
2978 app.insert_resource(RenderInfo::new());
2979 app.insert_resource(DebugOverlay::new());
2980 app.insert_resource(PostProcessStack::default());
2981 app.add_system_to_stage(
2982 lunar_core::UpdateStage::PostUpdate,
2983 decay_screen_flash_system,
2984 );
2985 app.add_system_to_stage(
2986 lunar_core::UpdateStage::PostUpdate,
2987 apply_post_process_system,
2988 );
2989 app.add_system_to_stage(
2990 lunar_core::UpdateStage::PostUpdate,
2991 (
2992 camera_follow::camera_follow_system,
2993 screen_shake::screen_shake_system,
2994 )
2995 .chain(),
2996 );
2997 #[cfg(not(target_arch = "wasm32"))]
3000 app.add_system_to_stage(
3001 lunar_core::UpdateStage::Render,
3002 (
3003 upload_new_textures_system,
3004 upload_new_fonts_system,
3005 evict_textures_system,
3006 frame_stats_system,
3007 auto_sprite_system,
3008 auto_text_system,
3009 debug_overlay_system,
3010 render_system,
3011 )
3012 .chain(),
3013 );
3014 #[cfg(target_arch = "wasm32")]
3015 app.add_system_to_stage(
3016 lunar_core::UpdateStage::Render,
3017 (
3018 wasm_upload_new_textures_system,
3019 wasm_upload_new_fonts_system,
3020 wasm_evict_textures_system,
3021 frame_stats_system,
3022 auto_sprite_system,
3023 auto_text_system,
3024 debug_overlay_system,
3025 wasm_render_system,
3026 )
3027 .chain(),
3028 );
3029 }
3030}
3031
3032#[cfg(target_arch = "wasm32")]
3036thread_local! {
3037 static WASM_RENDER_ENGINE: std::cell::RefCell<Option<RenderEngine>> =
3038 std::cell::RefCell::new(None);
3039}
3040
3041#[cfg(target_arch = "wasm32")]
3044pub fn wasm_set_render_engine(engine: RenderEngine) {
3045 WASM_RENDER_ENGINE.with(|cell| {
3046 *cell.borrow_mut() = Some(engine);
3047 });
3048}
3049
3050#[cfg(target_arch = "wasm32")]
3053#[allow(clippy::needless_pass_by_value)]
3054fn wasm_render_system(
3055 mut queue: ResMut<RenderQueue>,
3056 mut render_info: ResMut<RenderInfo>,
3057 camera: Option<Res<Camera>>,
3058) {
3059 WASM_RENDER_ENGINE.with(|cell| {
3060 if let Some(engine) = cell.borrow_mut().as_mut() {
3061 engine.render(queue.commands(), camera.as_deref(), &mut render_info);
3062 }
3063 });
3064 queue.clear();
3065}
3066
3067#[cfg(not(target_arch = "wasm32"))]
3073#[allow(clippy::needless_pass_by_value)]
3074fn upload_new_textures_system(mut assets: ResMut<AssetServer>, mut render: ResMut<RenderEngine>) {
3075 for id in assets.drain_new_texture_ids() {
3076 if let Some(texture) = assets.get_texture_by_id(id) {
3077 let handle = Handle::<Texture>::new(id, 0);
3078 render.upload_texture(&handle, texture);
3079 }
3080 }
3081}
3082
3083#[cfg(target_arch = "wasm32")]
3085#[allow(clippy::needless_pass_by_value)]
3086fn wasm_upload_new_textures_system(mut assets: ResMut<AssetServer>) {
3087 let ids = assets.drain_new_texture_ids();
3088 WASM_RENDER_ENGINE.with(|cell| {
3089 if let Some(engine) = cell.borrow_mut().as_mut() {
3090 for id in ids {
3091 if let Some(texture) = assets.get_texture_by_id(id) {
3092 let handle = Handle::<Texture>::new(id, 0);
3093 engine.upload_texture(&handle, texture);
3094 }
3095 }
3096 }
3097 });
3098}
3099
3100#[cfg(not(target_arch = "wasm32"))]
3101#[allow(clippy::needless_pass_by_value)]
3102fn upload_new_fonts_system(mut assets: ResMut<AssetServer>, mut render: ResMut<RenderEngine>) {
3103 for id in assets.drain_new_font_ids() {
3104 if let Some(font) = assets.get_font_by_id(id) {
3105 render.upload_font(id, &font.data);
3106 }
3107 }
3108}
3109
3110#[cfg(target_arch = "wasm32")]
3111#[allow(clippy::needless_pass_by_value)]
3112fn wasm_upload_new_fonts_system(mut assets: ResMut<AssetServer>) {
3113 let ids = assets.drain_new_font_ids();
3114 WASM_RENDER_ENGINE.with(|cell| {
3115 if let Some(engine) = cell.borrow_mut().as_mut() {
3116 for id in &ids {
3117 if let Some(font) = assets.get_font_by_id(*id) {
3118 engine.upload_font(*id, &font.data);
3119 }
3120 }
3121 }
3122 });
3123}
3124
3125#[cfg(not(target_arch = "wasm32"))]
3127#[allow(clippy::needless_pass_by_value)]
3128fn evict_textures_system(mut assets: ResMut<AssetServer>, mut render: ResMut<RenderEngine>) {
3129 for id in assets.drain_evicted_texture_ids() {
3130 render.remove_texture(id);
3131 }
3132}
3133
3134#[cfg(target_arch = "wasm32")]
3136#[allow(clippy::needless_pass_by_value)]
3137fn wasm_evict_textures_system(mut assets: ResMut<AssetServer>) {
3138 let ids = assets.drain_evicted_texture_ids();
3139 WASM_RENDER_ENGINE.with(|cell| {
3140 if let Some(engine) = cell.borrow_mut().as_mut() {
3141 for id in ids {
3142 engine.remove_texture(id);
3143 }
3144 }
3145 });
3146}
3147
3148#[allow(clippy::needless_pass_by_value)]
3152fn frame_stats_system(time: Res<Time>, mut info: ResMut<RenderInfo>) {
3153 let raw_delta = time.raw_delta_seconds();
3154 info.frame_time_ms = raw_delta * 1000.0;
3155 info.fps = if raw_delta > 0.0 {
3156 1.0 / raw_delta
3157 } else {
3158 0.0
3159 };
3160}
3161
3162#[derive(Component)]
3168pub struct YSort;
3169
3170#[allow(clippy::needless_pass_by_value)]
3174fn auto_sprite_system(
3175 assets: Option<Res<AssetServer>>,
3176 mut queue: ResMut<RenderQueue>,
3177 query: Query<(&Transform, &Sprite, Option<&YSort>)>,
3178) {
3179 for (transform, sprite, y_sort) in &query {
3180 let resolved_size = sprite.size.unwrap_or_else(|| {
3181 assets
3182 .as_deref()
3183 .and_then(|server| server.get_texture(&sprite.texture))
3184 .map_or(Vec2::splat(32.0), |texture| {
3185 Vec2::new(texture.width as f32, texture.height as f32)
3186 })
3187 });
3188 let final_size = resolved_size * transform.scale;
3189 let origin = sprite
3190 .origin
3191 .map_or_else(|| final_size * 0.5, |o| o * transform.scale);
3192 let sort_key = y_sort.map(|_| (transform.translation.y * 100.0) as i32);
3194 queue.push(DrawCommand {
3195 kind: DrawKind::Sprite {
3196 texture: Some(u64::from(sprite.texture.id())),
3197 position: transform.translation,
3198 rotation: transform.rotation,
3199 scale: final_size,
3200 tint: sprite.color,
3201 layer: sprite.layer,
3202 uv_rect: sprite.source_rect,
3203 origin,
3204 sort_key,
3205 },
3206 });
3207 }
3208}
3209
3210#[allow(clippy::needless_pass_by_value)]
3213fn auto_text_system(mut queue: ResMut<RenderQueue>, query: Query<(&Transform, &Text)>) {
3214 for (transform, text) in &query {
3215 queue.push(DrawCommand {
3216 kind: DrawKind::Text {
3217 font: Some(u64::from(text.font.id())),
3218 content: Arc::clone(&text.content),
3219 position: transform.translation,
3220 font_size: text.font_size,
3221 color: text.color,
3222 layer: text.layer,
3223 wrap_width: None,
3224 line_height: 0.0,
3225 },
3226 });
3227 }
3228}
3229
3230#[cfg(not(target_arch = "wasm32"))]
3235fn render_system(
3236 mut render_engine: ResMut<RenderEngine>,
3237 mut queue: ResMut<RenderQueue>,
3238 mut render_info: ResMut<RenderInfo>,
3239 camera: Option<Res<Camera>>,
3240) {
3241 render_engine.render(queue.commands(), camera.as_deref(), &mut render_info);
3242 queue.clear();
3243}
3244
3245#[allow(clippy::needless_pass_by_value)]
3247fn debug_overlay_system(
3248 mut overlay: ResMut<DebugOverlay>,
3249 info: Res<RenderInfo>,
3250 mut queue: ResMut<RenderQueue>,
3251 entities: Query<Entity>,
3252) {
3253 #[allow(clippy::cast_possible_truncation)]
3254 let entity_count = entities.iter().count() as u32;
3255 overlay.draw(
3256 &mut queue,
3257 info.fps,
3258 info.frame_time_ms,
3259 info.sprite_count,
3260 entity_count,
3261 );
3262}