aetheris_client_wasm/
shared_world.rs1use bytemuck::{Pod, Zeroable};
8use core::sync::atomic::{AtomicU64, Ordering};
9use std::collections::HashSet;
10use std::sync::{Mutex, OnceLock};
11
12static VALID_POINTERS: OnceLock<Mutex<HashSet<usize>>> = OnceLock::new();
13
14fn get_registry() -> &'static Mutex<HashSet<usize>> {
15 VALID_POINTERS.get_or_init(|| Mutex::new(HashSet::new()))
16}
17
18pub const MAX_ENTITIES: usize = 8192;
21
22#[derive(Copy, Clone, Debug, Pod, Zeroable)]
25#[repr(C)]
26pub struct SabSlot {
27 pub network_id: u64, pub x: f32, pub y: f32, pub z: f32, pub rotation: f32, pub dx: f32, pub dy: f32, pub dz: f32, pub integrity: u16, pub priority: u16, pub entity_type: u16, pub flags: u8, pub extraction_active: u8, pub payload_count: u16, pub payload_capacity: u16, pub extraction_target_id: u16, pub interaction_target_id: u16, pub interaction_flash_ticks: u8, pub padding: [u8; 3], }
66
67#[derive(Debug)]
74#[repr(C)]
75pub struct SabHeader {
76 pub state: AtomicU64, pub tick: AtomicU64, pub workspace_min_x: core::sync::atomic::AtomicU32, pub workspace_min_y: core::sync::atomic::AtomicU32, pub workspace_max_x: core::sync::atomic::AtomicU32, pub workspace_max_y: core::sync::atomic::AtomicU32, pub workspace_bounds_seq: core::sync::atomic::AtomicU32, pub sub_tick_fraction: core::sync::atomic::AtomicU32, }
90
91pub const SHARED_MEMORY_SIZE: usize =
95 core::mem::size_of::<SabHeader>() + (core::mem::size_of::<SabSlot>() * MAX_ENTITIES * 2);
96
97#[cfg(target_arch = "wasm32")]
99#[wasm_bindgen::prelude::wasm_bindgen]
100pub fn shared_world_size() -> usize {
101 SHARED_MEMORY_SIZE
102}
103
104pub struct SharedWorld {
107 ptr: *mut u8,
108 owns_memory: bool,
109}
110
111impl SharedWorld {
112 pub unsafe fn from_ptr(ptr: *mut u8) -> Self {
118 Self {
119 ptr,
120 owns_memory: false,
121 }
122 }
123
124 #[allow(clippy::missing_panics_doc)]
126 #[must_use]
127 pub fn new() -> Self {
128 let layout = core::alloc::Layout::from_size_align(SHARED_MEMORY_SIZE, 8)
129 .expect("Invalid SHARED_MEMORY_SIZE or alignment constants");
130 let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
131
132 if ptr.is_null() {
133 std::alloc::handle_alloc_error(layout);
134 }
135
136 get_registry()
138 .lock()
139 .expect("Registry mutex poisoned")
140 .insert(ptr as usize);
141
142 Self {
143 ptr,
144 owns_memory: true,
145 }
146 }
147
148 #[allow(clippy::missing_panics_doc)]
150 #[must_use]
151 pub fn is_valid(ptr: *mut u8) -> bool {
152 get_registry()
153 .lock()
154 .expect("Registry mutex poisoned")
155 .contains(&(ptr as usize))
156 }
157
158 #[must_use]
160 pub fn as_ptr(&self) -> *mut u8 {
161 self.ptr
162 }
163
164 #[allow(clippy::cast_ptr_alignment)]
165 fn header(&self) -> &SabHeader {
166 unsafe { &*(self.ptr.cast::<SabHeader>()) }
167 }
168
169 #[must_use]
171 pub fn active_index(&self) -> u32 {
172 (self.header().state.load(Ordering::Acquire) & 0xFFFF_FFFF) as u32
173 }
174
175 #[must_use]
177 pub fn entity_count(&self) -> u32 {
178 (self.header().state.load(Ordering::Acquire) >> 32) as u32
179 }
180
181 #[must_use]
183 pub fn tick(&self) -> u64 {
184 self.header().tick.load(Ordering::Acquire)
185 }
186
187 #[must_use]
189 pub fn sub_tick_fraction(&self) -> f32 {
190 f32::from_bits(self.header().sub_tick_fraction.load(Ordering::Acquire))
191 }
192
193 pub fn set_sub_tick_fraction(&self, fraction: f32) {
195 self.header()
196 .sub_tick_fraction
197 .store(fraction.to_bits(), Ordering::Release);
198 }
199
200 #[allow(clippy::cast_ptr_alignment)]
202 fn get_buffer(&self, idx: usize) -> &[SabSlot] {
203 let offset = core::mem::size_of::<SabHeader>()
204 + (idx * MAX_ENTITIES * core::mem::size_of::<SabSlot>());
205 unsafe { core::slice::from_raw_parts(self.ptr.add(offset).cast::<SabSlot>(), MAX_ENTITIES) }
206 }
207
208 #[allow(clippy::cast_ptr_alignment, clippy::mut_from_ref)]
210 fn get_buffer_mut(&self, idx: usize) -> &mut [SabSlot] {
211 let offset = core::mem::size_of::<SabHeader>()
212 + (idx * MAX_ENTITIES * core::mem::size_of::<SabSlot>());
213 unsafe {
214 core::slice::from_raw_parts_mut(self.ptr.add(offset).cast::<SabSlot>(), MAX_ENTITIES)
215 }
216 }
217
218 #[must_use]
223 pub fn get_read_buffer(&self) -> &[SabSlot] {
224 let state = self.header().state.load(Ordering::Acquire);
225 let active = (state & 0xFFFF_FFFF) as usize;
226 let count = ((state >> 32) as usize).min(MAX_ENTITIES);
227 &self.get_buffer(active)[..count]
228 }
229
230 #[must_use]
232 pub fn get_write_buffer(&self) -> &mut [SabSlot] {
233 let active = self.active_index() as usize;
234 let inactive = 1 - active;
235 self.get_buffer_mut(inactive)
236 }
237
238 pub fn commit_write(&self, entity_count: u32, tick: u64) {
240 let active = self.active_index();
241 let next_active = 1 - active;
242
243 let packed = (u64::from(entity_count) << 32) | u64::from(next_active);
244 self.header().tick.store(tick, Ordering::Release);
245 self.header().state.store(packed, Ordering::Release);
246 }
247
248 pub fn set_workspace_bounds(&self, min_x: f32, min_y: f32, max_x: f32, max_y: f32) {
253 let h = self.header();
254 let seq = h.workspace_bounds_seq.load(Ordering::Relaxed);
255 h.workspace_bounds_seq
257 .store(seq.wrapping_add(1), Ordering::Relaxed);
258 core::sync::atomic::fence(Ordering::Release);
259 h.workspace_min_x.store(min_x.to_bits(), Ordering::Relaxed);
260 h.workspace_min_y.store(min_y.to_bits(), Ordering::Relaxed);
261 h.workspace_max_x.store(max_x.to_bits(), Ordering::Relaxed);
262 h.workspace_max_y.store(max_y.to_bits(), Ordering::Relaxed);
263 h.workspace_bounds_seq
265 .store(seq.wrapping_add(2), Ordering::Release);
266 }
267
268 #[must_use]
271 pub fn get_workspace_bounds(&self) -> (f32, f32, f32, f32) {
272 let h = self.header();
273 loop {
274 let seq1 = h.workspace_bounds_seq.load(Ordering::Acquire);
275 if seq1 & 1 != 0 {
276 core::hint::spin_loop();
278 continue;
279 }
280 let min_x = f32::from_bits(h.workspace_min_x.load(Ordering::Relaxed));
281 let min_y = f32::from_bits(h.workspace_min_y.load(Ordering::Relaxed));
282 let max_x = f32::from_bits(h.workspace_max_x.load(Ordering::Relaxed));
283 let max_y = f32::from_bits(h.workspace_max_y.load(Ordering::Relaxed));
284 core::sync::atomic::fence(Ordering::Acquire);
285 let seq2 = h.workspace_bounds_seq.load(Ordering::Relaxed);
286 if seq1 == seq2 {
287 return (min_x, min_y, max_x, max_y);
288 }
289 core::hint::spin_loop();
291 }
292 }
293}
294
295impl Drop for SharedWorld {
296 #[allow(clippy::missing_panics_doc)]
297 fn drop(&mut self) {
298 if self.owns_memory {
299 if let Ok(mut reg) = get_registry().lock() {
300 reg.remove(&(self.ptr as usize));
301 }
302
303 let layout = core::alloc::Layout::from_size_align(SHARED_MEMORY_SIZE, 8)
304 .expect("Invalid SHARED_MEMORY_SIZE or alignment constants");
305
306 unsafe { std::alloc::dealloc(self.ptr, layout) };
307 }
308 }
309}
310
311impl Default for SharedWorld {
312 fn default() -> Self {
313 Self::new()
314 }
315}