1#![doc = include_str!("../README.md")]
2#![warn(unused_extern_crates)]
3
4mod fragment_only;
5mod game;
6mod limits;
7
8#[cfg(target_arch = "wasm32")]
9mod wasm;
10
11use std::sync::atomic::AtomicU32;
12use std::sync::atomic::Ordering;
13
14pub use fragment_only::FragmentOnlyRenderBundleEncoder;
15pub use fragment_only::FragmentOnlyRenderBundleEncoderDescriptor;
16pub use fragment_only::FragmentOnlyRenderPass;
17pub use fragment_only::FragmentOnlyRenderPassDescriptor;
18pub use fragment_only::FragmentOnlyRenderPipeline;
19pub use fragment_only::FragmentOnlyRenderPipelineDescriptor;
20pub use game::window::GameWindow;
21pub use game::window::WindowSizeDependent;
22pub use game::ExitFlag;
23pub use game::Game;
24pub use game::GameCommand;
25pub use game::GameData;
26pub use game::InputMode;
27pub mod input {
28 pub use crate::game::input::*;
29}
30pub mod local_storage;
31
32static FAST_HASH_SEED: AtomicU32 = AtomicU32::new(0);
34pub struct FastHashState {
35 seed: u32,
36}
37impl std::hash::BuildHasher for FastHashState {
38 type Hasher = rustc_hash::FxHasher;
39
40 fn build_hasher(&self) -> Self::Hasher {
41 use std::hash::Hasher;
42
43 let mut hasher = rustc_hash::FxHasher::default();
44 hasher.write_u32(self.seed);
45 return hasher;
46 }
47}
48impl Default for FastHashState {
49 fn default() -> Self {
50 Self {
51 seed: FAST_HASH_SEED.fetch_add(1, Ordering::SeqCst),
52 }
53 }
54}
55
56pub type FastHashSet<T> = std::collections::HashSet<T, FastHashState>;
58pub type FastHashMap<K, V> = std::collections::HashMap<K, V, FastHashState>;
60
61use wgpu::util::DeviceExt;
62
63fn next_multiple_of(
64 value: wgpu::BufferAddress,
65 multiple: wgpu::BufferAddress,
66) -> wgpu::BufferAddress {
67 match value % multiple {
68 0 => value,
69 r => value + (multiple - r),
70 }
71}
72
73pub fn block_on(future: impl std::future::Future<Output = ()> + 'static) {
75 #[cfg(target_arch = "wasm32")]
76 wasm_bindgen_futures::spawn_local(future);
77
78 #[cfg(not(target_arch = "wasm32"))]
79 pollster::block_on(future);
80}
81
82pub fn is_html_tag_supported(tag: &str) -> bool {
85 #[cfg(not(target_arch = "wasm32"))]
86 {
87 let _ = tag;
88 return true;
89 }
90
91 #[cfg(target_arch = "wasm32")]
92 {
93 let window = match web_sys::window() {
94 None => return false,
95 Some(window) => window,
96 };
97 let document = match window.document() {
98 None => return false,
99 Some(document) => document,
100 };
101 return document.create_element(tag).is_ok();
102 }
103}
104
105#[cfg(target_arch = "wasm32")]
106mod pretty_alert {
107 pub(super) struct PrettyAlertFailure;
108
109 impl From<wasm_bindgen::JsValue> for PrettyAlertFailure {
110 fn from(_: wasm_bindgen::JsValue) -> Self {
111 Self
112 }
113 }
114 impl From<()> for PrettyAlertFailure {
115 fn from(_: ()) -> Self {
116 Self
117 }
118 }
119
120 pub(super) fn try_pretty_alert(msg: &str) -> Result<(), PrettyAlertFailure> {
121 use wasm_bindgen::JsCast;
122
123 let document = web_sys::window()
124 .expect("app requires window")
125 .document()
126 .expect("app requires DOM");
127 let body = document.body().expect("DOM has body");
128
129 let dialog = document.create_element("dialog")?;
130 dialog.set_id("lf-alert");
131 dialog.set_attribute("style", r#"font-family: mono; max-width: 50%;"#)?;
132 {
133 let text_div = document.create_element("div")?;
134 text_div.set_text_content(Some(msg));
135 dialog.append_child(&text_div)?;
136
137 let button_div = document.create_element("div")?;
138 button_div.set_attribute("style", r#"display: grid; margin-top: 10px;"#)?;
139 {
140 let button = document.create_element("button")?;
141 button.set_attribute(
142 "style",
143 r#"border: 0px; font-family: sans; font-size: 20px; padding: 10px;"#,
144 )?;
145 button.set_attribute("autofocus", "true")?;
146 button.set_attribute("onclick", "document.getElementById('lf-alert').remove();")?;
147 button.set_text_content(Some("Okay"));
148 button_div.append_child(&button)?;
149 }
150 dialog.append_child(&button_div)?;
151 }
152 body.append_child(&dialog)?;
153
154 dialog
155 .dyn_ref::<web_sys::HtmlDialogElement>()
156 .ok_or(())?
157 .show_modal()?;
158
159 Ok(())
160 }
161}
162
163pub fn alert_dialogue(msg: &str) {
165 #[cfg(target_arch = "wasm32")]
166 {
167 let res = pretty_alert::try_pretty_alert(msg);
168 if res.is_err() {
169 web_sys::window()
170 .expect("app requires window")
171 .alert_with_message(msg)
172 .expect("all browsers should have alert");
173 }
174 }
175 #[cfg(not(target_arch = "wasm32"))]
176 {
177 use dialog::DialogBox;
178 dialog::Message::new(msg)
179 .title("Alert")
180 .show()
181 .expect("dialog box unavailable")
182 }
183}
184
185pub const BUFFER_PADDING: wgpu::BufferAddress = 256;
190
191mod sealed {
194 pub trait SealedDevice {}
195 impl SealedDevice for wgpu::Device {}
196
197 pub trait SealedInstance {}
198 impl SealedInstance for wgpu::Instance {}
199
200 pub trait SealedCommandEncoder {}
201 impl SealedCommandEncoder for wgpu::CommandEncoder {}
202
203 pub trait SealedLimits {}
204 impl SealedLimits for wgpu::Limits {}
205
206 pub trait SealedBuffer {}
207 impl SealedBuffer for wgpu::Buffer {}
208
209 pub trait SealedQueue {}
210 impl SealedQueue for wgpu::Queue {}
211
212 pub trait SealedBindGroupLayoutEntry {}
213 impl SealedBindGroupLayoutEntry for wgpu::BindGroupLayoutEntry {}
214
215 pub trait SealedGame {}
217 impl<T: crate::Game> SealedGame for T {}
218}
219
220pub struct PaddedBufferInitDescriptor<'a> {
221 pub label: wgpu::Label<'a>,
223 pub contents: Vec<u8>,
225 pub usage: wgpu::BufferUsages,
228}
229
230pub trait LfDeviceExt: sealed::SealedDevice {
232 fn create_buffer_padded(&self, desc: wgpu::BufferDescriptor) -> wgpu::Buffer;
233 fn create_buffer_init_padded(&self, desc: PaddedBufferInitDescriptor) -> wgpu::Buffer;
234
235 fn create_fragment_only_render_bundle_encoder(
236 &self,
237 desc: &FragmentOnlyRenderBundleEncoderDescriptor,
238 ) -> FragmentOnlyRenderBundleEncoder;
239
240 fn create_fragment_only_render_pipeline(
241 &self,
242 desc: &FragmentOnlyRenderPipelineDescriptor,
243 ) -> FragmentOnlyRenderPipeline;
244
245 unsafe fn create_shader_module_unchecked_on_release(
249 &self,
250 desc: wgpu::ShaderModuleDescriptor,
251 ) -> wgpu::ShaderModule;
252
253 fn assert_pop_error_scope(&self, msg: impl Into<String>);
255}
256
257impl LfDeviceExt for wgpu::Device {
258 fn create_buffer_padded(&self, mut desc: wgpu::BufferDescriptor) -> wgpu::Buffer {
259 desc.size = next_multiple_of(desc.size, BUFFER_PADDING);
260
261 self.create_buffer(&desc)
262 }
263
264 fn create_buffer_init_padded(&self, mut desc: PaddedBufferInitDescriptor) -> wgpu::Buffer {
265 let new_len = next_multiple_of(desc.contents.len() as wgpu::BufferAddress, BUFFER_PADDING);
266 desc.contents.resize(new_len as usize, 0u8);
267
268 self.create_buffer_init(&wgpu::util::BufferInitDescriptor {
269 label: desc.label,
270 contents: &desc.contents,
271 usage: desc.usage,
272 })
273 }
274
275 fn create_fragment_only_render_bundle_encoder(
276 &self,
277 desc: &FragmentOnlyRenderBundleEncoderDescriptor,
278 ) -> FragmentOnlyRenderBundleEncoder {
279 FragmentOnlyRenderBundleEncoder::new(self, desc)
280 }
281
282 fn create_fragment_only_render_pipeline(
283 &self,
284 desc: &FragmentOnlyRenderPipelineDescriptor,
285 ) -> FragmentOnlyRenderPipeline {
286 FragmentOnlyRenderPipeline::new(self, desc)
287 }
288
289 unsafe fn create_shader_module_unchecked_on_release(
290 &self,
291 desc: wgpu::ShaderModuleDescriptor,
292 ) -> wgpu::ShaderModule {
293 #[cfg(any(target_arch = "wasm32", debug_assertions))]
294 {
295 self.create_shader_module(desc)
296 }
297 #[cfg(not(any(target_arch = "wasm32", debug_assertions)))]
298 {
299 self.create_shader_module_unchecked(desc)
300 }
301 }
302
303 fn assert_pop_error_scope(&self, msg: impl Into<String>) {
304 let f = self.pop_error_scope();
305 async fn check_device_scope(
306 f: impl std::future::Future<Output = Option<wgpu::Error>>,
307 msg: String,
308 ) {
309 let res = f.await;
310 if let Some(e) = res {
311 panic!("error scope failure - {}: {:?}", msg, e)
312 }
313 }
314 block_on(check_device_scope(f, msg.into()));
315 }
316}
317
318pub trait LfCommandEncoderExt: sealed::SealedCommandEncoder {
320 fn begin_fragment_only_render_pass<'pass>(
321 &'pass mut self,
322 desc: &FragmentOnlyRenderPassDescriptor<'pass, '_>,
323 ) -> FragmentOnlyRenderPass<'pass>;
324}
325
326impl LfCommandEncoderExt for wgpu::CommandEncoder {
327 fn begin_fragment_only_render_pass<'pass>(
328 &'pass mut self,
329 desc: &FragmentOnlyRenderPassDescriptor<'pass, '_>,
330 ) -> FragmentOnlyRenderPass<'pass> {
331 FragmentOnlyRenderPass::new(self, desc)
332 }
333}
334
335pub trait LfLimitsExt: sealed::SealedLimits {
337 fn intersection<'a>(&self, other: &wgpu::Limits) -> wgpu::Limits;
339 fn union<'a>(&self, other: &wgpu::Limits) -> wgpu::Limits;
341}
342
343impl LfLimitsExt for wgpu::Limits {
344 fn intersection<'a>(&self, other: &wgpu::Limits) -> wgpu::Limits {
346 crate::limits::limits_intersection(self, other)
347 }
348 fn union<'a>(&self, other: &wgpu::Limits) -> wgpu::Limits {
350 crate::limits::limits_union(self, other)
351 }
352}
353
354pub trait LfQueueExt: sealed::SealedQueue {
356 fn write_buffer_padded(
363 &self,
364 buffer: &wgpu::Buffer,
365 offset: wgpu::BufferAddress,
366 data: Vec<u8>,
367 );
368}
369
370impl LfQueueExt for wgpu::Queue {
371 fn write_buffer_padded(
372 &self,
373 buffer: &wgpu::Buffer,
374 offset: wgpu::BufferAddress,
375 mut data: Vec<u8>,
376 ) {
377 const PAD_ALIGNMENT: usize = BUFFER_PADDING as usize;
378 let len = data.len();
379 let target_size = match len % PAD_ALIGNMENT {
380 0 => len,
381 r => len + (PAD_ALIGNMENT - r),
382 };
383 data.resize(target_size, 0u8);
384
385 self.write_buffer(buffer, offset, &data)
386 }
387}
388
389pub trait LfBufferExt: sealed::SealedBuffer {
391 fn debug_read_blocking(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> Vec<u8>;
396}
397
398impl LfBufferExt for wgpu::Buffer {
399 fn debug_read_blocking(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> Vec<u8> {
400 assert!(self.usage().contains(wgpu::BufferUsages::COPY_SRC));
401
402 let staging = device.create_buffer(&wgpu::BufferDescriptor {
403 label: Some("debug-read-staging"),
404 size: self.size(),
405 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
406 mapped_at_creation: false,
407 });
408
409 let mut cmd = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
410 label: Some("debug-read-cmd-encoder"),
411 });
412 cmd.copy_buffer_to_buffer(self, 0, &staging, 0, self.size());
413
414 queue.submit(vec![cmd.finish()]);
415
416 let (sender, receiver) = std::sync::mpsc::channel();
417 staging.slice(..).map_async(wgpu::MapMode::Read, move |e| {
418 sender.send(e).expect("failed to send result of map");
419 });
420
421 device.poll(wgpu::Maintain::Wait);
422
423 receiver
424 .recv()
425 .expect("failed to get result of map")
426 .expect("failed to read buffer");
427
428 let slice = staging.slice(..).get_mapped_range();
429 slice.to_vec()
430 }
431}
432
433pub trait LfBindGroupLayoutEntryExt: sealed::SealedBindGroupLayoutEntry {
435 fn read_only_compute_storage(binding: u32) -> Self;
437 fn mutable_compute_storage(binding: u32) -> Self;
438}
439
440impl LfBindGroupLayoutEntryExt for wgpu::BindGroupLayoutEntry {
441 fn read_only_compute_storage(binding: u32) -> Self {
442 wgpu::BindGroupLayoutEntry {
443 binding,
444 visibility: wgpu::ShaderStages::COMPUTE,
445 ty: wgpu::BindingType::Buffer {
446 ty: wgpu::BufferBindingType::Storage { read_only: true },
447 has_dynamic_offset: false,
448 min_binding_size: None,
449 },
450 count: None,
451 }
452 }
453
454 fn mutable_compute_storage(binding: u32) -> Self {
455 wgpu::BindGroupLayoutEntry {
456 binding,
457 visibility: wgpu::ShaderStages::COMPUTE,
458 ty: wgpu::BindingType::Buffer {
459 ty: wgpu::BufferBindingType::Storage { read_only: false },
460 has_dynamic_offset: false,
461 min_binding_size: None,
462 },
463 count: None,
464 }
465 }
466}
467
468pub trait LfGameExt: sealed::SealedGame {
470 type InitData;
471
472 fn run(init: Self::InitData);
474}
475
476impl<T: Game + 'static> LfGameExt for T {
477 type InitData = T::InitData;
478
479 fn run(init: T::InitData) {
481 game::GameState::<T>::run(init);
482 }
483}