1#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
7use std::cell::Cell;
8use std::collections::HashMap;
9#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::{Arc, RwLock};
12
13use anyhow::{Context, Result, anyhow, bail};
14use eluna::{EmotePlayerControl, EmoteStaticScene, EmoteTextureSource, TimelinePlayMode};
15
16mod player;
17
18#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
19static NEXT_RENDER_ID: AtomicU64 = AtomicU64::new(1);
20
21#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
22thread_local! {
23 static NEXT_RENDER_ID: Cell<u64> = Cell::new(1);
24}
25
26#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
27fn next_render_id() -> u64 {
28 NEXT_RENDER_ID.fetch_add(1, Ordering::Relaxed)
29}
30
31#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
32fn next_render_id() -> u64 {
33 NEXT_RENDER_ID.with(|next| {
34 let id = next.get();
35 next.set(id.wrapping_add(1).max(1));
36 id
37 })
38}
39
40#[derive(Debug, Clone)]
41pub struct EmoteDecodedTexture {
42 pub width: u32,
43 pub height: u32,
44 pub rgba: Arc<Vec<u8>>,
45}
46
47#[derive(Debug, Clone)]
48struct EmoteHitSurface {
49 version: u64,
50 width: u32,
51 height: u32,
52 alpha: Vec<u8>,
53}
54
55#[derive(Debug, Clone)]
56pub struct EmoteRenderPacket {
57 pub render_id: u64,
58 pub version: u64,
59 pub width: u32,
60 pub height: u32,
61 pub rep_x: f32,
62 pub rep_y: f32,
63 pub alpha_readback: bool,
64 pub scene: Arc<EmoteStaticScene>,
65 pub textures: Arc<HashMap<u32, EmoteDecodedTexture>>,
66 hit_surface: Arc<RwLock<Option<EmoteHitSurface>>>,
67}
68
69impl EmoteRenderPacket {
70 pub fn alpha_hit_test(&self, x: i32, y: i32) -> bool {
71 if x < 0 || y < 0 {
72 return false;
73 }
74 let Ok(surface) = self.hit_surface.read() else {
75 return false;
76 };
77 let Some(surface) = surface.as_ref() else {
78 return false;
79 };
80 if surface.version != self.version
81 || surface.width != self.width
82 || surface.height != self.height
83 {
84 return false;
85 }
86 let x = x as u32;
87 let y = y as u32;
88 if x >= surface.width || y >= surface.height {
89 return false;
90 }
91 surface
92 .alpha
93 .get((y as usize) * (surface.width as usize) + x as usize)
94 .copied()
95 .unwrap_or(0)
96 != 0
97 }
98
99 pub(crate) fn has_current_hit_surface(&self) -> bool {
100 let Ok(surface) = self.hit_surface.read() else {
101 return false;
102 };
103 surface.as_ref().is_some_and(|surface| {
104 surface.version == self.version
105 && surface.width == self.width
106 && surface.height == self.height
107 })
108 }
109
110 pub(crate) fn publish_hit_alpha(&self, alpha: Vec<u8>) {
111 if alpha.len() != self.width as usize * self.height as usize {
112 return;
113 }
114 let Ok(mut surface) = self.hit_surface.write() else {
115 return;
116 };
117 if surface
118 .as_ref()
119 .is_some_and(|current| current.version > self.version)
120 {
121 return;
122 }
123 *surface = Some(EmoteHitSurface {
124 version: self.version,
125 width: self.width,
126 height: self.height,
127 alpha,
128 });
129 }
130}
131
132#[derive(Debug, Clone)]
133pub struct SiglusEmoteRuntime {
134 runtime: player::Player,
135 decoded_textures: Arc<HashMap<u32, EmoteDecodedTexture>>,
136 render_id: u64,
137 version: u64,
138 hit_surface: Arc<RwLock<Option<EmoteHitSurface>>>,
139}
140
141impl SiglusEmoteRuntime {
142 pub fn from_psb_bytes(data: &[u8], key: Option<u32>) -> Result<Self> {
143 Self::from_psb_sources(&[data], key)
144 }
145
146 pub fn from_psb_sources(sources: &[&[u8]], key: Option<u32>) -> Result<Self> {
147 let runtime = player::Player::from_sources(sources, key)
148 .context("Eluna failed to create Emote runtime")?;
149 let spec = runtime.schema().spec.as_deref();
150
151 let mut decoded = HashMap::new();
152 for source in runtime.schema().textures.values() {
153 let bytes = runtime
154 .texture_bytes(source.resource_index)
155 .ok_or_else(|| {
156 anyhow!("missing Emote texture resource {}", source.resource_index)
157 })?;
158 let tex = decode_texture_source(source, bytes, spec).with_context(|| {
159 format!(
160 "failed to decode Emote texture {} resource {}",
161 source.name, source.resource_index
162 )
163 })?;
164 decoded.insert(source.resource_index, tex);
165 }
166
167 Ok(Self {
168 runtime,
169 decoded_textures: Arc::new(decoded),
170 render_id: next_render_id(),
171 version: 1,
172 hit_surface: Arc::new(RwLock::new(None)),
173 })
174 }
175
176 pub fn clone_for_object(&self) -> Self {
177 let mut cloned = self.clone();
178 cloned.render_id = next_render_id();
180 cloned.version = cloned.version.wrapping_add(1).max(1);
181 cloned.hit_surface = Arc::new(RwLock::new(None));
182 cloned
183 }
184
185 pub fn progress_ms(&mut self, ms: i32) -> Result<()> {
186 if ms <= 0 {
187 return Ok(());
188 }
189 self.runtime
192 .progress_ticks(ms as f32 * 60.0 / 1000.0)
193 .context("Eluna Emote Progress failed")?;
194 self.bump_version();
195 Ok(())
196 }
197
198 pub fn set_face_talk(&mut self, value: f32) -> Result<()> {
199 self.runtime
200 .inner
201 .set_variable_immediate("face_talk", value);
202 self.runtime
203 .rebuild_scene(0.0)
204 .context("Eluna SetVariable(face_talk) failed")?;
205 self.bump_version();
206 Ok(())
207 }
208
209 pub fn play_timeline(&mut self, name: &str, option: i64) -> Result<()> {
210 self.runtime
211 .play_timeline(name, TimelinePlayMode::from_flags(option as u32))
212 .with_context(|| format!("Eluna PlayTimeline({name:?}, {option}) failed"))?;
213 self.bump_version();
214 Ok(())
215 }
216
217 pub fn stop_all_timelines(&mut self) -> Result<()> {
220 self.runtime.inner.stop_timeline("");
221 self.runtime
222 .rebuild_scene(0.0)
223 .context("Eluna StopTimeline() failed")?;
224 self.bump_version();
225 Ok(())
226 }
227
228 pub fn stop_timeline(&mut self, name: &str) -> Result<()> {
229 self.runtime.inner.stop_timeline(name);
230 self.runtime
231 .rebuild_scene(0.0)
232 .with_context(|| format!("Eluna StopTimeline({name:?}) failed"))?;
233 self.bump_version();
234 Ok(())
235 }
236
237 pub fn is_animating(&self) -> bool {
238 self.runtime.inner.is_animating()
239 }
240
241 pub fn pass(&mut self) -> Result<()> {
242 self.runtime.inner.pass();
243 self.runtime
244 .rebuild_scene(0.0)
245 .context("Eluna Pass failed")?;
246 self.bump_version();
247 Ok(())
248 }
249
250 pub fn skip(&mut self) -> Result<()> {
251 self.runtime.inner.skip();
252 self.runtime
253 .rebuild_scene(0.0)
254 .context("Eluna scene rebuild after Skip failed")?;
255 self.bump_version();
256 Ok(())
257 }
258
259 pub fn packet(
260 &self,
261 width: i64,
262 height: i64,
263 rep_x: i64,
264 rep_y: i64,
265 alpha_readback: bool,
266 ) -> Arc<EmoteRenderPacket> {
267 Arc::new(EmoteRenderPacket {
268 render_id: self.render_id,
269 version: self.version,
270 width: width.max(1).min(u32::MAX as i64) as u32,
271 height: height.max(1).min(u32::MAX as i64) as u32,
272 rep_x: rep_x as f32,
273 rep_y: rep_y as f32,
274 alpha_readback,
275 scene: Arc::new(self.runtime.scene().clone()),
276 textures: self.decoded_textures.clone(),
277 hit_surface: self.hit_surface.clone(),
278 })
279 }
280
281 fn bump_version(&mut self) {
282 self.version = self.version.wrapping_add(1).max(1);
283 }
284}
285
286fn decode_texture_source(
287 source: &EmoteTextureSource,
288 bytes: &[u8],
289 spec: Option<&str>,
290) -> Result<EmoteDecodedTexture> {
291 let width = source.width.max(1);
292 let height = source.height.max(1);
293 let bit_count = source.bit_count.unwrap_or(32);
294 if bit_count != 32 {
295 bail!("unsupported Emote texture bit depth {bit_count}; expected 32-bit texture");
296 }
297 let expected = width as usize * height as usize * 4;
298
299 let raw = if source
300 .compress
301 .as_deref()
302 .map(|s| s.eq_ignore_ascii_case("RL"))
303 .unwrap_or(false)
304 {
305 decode_rl(bytes, 4, expected)?
306 } else if bytes.len() == expected {
307 bytes.to_vec()
308 } else {
309 let image = image::load_from_memory(bytes)
312 .context("Emote texture resource is neither raw RGBA/BGRA nor a decodable image")?
313 .to_rgba8();
314 if image.width() != width || image.height() != height {
315 bail!(
316 "decoded Emote texture size {}x{} does not match metadata {}x{}",
317 image.width(),
318 image.height(),
319 width,
320 height
321 );
322 }
323 return Ok(EmoteDecodedTexture {
324 width,
325 height,
326 rgba: Arc::new(image.into_raw()),
327 });
328 };
329
330 let format = source.format.as_deref().unwrap_or("");
331 let spec_upper = spec.unwrap_or("").to_ascii_uppercase();
332 let format_upper = format.to_ascii_uppercase();
333 let big_rgba_spec = matches!(
334 spec_upper.as_str(),
335 "COMMON" | "EMS" | "VITA" | "PSP" | "PS3"
336 );
337
338 let mode = match format_upper.as_str() {
339 "BERGBA8" => Raw32Mode::Rgba,
340 "LERGBA8" | "BGRA8" | "ARGB8" | "A8R8G8B8" | "D3DFMTA8R8G8B8" => Raw32Mode::Bgra,
341 "BGRX8" | "X8R8G8B8" | "D3DFMTX8R8G8B8" => Raw32Mode::Bgrx,
342 "RGBX8" | "RGBX" => Raw32Mode::Rgbx,
343 "RGBA" | "RGBA8" => {
344 if big_rgba_spec {
345 Raw32Mode::Rgba
346 } else {
347 Raw32Mode::Bgra
348 }
349 }
350 _ => {
351 if big_rgba_spec {
352 Raw32Mode::Rgba
353 } else {
354 Raw32Mode::Bgra
355 }
356 }
357 };
358
359 let mut rgba = vec![0u8; expected];
360 for (src, dst) in raw
361 .as_chunks::<4>()
362 .0
363 .iter()
364 .zip(rgba.as_chunks_mut::<4>().0.iter_mut())
365 {
366 match mode {
367 Raw32Mode::Rgba => dst.copy_from_slice(src),
368 Raw32Mode::Bgra => dst.copy_from_slice(&[src[2], src[1], src[0], src[3]]),
369 Raw32Mode::Bgrx => dst.copy_from_slice(&[src[2], src[1], src[0], 255]),
370 Raw32Mode::Rgbx => dst.copy_from_slice(&[src[0], src[1], src[2], 255]),
371 }
372 }
373
374 Ok(EmoteDecodedTexture {
375 width,
376 height,
377 rgba: Arc::new(rgba),
378 })
379}
380
381#[derive(Clone, Copy)]
382enum Raw32Mode {
383 Rgba,
384 Bgra,
385 Bgrx,
386 Rgbx,
387}
388
389fn decode_rl(bytes: &[u8], align: usize, expected: usize) -> Result<Vec<u8>> {
390 let mut out = Vec::with_capacity(expected);
391 let mut pos = 0usize;
392 while pos < bytes.len() && out.len() < expected {
393 let cmd = bytes[pos];
394 pos += 1;
395 if cmd & 0x80 != 0 {
396 let count = ((cmd ^ 0x80) as usize) + 3;
397 if pos + align > bytes.len() {
398 bail!("truncated Emote RL repeat command");
399 }
400 let unit = &bytes[pos..pos + align];
401 pos += align;
402 for _ in 0..count {
403 out.extend_from_slice(unit);
404 }
405 } else {
406 let count = (cmd as usize) + 1;
407 let len = count
408 .checked_mul(align)
409 .ok_or_else(|| anyhow!("Emote RL literal length overflow"))?;
410 if pos + len > bytes.len() {
411 bail!("truncated Emote RL literal command");
412 }
413 out.extend_from_slice(&bytes[pos..pos + len]);
414 pos += len;
415 }
416 if out.len() > expected {
417 bail!("Emote RL output exceeds expected texture size");
418 }
419 }
420 if out.len() != expected {
421 bail!(
422 "Emote RL output size mismatch: got {}, expected {}",
423 out.len(),
424 expected
425 );
426 }
427 Ok(out)
428}
429
430#[cfg(test)]
431mod physics_regression_tests {
432 use super::*;
433
434 #[test]
435 #[ignore = "requires SIGLUS_EMOTE_TEST_PROJECT pointing to 神待少女纱波"]
436 fn after_idle_physics_is_continuous_and_mouth_updates_preserve_it() {
437 let project =
438 std::path::PathBuf::from(std::env::var_os("SIGLUS_EMOTE_TEST_PROJECT").unwrap());
439 let key = siglus_assets::key_toml::load_emote_key_from_project_dir(&project).unwrap();
440 for (prefix, body, head) in [
443 ("bup", "sn01_03制服+エプロン", "sn01_頭部"),
444 ("ap_bup", "sn01_03制服+エプロン", "sn01_頭部"),
445 ("bup", "yu02_02私服", "yu02_頭部"),
446 ("ap_bup", "yu02_02私服", "yu02_頭部"),
447 ] {
448 let sources = [
449 format!("{prefix}_{body}.psb"),
450 format!("{prefix}_{head}.psb"),
451 "bup_共通tl.psb".to_owned(),
452 ]
453 .map(|name| std::fs::read(project.join("dat").join(name)).unwrap());
454 let mut runtime = SiglusEmoteRuntime::from_psb_sources(
455 &sources.iter().map(Vec::as_slice).collect::<Vec<_>>(),
456 key,
457 )
458 .unwrap();
459 runtime.play_timeline("喜ぶ02", 0).unwrap();
460 runtime.skip().unwrap();
461 runtime.play_timeline("ポーズA", 1).unwrap();
462 runtime.play_timeline("待機ループ00", 2).unwrap();
463 let mut previous = std::collections::BTreeMap::<String, f32>::new();
464 for frame in 0..600 {
466 runtime.progress_ms(16).unwrap();
467 let before_mouth = runtime.runtime.inner.evaluated_variable_values();
468 runtime.set_face_talk((frame % 10) as f32 / 10.0).unwrap();
469 let values = runtime.runtime.inner.evaluated_variable_values();
470 assert!(
471 values
472 .keys()
473 .any(|name| name.starts_with("hair_") || name.starts_with("bust_")),
474 "{prefix}_{body}: no physics outputs to check"
475 );
476 for (name, value) in values
477 .iter()
478 .filter(|(name, _)| name.starts_with("hair_") || name.starts_with("bust_"))
479 {
480 assert_eq!(
481 *value, before_mouth[name],
482 "{prefix} {frame} {name}: mouth changed physics"
483 );
484 assert!(value.is_finite());
485 if frame == 0 {
486 assert!(
487 value.abs() < 1.0,
488 "{prefix} {name}: startup impulse {value}"
489 );
490 }
491 if let Some(prev) = previous.insert(name.clone(), *value) {
492 assert!(
493 (value - prev).abs() < 2.0,
494 "{prefix} {frame} {name}: jumped from {prev} to {value}"
495 );
496 }
497 }
498 }
499 }
500 }
501}