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 SealedCommandEncoder {}
198 impl SealedCommandEncoder for wgpu::CommandEncoder {}
199
200 pub trait SealedLimits {}
201 impl SealedLimits for wgpu::Limits {}
202
203 pub trait SealedBuffer {}
204 impl SealedBuffer for wgpu::Buffer {}
205
206 pub trait SealedQueue {}
207 impl SealedQueue for wgpu::Queue {}
208
209 pub trait SealedBindGroupLayoutEntry {}
210 impl SealedBindGroupLayoutEntry for wgpu::BindGroupLayoutEntry {}
211
212 pub trait SealedGame {}
214 impl<T: crate::Game> SealedGame for T {}
215}
216
217pub struct PaddedBufferInitDescriptor<'a> {
218 pub label: wgpu::Label<'a>,
220 pub contents: Vec<u8>,
222 pub usage: wgpu::BufferUsages,
225}
226
227pub trait LfDeviceExt: sealed::SealedDevice {
229 fn create_buffer_padded(&self, desc: wgpu::BufferDescriptor) -> wgpu::Buffer;
230 fn create_buffer_init_padded(&self, desc: PaddedBufferInitDescriptor) -> wgpu::Buffer;
231
232 fn create_fragment_only_render_bundle_encoder<'a>(
233 &'a self,
234 desc: &FragmentOnlyRenderBundleEncoderDescriptor,
235 ) -> FragmentOnlyRenderBundleEncoder<'a>;
236
237 fn create_fragment_only_render_pipeline(
238 &self,
239 desc: &FragmentOnlyRenderPipelineDescriptor,
240 ) -> FragmentOnlyRenderPipeline;
241
242 unsafe fn create_shader_module_unchecked_on_release(
246 &self,
247 desc: wgpu::ShaderModuleDescriptor,
248 ) -> wgpu::ShaderModule;
249
250 fn assert_pop_error_scope(&self, msg: impl Into<String>);
252}
253
254impl LfDeviceExt for wgpu::Device {
255 fn create_buffer_padded(&self, mut desc: wgpu::BufferDescriptor) -> wgpu::Buffer {
256 desc.size = next_multiple_of(desc.size, BUFFER_PADDING);
257
258 self.create_buffer(&desc)
259 }
260
261 fn create_buffer_init_padded(&self, mut desc: PaddedBufferInitDescriptor) -> wgpu::Buffer {
262 let new_len = next_multiple_of(desc.contents.len() as wgpu::BufferAddress, BUFFER_PADDING);
263 desc.contents.resize(new_len as usize, 0u8);
264
265 self.create_buffer_init(&wgpu::util::BufferInitDescriptor {
266 label: desc.label,
267 contents: &desc.contents,
268 usage: desc.usage,
269 })
270 }
271
272 fn create_fragment_only_render_bundle_encoder<'a>(
273 &'a self,
274 desc: &FragmentOnlyRenderBundleEncoderDescriptor,
275 ) -> FragmentOnlyRenderBundleEncoder<'a> {
276 FragmentOnlyRenderBundleEncoder::new(self, desc)
277 }
278
279 fn create_fragment_only_render_pipeline(
280 &self,
281 desc: &FragmentOnlyRenderPipelineDescriptor,
282 ) -> FragmentOnlyRenderPipeline {
283 FragmentOnlyRenderPipeline::new(self, desc)
284 }
285
286 unsafe fn create_shader_module_unchecked_on_release(
287 &self,
288 desc: wgpu::ShaderModuleDescriptor,
289 ) -> wgpu::ShaderModule {
290 #[cfg(any(target_arch = "wasm32", debug_assertions))]
291 {
292 self.create_shader_module(desc)
293 }
294 #[cfg(not(any(target_arch = "wasm32", debug_assertions)))]
295 {
296 self.create_shader_module_unchecked(desc)
297 }
298 }
299
300 fn assert_pop_error_scope(&self, msg: impl Into<String>) {
301 let f = self.pop_error_scope();
302 async fn check_device_scope(
303 f: impl std::future::Future<Output = Option<wgpu::Error>>,
304 msg: String,
305 ) {
306 let res = f.await;
307 if let Some(e) = res {
308 panic!("error scope failure - {}: {:?}", msg, e)
309 }
310 }
311 block_on(check_device_scope(f, msg.into()));
312 }
313}
314
315pub trait LfCommandEncoderExt: sealed::SealedCommandEncoder {
317 fn begin_fragment_only_render_pass<'pass>(
318 &'pass mut self,
319 desc: &FragmentOnlyRenderPassDescriptor<'pass, '_>,
320 ) -> FragmentOnlyRenderPass<'pass>;
321}
322
323impl LfCommandEncoderExt for wgpu::CommandEncoder {
324 fn begin_fragment_only_render_pass<'pass>(
325 &'pass mut self,
326 desc: &FragmentOnlyRenderPassDescriptor<'pass, '_>,
327 ) -> FragmentOnlyRenderPass<'pass> {
328 FragmentOnlyRenderPass::new(self, desc)
329 }
330}
331
332pub trait LfLimitsExt: sealed::SealedLimits {
334 fn intersection<'a>(&self, other: &wgpu::Limits) -> wgpu::Limits;
336 fn union<'a>(&self, other: &wgpu::Limits) -> wgpu::Limits;
338}
339
340impl LfLimitsExt for wgpu::Limits {
341 fn intersection<'a>(&self, other: &wgpu::Limits) -> wgpu::Limits {
343 crate::limits::limits_intersection(self, other)
344 }
345 fn union<'a>(&self, other: &wgpu::Limits) -> wgpu::Limits {
347 crate::limits::limits_union(self, other)
348 }
349}
350
351pub trait LfQueueExt: sealed::SealedQueue {
353 fn write_buffer_padded(
360 &self,
361 buffer: &wgpu::Buffer,
362 offset: wgpu::BufferAddress,
363 data: Vec<u8>,
364 );
365}
366
367impl LfQueueExt for wgpu::Queue {
368 fn write_buffer_padded(
369 &self,
370 buffer: &wgpu::Buffer,
371 offset: wgpu::BufferAddress,
372 mut data: Vec<u8>,
373 ) {
374 const PAD_ALIGNMENT: usize = BUFFER_PADDING as usize;
375 let len = data.len();
376 let target_size = match len % PAD_ALIGNMENT {
377 0 => len,
378 r => len + (PAD_ALIGNMENT - r),
379 };
380 data.resize(target_size, 0u8);
381
382 self.write_buffer(buffer, offset, &data)
383 }
384}
385
386pub trait LfBufferExt: sealed::SealedBuffer {
388 fn debug_read_blocking(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> Vec<u8>;
393}
394
395impl LfBufferExt for wgpu::Buffer {
396 fn debug_read_blocking(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> Vec<u8> {
397 assert!(self.usage().contains(wgpu::BufferUsages::COPY_SRC));
398
399 let staging = device.create_buffer(&wgpu::BufferDescriptor {
400 label: Some("debug-read-staging"),
401 size: self.size(),
402 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
403 mapped_at_creation: false,
404 });
405
406 let mut cmd = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
407 label: Some("debug-read-cmd-encoder"),
408 });
409 cmd.copy_buffer_to_buffer(self, 0, &staging, 0, self.size());
410
411 queue.submit(vec![cmd.finish()]);
412
413 let (sender, receiver) = std::sync::mpsc::channel();
414 staging.slice(..).map_async(wgpu::MapMode::Read, move |e| {
415 sender.send(e).expect("failed to send result of map");
416 });
417
418 let _ = device.poll(wgpu::PollType::Wait {
419 submission_index: None,
420 timeout: None,
421 });
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}