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
11pub use fragment_only::FragmentOnlyRenderBundleEncoder;
12pub use fragment_only::FragmentOnlyRenderBundleEncoderDescriptor;
13pub use fragment_only::FragmentOnlyRenderPass;
14pub use fragment_only::FragmentOnlyRenderPassDescriptor;
15pub use fragment_only::FragmentOnlyRenderPipeline;
16pub use fragment_only::FragmentOnlyRenderPipelineDescriptor;
17pub use game::window::GameWindow;
18pub use game::window::WindowSizeDependent;
19pub use game::ExitFlag;
20pub use game::Game;
21pub use game::GameCommand;
22pub use game::GameData;
23pub use game::InputMode;
24pub mod input {
25 pub use crate::game::input::*;
26}
27pub mod local_storage;
28
29pub struct FastHashState {
31 seed: u32,
32}
33impl std::hash::BuildHasher for FastHashState {
34 type Hasher = rustc_hash::FxHasher;
35
36 fn build_hasher(&self) -> Self::Hasher {
37 use std::hash::Hasher;
38
39 let mut hasher = rustc_hash::FxHasher::default();
40 hasher.write_u32(self.seed);
41 return hasher;
42 }
43}
44impl Default for FastHashState {
45 fn default() -> Self {
46 use rand::Rng;
47 Self {
48 seed: rand::rng().random(),
49 }
50 }
51}
52
53pub type FastHashSet<T> = std::collections::HashSet<T, FastHashState>;
55pub type FastHashMap<K, V> = std::collections::HashMap<K, V, FastHashState>;
57
58use wgpu::util::DeviceExt;
59
60fn next_multiple_of(
61 value: wgpu::BufferAddress,
62 multiple: wgpu::BufferAddress,
63) -> wgpu::BufferAddress {
64 match value % multiple {
65 0 => value,
66 r => value + (multiple - r),
67 }
68}
69
70pub fn block_on(future: impl std::future::Future<Output = ()> + 'static) {
72 #[cfg(target_arch = "wasm32")]
73 wasm_bindgen_futures::spawn_local(future);
74
75 #[cfg(not(target_arch = "wasm32"))]
76 pollster::block_on(future);
77}
78
79pub fn is_html_tag_supported(tag: &str) -> bool {
82 #[cfg(not(target_arch = "wasm32"))]
83 {
84 let _ = tag;
85 return true;
86 }
87
88 #[cfg(target_arch = "wasm32")]
89 {
90 let window = match web_sys::window() {
91 None => return false,
92 Some(window) => window,
93 };
94 let document = match window.document() {
95 None => return false,
96 Some(document) => document,
97 };
98 return document.create_element(tag).is_ok();
99 }
100}
101
102#[cfg(target_arch = "wasm32")]
103mod pretty_alert {
104 pub(super) struct PrettyAlertFailure;
105
106 impl From<wasm_bindgen::JsValue> for PrettyAlertFailure {
107 fn from(_: wasm_bindgen::JsValue) -> Self {
108 Self
109 }
110 }
111 impl From<()> for PrettyAlertFailure {
112 fn from(_: ()) -> Self {
113 Self
114 }
115 }
116
117 pub(super) fn try_pretty_alert(msg: &str) -> Result<(), PrettyAlertFailure> {
118 use wasm_bindgen::JsCast;
119
120 let document = web_sys::window()
121 .expect("app requires window")
122 .document()
123 .expect("app requires DOM");
124 let body = document.body().expect("DOM has body");
125
126 let dialog = document.create_element("dialog")?;
127 dialog.set_id("lf-alert");
128 dialog.set_attribute("style", r#"font-family: mono; max-width: 50%;"#)?;
129 {
130 let text_div = document.create_element("div")?;
131 text_div.set_text_content(Some(msg));
132 dialog.append_child(&text_div)?;
133
134 let button_div = document.create_element("div")?;
135 button_div.set_attribute("style", r#"display: grid; margin-top: 10px;"#)?;
136 {
137 let button = document.create_element("button")?;
138 button.set_attribute(
139 "style",
140 r#"border: 0px; font-family: sans; font-size: 20px; padding: 10px;"#,
141 )?;
142 button.set_attribute("autofocus", "true")?;
143 button.set_attribute("onclick", "document.getElementById('lf-alert').remove();")?;
144 button.set_text_content(Some("Okay"));
145 button_div.append_child(&button)?;
146 }
147 dialog.append_child(&button_div)?;
148 }
149 body.append_child(&dialog)?;
150
151 dialog
152 .dyn_ref::<web_sys::HtmlDialogElement>()
153 .ok_or(())?
154 .show_modal()?;
155
156 Ok(())
157 }
158}
159
160pub fn alert_dialogue(msg: &str) {
162 #[cfg(target_arch = "wasm32")]
163 {
164 let res = pretty_alert::try_pretty_alert(msg);
165 if res.is_err() {
166 web_sys::window()
167 .expect("app requires window")
168 .alert_with_message(msg)
169 .expect("all browsers should have alert");
170 }
171 }
172 #[cfg(not(target_arch = "wasm32"))]
173 {
174 use dialog::DialogBox;
175 dialog::Message::new(msg)
176 .title("Alert")
177 .show()
178 .expect("dialog box unavailable")
179 }
180}
181
182pub const BUFFER_PADDING: wgpu::BufferAddress = 256;
187
188mod sealed {
191 pub trait SealedDevice {}
192 impl SealedDevice for wgpu::Device {}
193
194 pub trait SealedInstance {}
195 impl SealedInstance for wgpu::Instance {}
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(
233 &self,
234 desc: &FragmentOnlyRenderBundleEncoderDescriptor,
235 ) -> FragmentOnlyRenderBundleEncoder;
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(
273 &self,
274 desc: &FragmentOnlyRenderBundleEncoderDescriptor,
275 ) -> FragmentOnlyRenderBundleEncoder {
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 device.poll(wgpu::Maintain::Wait);
419
420 receiver
421 .recv()
422 .expect("failed to get result of map")
423 .expect("failed to read buffer");
424
425 let slice = staging.slice(..).get_mapped_range();
426 slice.to_vec()
427 }
428}
429
430pub trait LfBindGroupLayoutEntryExt: sealed::SealedBindGroupLayoutEntry {
432 fn read_only_compute_storage(binding: u32) -> Self;
434 fn mutable_compute_storage(binding: u32) -> Self;
435}
436
437impl LfBindGroupLayoutEntryExt for wgpu::BindGroupLayoutEntry {
438 fn read_only_compute_storage(binding: u32) -> Self {
439 wgpu::BindGroupLayoutEntry {
440 binding,
441 visibility: wgpu::ShaderStages::COMPUTE,
442 ty: wgpu::BindingType::Buffer {
443 ty: wgpu::BufferBindingType::Storage { read_only: true },
444 has_dynamic_offset: false,
445 min_binding_size: None,
446 },
447 count: None,
448 }
449 }
450
451 fn mutable_compute_storage(binding: u32) -> Self {
452 wgpu::BindGroupLayoutEntry {
453 binding,
454 visibility: wgpu::ShaderStages::COMPUTE,
455 ty: wgpu::BindingType::Buffer {
456 ty: wgpu::BufferBindingType::Storage { read_only: false },
457 has_dynamic_offset: false,
458 min_binding_size: None,
459 },
460 count: None,
461 }
462 }
463}
464
465pub trait LfGameExt: sealed::SealedGame {
467 type InitData;
468
469 fn run(init: Self::InitData);
471}
472
473impl<T: Game + 'static> LfGameExt for T {
474 type InitData = T::InitData;
475
476 fn run(init: T::InitData) {
478 game::GameState::<T>::run(init);
479 }
480}