diff --git a/.analyze.sh b/.analyze.sh new file mode 100644 index 0000000..297df47 --- /dev/null +++ b/.analyze.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# 分析下载页面 +echo "=== Page size ===" +wc -c /tmp/dl_page.html + +echo "=== All URLs containing download/zip/native ===" +grep -oiE 'https?://[^"'\'' <>]+(download|\.zip|native|cubism)[^"'\'' <>]*' /tmp/dl_page.html | sort -u | head -40 + +echo "=== Script sources ===" +grep -oiE ']*src="[^"]*"' /tmp/dl_page.html | head -20 + +echo "=== Data attributes ===" +grep -oiE 'data-[a-z]+="[^"]*"' /tmp/dl_page.html | head -20 + +echo "=== Form actions ===" +grep -oiE ']*action="[^"]*"' /tmp/dl_page.html | head -10 + +echo "=== Any cubism.live2d URLs ===" +grep -oiE 'https?://[a-z.]*live2d[^"'\'' <>]*' /tmp/dl_page.html | sort -u | head -30 + +echo "=== DONE ===" diff --git a/.dl_core.sh b/.dl_core.sh new file mode 100644 index 0000000..6399a33 --- /dev/null +++ b/.dl_core.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# 尝试下载 Cubism SDK for Native (含 Core 库) +cd /tmp +echo "=== Try downloading Cubism SDK for Native ===" + +# Live2D 官方下载通常用这种 URL 模式 +# 先尝试常见的下载链接 +URLS=( + "https://cubism.live2d.com/sdk-native/bin/CubismSdkForNative.zip" + "https://www.live2d.com/download/cubism-sdk/download-native/?agree=true" + "https://download.live2d.com/cubism-sdk/native/CubismSdkForNative.zip" +) + +for url in "${URLS[@]}"; do + echo "Trying: $url" + wget --timeout=15 --tries=1 -q "$url" -O test_download.bin 2>&1 + if [ $? -eq 0 ]; then + SIZE=$(stat -c%s test_download.bin 2>/dev/null || echo 0) + echo " Downloaded: $SIZE bytes" + if [ "$SIZE" -gt 100000 ]; then + echo " Looks like a real file! Checking type..." + file test_download.bin + mv test_download.bin CubismSdkForNative.zip + echo " SUCCESS: saved as CubismSdkForNative.zip" + exit 0 + fi + else + echo " Failed or too small" + fi + rm -f test_download.bin +done + +echo "=== Direct URL attempts failed ===" +echo "Checking actual download page for hidden links..." +curl -sL "https://www.live2d.com/zh-CHS/sdk/download/native/" 2>/dev/null | grep -oiE 'href="[^"]*\.zip[^"]*"' | head -10 +echo "---" +curl -sL "https://www.live2d.com/download/cubism-sdk/download-native/" 2>/dev/null | grep -oiE 'href="[^"]*download[^"]*"' | head -10 + +echo "=== DONE ===" diff --git a/.extract.sh b/.extract.sh new file mode 100644 index 0000000..52dd709 --- /dev/null +++ b/.extract.sh @@ -0,0 +1,15 @@ +#!/bin/bash +cd /tmp +echo "=== Extracting SDK ===" +unzip -o cubism_sdk_native.zip -d cubism_sdk 2>&1 | tail -5 + +echo "=== Finding Core .so files ===" +find cubism_sdk -name "libLive2DCubismCore*" -type f 2>/dev/null + +echo "=== Checking arm64 linux .so ===" +ls -lh cubism_sdk/Core/dll/experimental/linux/arm64/ 2>/dev/null + +echo "=== File type ===" +file cubism_sdk/Core/dll/experimental/linux/arm64/libLive2DCubismCore.so 2>/dev/null + +echo "=== DONE ===" diff --git a/.gitignore b/.gitignore index b9313f9..98a12c3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ target/ *.swp *.swo *~ +extern/ # 模型文件不进 git,只保留下载链接在 model_manager.rs 清单里 model_download/ model_store/ diff --git a/.haru.moc3 b/.haru.moc3 new file mode 100644 index 0000000..e5fc43a Binary files /dev/null and b/.haru.moc3 differ diff --git a/.haru.model3.json b/.haru.model3.json new file mode 100644 index 0000000..3e20b91 --- /dev/null +++ b/.haru.model3.json @@ -0,0 +1,115 @@ +{ + "Version": 3, + "FileReferences": { + "Moc": "Haru.moc3", + "Textures": [ + "Haru.2048/texture_00.png", + "Haru.2048/texture_01.png" + ], + "Physics": "Haru.physics3.json", + "Pose": "Haru.pose3.json", + "DisplayInfo": "Haru.cdi3.json", + "Expressions": [ + { + "Name": "F01", + "File": "expressions/F01.exp3.json" + }, + { + "Name": "F02", + "File": "expressions/F02.exp3.json" + }, + { + "Name": "F03", + "File": "expressions/F03.exp3.json" + }, + { + "Name": "F04", + "File": "expressions/F04.exp3.json" + }, + { + "Name": "F05", + "File": "expressions/F05.exp3.json" + }, + { + "Name": "F06", + "File": "expressions/F06.exp3.json" + }, + { + "Name": "F07", + "File": "expressions/F07.exp3.json" + }, + { + "Name": "F08", + "File": "expressions/F08.exp3.json" + } + ], + "Motions": { + "Idle": [ + { + "File": "motions/haru_g_idle.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + }, + { + "File": "motions/haru_g_m15.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5 + } + ], + "TapBody": [ + { + "File": "motions/haru_g_m26.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5, + "Sound": "sounds/haru_talk_13.wav" + }, + { + "File": "motions/haru_g_m06.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5, + "Sound": "sounds/haru_Info_14.wav" + }, + { + "File": "motions/haru_g_m20.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5, + "Sound": "sounds/haru_normal_6.wav" + }, + { + "File": "motions/haru_g_m09.motion3.json", + "FadeInTime": 0.5, + "FadeOutTime": 0.5, + "Sound": "sounds/haru_Info_04.wav" + } + ] + }, + "UserData": "Haru.userdata3.json" + }, + "Groups": [ + { + "Target": "Parameter", + "Name": "EyeBlink", + "Ids": [ + "ParamEyeLOpen", + "ParamEyeROpen" + ] + }, + { + "Target": "Parameter", + "Name": "LipSync", + "Ids": [ + "ParamMouthOpenY" + ] + } + ], + "HitAreas": [ + { + "Id": "HitArea", + "Name": "Head" + }, + { + "Id": "HitArea2", + "Name": "Body" + } + ] +} \ No newline at end of file diff --git a/.ssh_check.sh b/.ssh_check.sh new file mode 100644 index 0000000..c8da3e7 --- /dev/null +++ b/.ssh_check.sh @@ -0,0 +1,23 @@ +#!/bin/bash +echo "=== DOWNLOAD TOOLS ===" +which wget 2>/dev/null && echo "wget: yes" || echo "wget: no" +which curl 2>/dev/null && echo "curl: yes" || echo "curl: no" + +echo "=== OPENGL/EGL LIBS ===" +ls /usr/lib/aarch64-linux-gnu/libEGL* 2>/dev/null || echo "no EGL lib" +ls /usr/lib/aarch64-linux-gnu/libGLESv2* 2>/dev/null || echo "no GLES lib" +ls /usr/lib/aarch64-linux-gnu/libGL* 2>/dev/null || echo "no GL lib" + +echo "=== PKG CONFIG ===" +pkg-config --list-all 2>/dev/null | grep -iE 'egl|gles|gl' || echo "no pkg-config gl" + +echo "=== BUILD TOOLS ===" +gcc --version 2>/dev/null | head -1 || echo "no gcc" +g++ --version 2>/dev/null | head -1 || echo "no g++" +cmake --version 2>/dev/null | head -1 || echo "no cmake" + +echo "=== RUST ===" +ls ~/.rustup/toolchains/*/bin/rustc 2>/dev/null | head -1 +~/.rustup/toolchains/stable-aarch64-unknown-linux-gnu/bin/rustc --version 2>/dev/null + +echo "=== DONE ===" diff --git a/src/plugins/live2d/animation.rs b/src/plugins/live2d/animation.rs new file mode 100644 index 0000000..9423f1d --- /dev/null +++ b/src/plugins/live2d/animation.rs @@ -0,0 +1,148 @@ +//! 动画系统:眨眼、嘴部动效、待机呼吸 + +use super::core_ffi::CubismCore; +use super::model::CubismModel; +use std::time::{Duration, Instant}; + +/// 动画状态 +pub struct AnimationState { + /// 眨眼定时器 + last_blink: Instant, + next_blink_interval: Duration, + blink_phase: BlinkPhase, + /// 嘴部状态 + talking: bool, + mouth_open: f32, + mouth_target: f32, + last_mouth_update: Instant, + /// 呼吸 + breath_phase: f32, + /// 身体摇摆 + body_phase: f32, + /// 眼球跟随(简化为固定值) + eye_ball_x: f32, + eye_ball_y: f32, +} + +#[derive(Clone, Copy, Debug)] +enum BlinkPhase { + Idle, + Closing(f32), // 0→1 + Opening(f32), // 1→0 +} + +impl Default for AnimationState { + fn default() -> Self { + Self { + last_blink: Instant::now(), + next_blink_interval: Duration::from_secs(3), + blink_phase: BlinkPhase::Idle, + talking: false, + mouth_open: 0.0, + mouth_target: 0.0, + last_mouth_update: Instant::now(), + breath_phase: 0.0, + body_phase: 0.0, + eye_ball_x: 0.0, + eye_ball_y: 0.0, + } + } +} + +impl AnimationState { + pub fn set_talking(&mut self, talking: bool) { + self.talking = talking; + if !talking { + self.mouth_target = 0.0; + } + } + + /// 更新动画并应用到 model 参数 + pub fn update(&mut self, core: &CubismCore, model: &CubismModel, now: Instant, dt: f32) { + // === 呼吸 === + self.breath_phase += dt * 0.5; // 0.5 Hz + let breath = self.breath_phase.sin() * 0.5 + 0.5; + model.set_param(core, "ParamBreath", breath); + + // === 身体摇摆 === + self.body_phase += dt * 0.3; + let body_x = self.body_phase.sin() * 8.0; + let body_y = self.body_phase.sin() * 0.5; + model.set_param(core, "ParamBodyAngleX", body_x); + model.set_param(core, "ParamBodyAngleY", body_y); + + // === 眨眼 === + self.update_blink(now, core, model); + + // === 嘴部 === + self.update_mouth(now, dt, core, model); + + // === 眼球(轻微随机移动)=== + self.eye_ball_x = (now.elapsed().as_secs_f32() * 0.3).sin() * 0.3; + self.eye_ball_y = (now.elapsed().as_secs_f32() * 0.2).cos() * 0.2; + model.set_param(core, "ParamEyeBallX", self.eye_ball_x); + model.set_param(core, "ParamEyeBallY", self.eye_ball_y); + } + + fn update_blink(&mut self, now: Instant, core: &CubismCore, model: &CubismModel) { + match &mut self.blink_phase { + BlinkPhase::Idle => { + if now.duration_since(self.last_blink) >= self.next_blink_interval { + self.blink_phase = BlinkPhase::Closing(0.0); + self.last_blink = now; + } + } + BlinkPhase::Closing(t) => { + *t += 0.15; // 闭眼速度 + if *t >= 1.0 { + *t = 1.0; + self.blink_phase = BlinkPhase::Opening(1.0); + } + let eye_open = 1.0 - *t; + model.set_param(core, "ParamEyeLOpen", eye_open); + model.set_param(core, "ParamEyeROpen", eye_open); + } + BlinkPhase::Opening(t) => { + *t -= 0.1; // 睁眼速度(比闭眼慢) + if *t <= 0.0 { + *t = 0.0; + self.blink_phase = BlinkPhase::Idle; + self.last_blink = now; + // 下次眨眼间隔(2-5 秒随机) + self.next_blink_interval = + Duration::from_secs(2 + (now.elapsed().as_nanos() % 3) as u64); + } + let eye_open = 1.0 - *t; + model.set_param(core, "ParamEyeLOpen", eye_open); + model.set_param(core, "ParamEyeROpen", eye_open); + } + } + } + + fn update_mouth( + &mut self, + now: Instant, + dt: f32, + core: &CubismCore, + model: &CubismModel, + ) { + if self.talking { + // 说话时嘴部快速开合 + let t = now.elapsed().as_secs_f32(); + // 用多个正弦叠加产生自然的变化 + let base = (t * 12.0).sin() * 0.4 + (t * 7.0).sin() * 0.3 + (t * 19.0).sin() * 0.2; + self.mouth_target = (base * 0.5 + 0.5).clamp(0.0, 1.0) * 0.9 + 0.1; + } else { + self.mouth_target = 0.0; + } + + // 平滑过渡 + let lerp_speed = 15.0 * dt; + self.mouth_open += (self.mouth_target - self.mouth_open) * lerp_speed.min(1.0); + model.set_param(core, "ParamMouthOpenY", self.mouth_open); + + // 嘴部形态也轻微变化 + let form = (now.elapsed().as_secs_f32() * 3.0).sin() * 0.3 * if self.talking { 1.0 } else { 0.0 }; + model.set_param(core, "ParamMouthForm", form); + } +} diff --git a/src/plugins/live2d/assets.rs b/src/plugins/live2d/assets.rs new file mode 100644 index 0000000..61606af --- /dev/null +++ b/src/plugins/live2d/assets.rs @@ -0,0 +1,155 @@ +//! 资源管理:model3.json 解析 + PNG 纹理加载 + +use anyhow::{anyhow, Context, Result}; +use serde::Deserialize; +use std::path::{Path, PathBuf}; + +/// model3.json 中的 Groups 条目 +#[derive(Deserialize, Clone, Debug)] +pub struct ModelGroup { + #[serde(rename = "Target")] + pub target: String, + #[serde(rename = "Name")] + pub name: String, + #[serde(rename = "Ids")] + pub ids: Vec, +} + +/// model3.json 文件引用 +#[derive(Deserialize, Clone, Debug)] +pub struct FileReferences { + #[serde(rename = "Moc")] + pub moc: String, + #[serde(rename = "Textures", default)] + pub textures: Vec, + #[serde(rename = "Physics", default)] + pub physics: Option, + #[serde(rename = "Pose", default)] + pub pose: Option, + #[serde(rename = "DisplayInfo", default)] + pub display_info: Option, +} + +/// model3.json 根结构 +#[derive(Deserialize, Clone, Debug)] +pub struct Model3Json { + #[serde(rename = "Version")] + pub version: i32, + #[serde(rename = "FileReferences")] + pub file_references: FileReferences, + #[serde(rename = "Groups", default)] + pub groups: Vec, +} + +impl Model3Json { + /// 从文件解析 + pub fn from_path(path: &Path) -> Result { + let text = std::fs::read_to_string(path) + .with_context(|| format!("读取 model3.json 失败: {}", path.display()))?; + let json: Model3Json = serde_json::from_str(&text) + .with_context(|| format!("解析 model3.json 失败: {}", path.display()))?; + Ok(json) + } + + /// 获取 LipSync 参数 ID 列表 + pub fn lip_sync_ids(&self) -> Vec { + self.groups + .iter() + .find(|g| g.target == "Parameter" && g.name == "LipSync") + .map(|g| g.ids.clone()) + .unwrap_or_else(|| vec!["ParamMouthOpenY".to_string()]) + } + + /// 获取 EyeBlink 参数 ID 列表 + pub fn eye_blink_ids(&self) -> Vec { + self.groups + .iter() + .find(|g| g.target == "Parameter" && g.name == "EyeBlink") + .map(|g| g.ids.clone()) + .unwrap_or_else(|| { + vec!["ParamEyeLOpen".to_string(), "ParamEyeROpen".to_string()] + }) + } +} + +/// 加载的纹理图像(RGBA) +pub struct TextureImage { + pub width: u32, + pub height: u32, + pub rgba: Vec, +} + +/// 加载 PNG 纹理(使用 image crate) +pub fn load_texture(path: &Path) -> Result { + let img = image::open(path) + .with_context(|| format!("加载纹理失败: {}", path.display()))? + .to_rgba8(); + let width = img.width(); + let height = img.height(); + let rgba = img.into_raw(); + Ok(TextureImage { + width, + height, + rgba, + }) +} + +/// 加载 model3.json 引用的所有纹理(返回按 texture_00, texture_01 顺序的列表) +pub fn load_all_textures( + model3_dir: &Path, + file_refs: &FileReferences, +) -> Result> { + let mut textures = Vec::new(); + for tex_rel in &file_refs.textures { + let tex_path = model3_dir.join(tex_rel); + let img = load_texture(&tex_path)?; + println!( + "[Live2D] 纹理加载: {} ({}x{})", + tex_rel, img.width, img.height + ); + textures.push(img); + } + Ok(textures) +} + +/// 解析 model3.json 并返回完整路径集 +pub struct ModelAssetPaths { + pub model3_json: PathBuf, + pub model3_dir: PathBuf, + pub moc3: PathBuf, + pub textures: Vec, +} + +/// 从 model3.json 相对路径(如 "haru/Haru.model3.json")解析资源路径 +pub fn resolve_asset_paths( + live2d_base_dir: &Path, + model3_json_rel: &str, +) -> Result { + let model3_json = live2d_base_dir.join(model3_json_rel); + if !model3_json.exists() { + return Err(anyhow!( + "model3.json 不存在: {}", + model3_json.display() + )); + } + let model3_dir = model3_json + .parent() + .ok_or_else(|| anyhow!("无法获取 model3.json 的父目录"))? + .to_path_buf(); + + let model3 = Model3Json::from_path(&model3_json)?; + let moc3 = model3_dir.join(&model3.file_references.moc); + let textures = model3 + .file_references + .textures + .iter() + .map(|t| model3_dir.join(t)) + .collect(); + + Ok(ModelAssetPaths { + model3_json, + model3_dir, + moc3, + textures, + }) +} diff --git a/src/plugins/live2d/core_ffi.rs b/src/plugins/live2d/core_ffi.rs new file mode 100644 index 0000000..1f1f2b1 --- /dev/null +++ b/src/plugins/live2d/core_ffi.rs @@ -0,0 +1,276 @@ +//! Cubism Core FFI 绑定 +//! +//! 直接对应 `Live2DCubismCore.h` 的 C API。 +//! 动态加载 libLive2DCubismCore.so(需 libm.so 预加载,因 Core 内部使用 powf)。 + +use libloading::{Library, Symbol}; +use std::ffi::CString; +use std::os::raw::{c_char, c_int, c_uchar, c_uint, c_void}; + +/// moc 对齐要求(字节) +pub const CSM_ALIGNOF_MOC: usize = 64; +/// model 对齐要求(字节) +pub const CSM_ALIGNOF_MODEL: usize = 16; + +/// 可绘制常量标志位 +pub mod constant_flags { + pub const BLEND_ADDITIVE: u8 = 1 << 0; + pub const BLEND_MULTIPLICATIVE: u8 = 1 << 1; + pub const IS_DOUBLE_SIDED: u8 = 1 << 2; + pub const IS_INVERTED_MASK: u8 = 1 << 3; +} + +/// 可绘制动态标志位 +pub mod dynamic_flags { + pub const IS_VISIBLE: u8 = 1 << 0; + pub const VISIBILITY_DID_CHANGE: u8 = 1 << 1; + pub const OPACITY_DID_CHANGE: u8 = 1 << 2; + pub const DRAW_ORDER_DID_CHANGE: u8 = 1 << 3; + pub const RENDER_ORDER_DID_CHANGE: u8 = 1 << 4; + pub const VERTEX_POSITIONS_DID_CHANGE: u8 = 1 << 5; + pub const BLEND_COLOR_DID_CHANGE: u8 = 1 << 6; +} + +/// 混合模式 +pub mod blend_mode { + pub const NORMAL: c_int = 0; + pub const ADDITIVE: c_int = 1; + pub const MULTIPLICATIVE: c_int = 2; +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +pub struct CsmVector2 { + pub x: f32, + pub y: f32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +pub struct CsmVector4 { + pub x: f32, + pub y: f32, + pub z: f32, + pub w: f32, +} + +/// Cubism Core 动态加载封装 +pub struct CubismCore { + _libm: Library, + _lib: Library, + pub csm_get_version: unsafe extern "C" fn() -> c_uint, + pub csm_get_latest_moc_version: unsafe extern "C" fn() -> c_uint, + pub csm_get_moc_version: + unsafe extern "C" fn(address: *const c_void, size: c_uint) -> c_uint, + pub csm_has_moc_consistency: + unsafe extern "C" fn(address: *mut c_void, size: c_uint) -> c_int, + pub csm_revive_moc_in_place: + unsafe extern "C" fn(address: *mut c_void, size: c_uint) -> *mut c_void, + pub csm_get_sizeof_model: unsafe extern "C" fn(moc: *const c_void) -> c_uint, + pub csm_initialize_model_in_place: unsafe extern "C" fn( + moc: *const c_void, + address: *mut c_void, + size: c_uint, + ) -> *mut c_void, + pub csm_update_model: unsafe extern "C" fn(model: *mut c_void), + pub csm_get_render_orders: unsafe extern "C" fn(model: *const c_void) -> *const c_int, + pub csm_read_canvas_info: unsafe extern "C" fn( + model: *const c_void, + out_size: *mut CsmVector2, + out_origin: *mut CsmVector2, + out_ppu: *mut f32, + ), + pub csm_get_parameter_count: unsafe extern "C" fn(model: *const c_void) -> c_int, + pub csm_get_parameter_ids: + unsafe extern "C" fn(model: *const c_void) -> *const *const c_char, + pub csm_get_parameter_minimum_values: + unsafe extern "C" fn(model: *const c_void) -> *const f32, + pub csm_get_parameter_maximum_values: + unsafe extern "C" fn(model: *const c_void) -> *const f32, + pub csm_get_parameter_default_values: + unsafe extern "C" fn(model: *const c_void) -> *const f32, + pub csm_get_parameter_values: unsafe extern "C" fn(model: *mut c_void) -> *mut f32, + pub csm_get_part_count: unsafe extern "C" fn(model: *const c_void) -> c_int, + pub csm_get_part_opacities: unsafe extern "C" fn(model: *mut c_void) -> *mut f32, + pub csm_get_drawable_count: unsafe extern "C" fn(model: *const c_void) -> c_int, + pub csm_get_drawable_ids: + unsafe extern "C" fn(model: *const c_void) -> *const *const c_char, + pub csm_get_drawable_constant_flags: + unsafe extern "C" fn(model: *const c_void) -> *const c_uchar, + pub csm_get_drawable_dynamic_flags: + unsafe extern "C" fn(model: *const c_void) -> *const c_uchar, + pub csm_get_drawable_blend_modes: + unsafe extern "C" fn(model: *const c_void) -> *const c_int, + pub csm_get_drawable_texture_indices: + unsafe extern "C" fn(model: *const c_void) -> *const c_int, + pub csm_get_drawable_draw_orders: + unsafe extern "C" fn(model: *const c_void) -> *const c_int, + pub csm_get_drawable_opacities: + unsafe extern "C" fn(model: *const c_void) -> *const f32, + pub csm_get_drawable_mask_counts: + unsafe extern "C" fn(model: *const c_void) -> *const c_int, + pub csm_get_drawable_masks: + unsafe extern "C" fn(model: *const c_void) -> *const *const c_int, + pub csm_get_drawable_vertex_counts: + unsafe extern "C" fn(model: *const c_void) -> *const c_int, + pub csm_get_drawable_vertex_positions: + unsafe extern "C" fn(model: *const c_void) -> *const *const CsmVector2, + pub csm_get_drawable_vertex_uvs: + unsafe extern "C" fn(model: *const c_void) -> *const *const CsmVector2, + pub csm_get_drawable_index_counts: + unsafe extern "C" fn(model: *const c_void) -> *const c_int, + pub csm_get_drawable_indices: + unsafe extern "C" fn(model: *const c_void) -> *const *const u16, + pub csm_get_drawable_multiply_colors: + unsafe extern "C" fn(model: *const c_void) -> *const CsmVector4, + pub csm_get_drawable_screen_colors: + unsafe extern "C" fn(model: *const c_void) -> *const CsmVector4, + pub csm_reset_drawable_dynamic_flags: unsafe extern "C" fn(model: *mut c_void), +} + +impl CubismCore { + /// 加载 Cubism Core 动态库 + /// + /// `so_path` 为 libLive2DCubismCore.so 的路径。 + /// 内部会先 dlopen libm.so(Core 依赖 powf 但 NEEDED 未声明)。 + pub fn load(so_path: &str) -> anyhow::Result { + // 预加载 libm,RTLD_GLOBAL 让后续 .so 能解析到 powf + let libm = unsafe { + Library::new("libm.so.6").or_else(|_| Library::new("libm.so.6.1"))? + }; + + let lib = unsafe { Library::new(so_path)? }; + + unsafe { + Ok(Self { + csm_get_version: *lib.get(b"csmGetVersion\0")?, + csm_get_latest_moc_version: *lib.get(b"csmGetLatestMocVersion\0")?, + csm_get_moc_version: *lib.get(b"csmGetMocVersion\0")?, + csm_has_moc_consistency: *lib.get(b"csmHasMocConsistency\0")?, + csm_revive_moc_in_place: *lib.get(b"csmReviveMocInPlace\0")?, + csm_get_sizeof_model: *lib.get(b"csmGetSizeofModel\0")?, + csm_initialize_model_in_place: *lib.get(b"csmInitializeModelInPlace\0")?, + csm_update_model: *lib.get(b"csmUpdateModel\0")?, + csm_get_render_orders: *lib.get(b"csmGetRenderOrders\0")?, + csm_read_canvas_info: *lib.get(b"csmReadCanvasInfo\0")?, + csm_get_parameter_count: *lib.get(b"csmGetParameterCount\0")?, + csm_get_parameter_ids: *lib.get(b"csmGetParameterIds\0")?, + csm_get_parameter_minimum_values: *lib + .get(b"csmGetParameterMinimumValues\0")?, + csm_get_parameter_maximum_values: *lib + .get(b"csmGetParameterMaximumValues\0")?, + csm_get_parameter_default_values: *lib + .get(b"csmGetParameterDefaultValues\0")?, + csm_get_parameter_values: *lib.get(b"csmGetParameterValues\0")?, + csm_get_part_count: *lib.get(b"csmGetPartCount\0")?, + csm_get_part_opacities: *lib.get(b"csmGetPartOpacities\0")?, + csm_get_drawable_count: *lib.get(b"csmGetDrawableCount\0")?, + csm_get_drawable_ids: *lib.get(b"csmGetDrawableIds\0")?, + csm_get_drawable_constant_flags: *lib + .get(b"csmGetDrawableConstantFlags\0")?, + csm_get_drawable_dynamic_flags: *lib + .get(b"csmGetDrawableDynamicFlags\0")?, + csm_get_drawable_blend_modes: *lib.get(b"csmGetDrawableBlendModes\0")?, + csm_get_drawable_texture_indices: *lib + .get(b"csmGetDrawableTextureIndices\0")?, + csm_get_drawable_draw_orders: *lib.get(b"csmGetDrawableDrawOrders\0")?, + csm_get_drawable_opacities: *lib.get(b"csmGetDrawableOpacities\0")?, + csm_get_drawable_mask_counts: *lib.get(b"csmGetDrawableMaskCounts\0")?, + csm_get_drawable_masks: *lib.get(b"csmGetDrawableMasks\0")?, + csm_get_drawable_vertex_counts: *lib + .get(b"csmGetDrawableVertexCounts\0")?, + csm_get_drawable_vertex_positions: *lib + .get(b"csmGetDrawableVertexPositions\0")?, + csm_get_drawable_vertex_uvs: *lib.get(b"csmGetDrawableVertexUvs\0")?, + csm_get_drawable_index_counts: *lib.get(b"csmGetDrawableIndexCounts\0")?, + csm_get_drawable_indices: *lib.get(b"csmGetDrawableIndices\0")?, + csm_get_drawable_multiply_colors: *lib + .get(b"csmGetDrawableMultiplyColors\0")?, + csm_get_drawable_screen_colors: *lib + .get(b"csmGetDrawableScreenColors\0")?, + csm_reset_drawable_dynamic_flags: *lib + .get(b"csmResetDrawableDynamicFlags\0")?, + _libm: libm, + _lib: lib, + }) + } + } + + /// 查询 Core 版本(返回原始 u32,格式 0xMMmmpppp) + pub fn version(&self) -> u32 { + unsafe { (self.csm_get_version)() } + } + + /// 格式化版本号为 "major.minor.patch" + pub fn version_string(&self) -> String { + let v = self.version(); + format!( + "{}.{}.{}", + (v >> 24) & 0xFF, + (v >> 16) & 0xFF, + v & 0xFFFF + ) + } +} + +/// 分配对齐内存(返回对齐指针和底层分配,底层分配需保持存活) +pub fn alloc_aligned(size: usize, align: usize) -> anyhow::Result { + let layout = std::alloc::Layout::from_size_align(size, align)?; + let ptr = unsafe { std::alloc::alloc(layout) }; + if ptr.is_null() { + anyhow::bail!("aligned alloc failed for size={size} align={align}"); + } + Ok(AlignedBuffer { ptr, layout }) +} + +pub struct AlignedBuffer { + ptr: *mut u8, + layout: std::alloc::Layout, +} + +impl AlignedBuffer { + pub fn as_ptr(&self) -> *mut u8 { + self.ptr + } + pub fn as_mut_void(&mut self) -> *mut c_void { + self.ptr as *mut c_void + } + pub fn as_void(&self) -> *const c_void { + self.ptr as *const c_void + } +} + +impl Drop for AlignedBuffer { + fn drop(&mut self) { + unsafe { std::alloc::dealloc(self.ptr, self.layout) } + } +} + +unsafe impl Send for AlignedBuffer {} +unsafe impl Sync for AlignedBuffer {} + +/// 将 C 字符串指针转换为 Rust String(空指针返回 None) +pub unsafe fn cstr_ptr_to_string(ptr: *const c_char) -> Option { + if ptr.is_null() { + return None; + } + let raw = std::slice::from_raw_parts(ptr as *const u8, libc_strlen(ptr)); + Some(String::from_utf8_lossy(raw).into_owned()) +} + +/// 简易 strlen(避免依赖 libc crate) +unsafe fn libc_strlen(ptr: *const c_char) -> usize { + let mut p = ptr as *const u8; + let mut n = 0; + while *p != 0 { + p = p.add(1); + n += 1; + } + n +} + +// 保留 CString 防止 unused 警告(未来日志函数需要) +#[allow(dead_code)] +fn _ensure_cstring_linked() -> CString { + CString::new("").unwrap() +} diff --git a/src/plugins/live2d/gl.rs b/src/plugins/live2d/gl.rs new file mode 100644 index 0000000..1437039 --- /dev/null +++ b/src/plugins/live2d/gl.rs @@ -0,0 +1,736 @@ +//! OpenGL ES 2.0 / EGL 渲染后端 +//! +//! 使用 EGL 创建离屏 PBuffer 上下文(无窗口),用 OpenGL ES 2.0 渲染 Live2D 模型。 +//! 渲染结果通过 glReadPixels 读取,交给 video 插件或直接显示。 +//! +//! 这里采用 X11 窗口方式渲染(设备有 X11 + kwin),直接显示到屏幕。 +//! 如需离屏渲染可改为 PBuffer。 + +use anyhow::{anyhow, Context, Result}; +use std::ffi::c_void; +use std::os::raw::{c_char, c_int, c_uint, c_void as c_void_raw}; + +// ============ EGL 类型与常量 ============ + +pub mod egl { + pub const EGL_VERSION_1_5: c_int = 1; + + pub const EGL_SURFACE_TYPE: c_int = 0x3033; + pub const EGL_WINDOW_BIT: c_int = 0x0004; + pub const EGL_PBUFFER_BIT: c_int = 0x0001; + pub const EGL_BLUE_SIZE: c_int = 0x3022; + pub const EGL_GREEN_SIZE: c_int = 0x3023; + pub const EGL_RED_SIZE: c_int = 0x3024; + pub const EGL_DEPTH_SIZE: c_int = 0x3025; + pub const EGL_ALPHA_SIZE: c_int = 0x3021; + pub const EGL_STENCIL_SIZE: c_int = 0x3026; + pub const EGL_RENDERABLE_TYPE: c_int = 0x3040; + pub const EGL_OPENGL_ES2_BIT: c_int = 0x0004; + pub const EGL_NONE: c_int = 0x3038; + pub const EGL_CONTEXT_CLIENT_VERSION: c_int = 0x3098; + + pub const EGL_DISPLAY: usize = 0; + pub const EGL_NO_DISPLAY: isize = 0; + pub const EGL_NO_CONTEXT: isize = 0; + pub const EGL_NO_SURFACE: isize = 0; + + pub type EGLDisplay = *mut c_void; + pub type EGLConfig = *mut c_void; + pub type EGLContext = *mut c_void; + pub type EGLSurface = *mut c_void; + pub type NativeDisplayType = *mut c_void; + pub type NativeWindowType = *mut c_void; + pub type EGLBoolean = c_uint; + pub type EGLint = c_int; +} + +// ============ X11 类型 ============ + +pub mod x11 { + use std::os::raw::{c_long, c_uint, c_void}; + + pub type Display = c_void; + pub type Window = c_long; + pub type XID = c_long; + + #[repr(C)] + pub struct XSetWindowAttributes { + pub background_pixmap: c_long, + pub background_pixel: c_long, + pub border_pixmap: c_long, + pub border_pixel: c_long, + pub bit_gravity: c_int, + pub win_gravity: c_int, + pub backing_store: c_int, + pub backing_planes: c_long, + pub backing_pixel: c_long, + pub save_under: c_int, + pub event_mask: c_long, + pub do_not_propagate_mask: c_long, + pub override_redirect: c_int, + pub colormap: c_long, + pub cursor: c_long, + } + + pub const CWOverrideRedirect: c_long = 1 << 9; + pub const CWEventMask: c_long = 1 << 11; + pub const ExposureMask: c_long = 1 << 15; + pub const StructureNotifyMask: c_long = 1 << 17; + pub const KeyPressMask: c_long = 1 << 0; +} + +// ============ GLES2 类型与常量 ============ + +pub mod gl { + use std::os::raw::{c_int, c_uint}; + + pub type GLenum = c_uint; + pub type GLuint = c_uint; + pub type GLint = c_int; + pub type GLsizei = c_int; + pub type GLbitfield = c_uint; + pub type GLfloat = f32; + + pub const GL_FLOAT: GLenum = 0x1406; + pub const GL_FLOAT_VEC2: GLenum = 0x8B50; + pub const GL_FLOAT_VEC4: GLenum = 0x8B52; + pub const GL_FLOAT_MAT4: GLenum = 0x8B5C; + pub const GL_SAMPLER_2D: GLenum = 0x8B5E; + + pub const GL_ARRAY_BUFFER: GLenum = 0x8892; + pub const GL_ELEMENT_ARRAY_BUFFER: GLenum = 0x8893; + + pub const GL_TEXTURE_2D: GLenum = 0x0DE1; + pub const GL_TEXTURE0: GLenum = 0x84C0; + pub const GL_RGBA: GLenum = 0x1908; + pub const GL_UNSIGNED_BYTE: GLenum = 0x1401; + pub const GL_UNSIGNED_SHORT: GLenum = 0x1403; + pub const GL_NEAREST: GLenum = 0x2600; + pub const GL_LINEAR: GLenum = 0x2601; + pub const GL_CLAMP_TO_EDGE: GLenum = 0x812F; + pub const GL_TEXTURE_MIN_FILTER: GLenum = 0x2801; + pub const GL_TEXTURE_MAG_FILTER: GLenum = 0x2800; + pub const GL_TEXTURE_WRAP_S: GLenum = 0x2802; + pub const GL_TEXTURE_WRAP_T: GLenum = 0x2803; + + pub const GL_VERTEX_SHADER: GLenum = 0x8B31; + pub const GL_FRAGMENT_SHADER: GLenum = 0x8B30; + pub const GL_COMPILE_STATUS: GLenum = 0x8B81; + pub const GL_LINK_STATUS: GLenum = 0x8B82; + + pub const GL_TRIANGLES: GLenum = 0x0004; + pub const GL_BLEND: GLenum = 0x0BE2; + pub const GL_SRC_ALPHA: GLenum = 0x0302; + pub const GL_ONE_MINUS_SRC_ALPHA: GLenum = 0x0303; + pub const GL_ONE: GLenum = 1; + pub const GL_ZERO: GLenum = 0; + pub const GL_COLOR_BUFFER_BIT: GLbitfield = 0x4000; + pub const GL_DEPTH_BUFFER_BIT: GLbitfield = 0x0100; + pub const GL_BLEND_DST_RGB: GLenum = 0x80C8; + pub const GL_BLEND_SRC_RGB: GLenum = 0x80C9; + pub const GL_BLEND_DST_ALPHA: GLenum = 0x80CA; + pub const GL_BLEND_SRC_ALPHA: GLenum = 0x80CB; + pub const GL_BLEND_EQUATION: GLenum = 0x8009; + pub const GL_FUNC_ADD: GLenum = 0x8006; + pub const GL_FUNC_REVERSE_SUBTRACT: GLenum = 0x800B; + pub const GL_STATIC_DRAW: GLenum = 0x88E4; + pub const GL_FALSE: GLenum = 0; + pub const GL_TRUE: GLenum = 1; + pub const GL_NO_ERROR: GLenum = 0; +} + +/// 着色器源码(标准模式,支持 mask 和非 mask) +pub const VERT_SHADER_SRC: &str = r#"#version 100 +attribute vec4 a_position; +attribute vec2 a_texCoord; +varying vec2 v_texCoord; +uniform mat4 u_matrix; +void main() { + gl_Position = u_matrix * a_position; + v_texCoord = a_texCoord; + v_texCoord.y = 1.0 - v_texCoord.y; +} +"#; + +pub const FRAG_SHADER_SRC: &str = r#"#version 100 +precision highp float; +varying vec2 v_texCoord; +uniform sampler2D s_texture0; +uniform vec4 u_baseColor; +uniform vec4 u_multiplyColor; +uniform vec4 u_screenColor; +void main() { + vec4 texColor = texture2D(s_texture0, v_texCoord); + texColor.rgb = texColor.rgb * u_multiplyColor.rgb; + texColor.rgb = texColor.rgb + u_screenColor.rgb - (texColor.rgb * u_screenColor.rgb); + vec4 color = texColor * u_baseColor; + gl_FragColor = vec4(color.rgb * color.a, color.a); +} +"#; + +/// OpenGL ES2 / EGL 动态库函数集合 +pub struct Gl { + _lib_egl: libloading::Library, + _lib_gl: libloading::Library, + _lib_x11: libloading::Library, + // EGL + egl_get_display: + unsafe extern "C" fn(disp: egl::NativeDisplayType) -> egl::EGLDisplay, + egl_initialize: + unsafe extern "C" fn(dpy: egl::EGLDisplay, major: *mut c_int, minor: *mut c_int) -> egl::EGLBoolean, + egl_choose_config: unsafe extern "C" fn( + dpy: egl::EGLDisplay, + attrib_list: *const egl::EGLint, + configs: *mut egl::EGLConfig, + config_size: egl::EGLint, + num_config: *mut egl::EGLint, + ) -> egl::EGLBoolean, + egl_create_window_surface: unsafe extern "C" fn( + dpy: egl::EGLDisplay, + config: egl::EGLConfig, + win: egl::NativeWindowType, + attrib_list: *const egl::EGLint, + ) -> egl::EGLSurface, + egl_create_context: unsafe extern "C" fn( + dpy: egl::EGLDisplay, + config: egl::EGLConfig, + share_ctx: egl::EGLContext, + attrib_list: *const egl::EGLint, + ) -> egl::EGLContext, + egl_make_current: unsafe extern "C" fn( + dpy: egl::EGLDisplay, + draw: egl::EGLSurface, + read: egl::EGLSurface, + ctx: egl::EGLContext, + ) -> egl::EGLBoolean, + egl_swap_buffers: + unsafe extern "C" fn(dpy: egl::EGLDisplay, surface: egl::EGLSurface) -> egl::EGLBoolean, + egl_terminate: unsafe extern "C" fn(dpy: egl::EGLDisplay) -> egl::EGLBoolean, + + // GLES2 + gl_clear: unsafe extern "C" fn(mask: gl::GLbitfield), + gl_clear_color: unsafe extern "C" fn(r: f32, g: f32, b: f32, a: f32), + gl_enable: unsafe extern "C" fn(cap: gl::GLenum), + gl_disable: unsafe extern "C" fn(cap: gl::GLenum), + gl_blend_func: unsafe extern "C" fn(sfactor: gl::GLenum, dfactor: gl::GLenum), + gl_blend_func_separate: unsafe extern "C" fn( + src_rgb: gl::GLenum, + dst_rgb: gl::GLenum, + src_alpha: gl::GLenum, + dst_alpha: gl::GLenum, + ), + gl_viewport: unsafe extern "C" fn(x: gl::GLint, y: gl::GLint, w: gl::GLsizei, h: gl::GLsizei), + gl_create_shader: unsafe extern "C" fn(t: gl::GLenum) -> gl::GLuint, + gl_shader_source: unsafe extern "C" fn( + shader: gl::GLuint, + count: gl::GLsizei, + string: *const *const c_char, + length: *const gl::GLint, + ), + gl_compile_shader: unsafe extern "C" fn(shader: gl::GLuint), + gl_get_shaderiv: unsafe extern "C" fn( + shader: gl::GLuint, + pname: gl::GLenum, + params: *mut gl::GLint, + ), + gl_get_shader_info_log: unsafe extern "C" fn( + shader: gl::GLuint, + buf_size: gl::GLsizei, + length: *mut gl::GLsizei, + info_log: *mut c_char, + ), + gl_create_program: unsafe extern "C" fn() -> gl::GLuint, + gl_attach_shader: unsafe extern "C" fn(program: gl::GLuint, shader: gl::GLuint), + gl_link_program: unsafe extern "C" fn(program: gl::GLuint), + gl_get_programiv: unsafe extern "C" fn( + program: gl::GLuint, + pname: gl::GLenum, + params: *mut gl::GLint, + ), + gl_get_program_info_log: unsafe extern "C" fn( + program: gl::GLuint, + buf_size: gl::GLsizei, + length: *mut gl::GLsizei, + info_log: *mut c_char, + ), + gl_use_program: unsafe extern "C" fn(program: gl::GLuint), + gl_get_attrib_location: + unsafe extern "C" fn(program: gl::GLuint, name: *const c_char) -> gl::GLint, + gl_get_uniform_location: + unsafe extern "C" fn(program: gl::GLuint, name: *const c_char) -> gl::GLint, + gl_uniform_matrix4fv: unsafe extern "C" fn( + location: gl::GLint, + count: gl::GLsizei, + transpose: gl::GLenum, + value: *const f32, + ), + gl_uniform1i: unsafe extern "C" fn(location: gl::GLint, v: gl::GLint), + gl_uniform4f: + unsafe extern "C" fn(location: gl::GLint, x: f32, y: f32, z: f32, w: f32), + gl_gen_textures: unsafe extern "C" fn(n: gl::GLsizei, textures: *mut gl::GLuint), + gl_bind_texture: unsafe extern "C" fn(target: gl::GLenum, texture: gl::GLuint), + gl_tex_parameteri: + unsafe extern "C" fn(target: gl::GLenum, pname: gl::GLenum, param: gl::GLint), + gl_tex_image_2d: unsafe extern "C" fn( + target: gl::GLenum, + level: gl::GLint, + internalformat: gl::GLenum, + width: gl::GLsizei, + height: gl::GLsizei, + border: gl::GLint, + format: gl::GLenum, + type_: gl::GLenum, + data: *const c_void_raw, + ), + gl_active_texture: unsafe extern "C" fn(texture: gl::GLenum), + gl_gen_buffers: unsafe extern "C" fn(n: gl::GLsizei, buffers: *mut gl::GLuint), + gl_bind_buffer: unsafe extern "C" fn(target: gl::GLenum, buffer: gl::GLuint), + gl_buffer_data: unsafe extern "C" fn( + target: gl::GLenum, + size: isize, + data: *const c_void_raw, + usage: gl::GLenum, + ), + gl_enable_vertex_attrib_array: unsafe extern "C" fn(index: gl::GLuint), + gl_vertex_attrib_pointer: unsafe extern "C" fn( + index: gl::GLuint, + size: gl::GLint, + type_: gl::GLenum, + normalized: gl::GLenum, + stride: gl::GLsizei, + pointer: *const c_void_raw, + ), + gl_draw_elements: unsafe extern "C" fn( + mode: gl::GLenum, + count: gl::GLsizei, + type_: gl::GLenum, + indices: *const c_void_raw, + ), + gl_get_error: unsafe extern "C" fn() -> gl::GLenum, + gl_delete_textures: unsafe extern "C" fn(n: gl::GLsizei, textures: *const gl::GLuint), + gl_delete_buffers: unsafe extern "C" fn(n: gl::GLsizei, buffers: *const gl::GLuint), + + // X11 + x_open_display: + unsafe extern "C" fn(name: *const c_char) -> *mut x11::Display, + x_close_display: unsafe extern "C" fn(disp: *mut x11::Display) -> c_int, + x_create_window: unsafe extern "C" fn( + disp: *mut x11::Display, + parent: x11::Window, + x: c_int, + y: c_int, + width: c_uint, + height: c_uint, + border_width: c_uint, + depth: c_int, + class: c_int, + visual: *mut c_void, + valuemask: c_long, + attributes: *mut x11::XSetWindowAttributes, + ) -> x11::Window, + x_map_window: unsafe extern "C" fn(disp: *mut x11::Display, w: x11::Window) -> c_int, + x_store_name: + unsafe extern "C" fn(disp: *mut x11::Display, w: x11::Window, name: *const c_char) -> c_int, + x_flush: unsafe extern "C" fn(disp: *mut x11::Display) -> c_int, +} + +impl Gl { + /// 加载所有动态库函数 + pub fn load() -> Result { + let lib_egl = unsafe { libloading::Library::new("libEGL.so.1")? }; + let lib_gl = unsafe { libloading::Library::new("libGLESv2.so.2")? }; + let lib_x11 = unsafe { libloading::Library::new("libX11.so.6")? }; + + unsafe { + Ok(Self { + egl_get_display: *lib_egl.get(b"eglGetDisplay\0")?, + egl_initialize: *lib_egl.get(b"eglInitialize\0")?, + egl_choose_config: *lib_egl.get(b"eglChooseConfig\0")?, + egl_create_window_surface: *lib_egl.get(b"eglCreateWindowSurface\0")?, + egl_create_context: *lib_egl.get(b"eglCreateContext\0")?, + egl_make_current: *lib_egl.get(b"eglMakeCurrent\0")?, + egl_swap_buffers: *lib_egl.get(b"eglSwapBuffers\0")?, + egl_terminate: *lib_egl.get(b"eglTerminate\0")?, + + gl_clear: *lib_gl.get(b"glClear\0")?, + gl_clear_color: *lib_gl.get(b"glClearColor\0")?, + gl_enable: *lib_gl.get(b"glEnable\0")?, + gl_disable: *lib_gl.get(b"glDisable\0")?, + gl_blend_func: *lib_gl.get(b"glBlendFunc\0")?, + gl_blend_func_separate: *lib_gl.get(b"glBlendFuncSeparate\0")?, + gl_viewport: *lib_gl.get(b"glViewport\0")?, + gl_create_shader: *lib_gl.get(b"glCreateShader\0")?, + gl_shader_source: *lib_gl.get(b"glShaderSource\0")?, + gl_compile_shader: *lib_gl.get(b"glCompileShader\0")?, + gl_get_shaderiv: *lib_gl.get(b"glGetShaderiv\0")?, + gl_get_shader_info_log: *lib_gl.get(b"glGetShaderInfoLog\0")?, + gl_create_program: *lib_gl.get(b"glCreateProgram\0")?, + gl_attach_shader: *lib_gl.get(b"glAttachShader\0")?, + gl_link_program: *lib_gl.get(b"glLinkProgram\0")?, + gl_get_programiv: *lib_gl.get(b"glGetProgramiv\0")?, + gl_get_program_info_log: *lib_gl.get(b"glGetProgramInfoLog\0")?, + gl_use_program: *lib_gl.get(b"glUseProgram\0")?, + gl_get_attrib_location: *lib_gl.get(b"glGetAttribLocation\0")?, + gl_get_uniform_location: *lib_gl.get(b"glGetUniformLocation\0")?, + gl_uniform_matrix4fv: *lib_gl.get(b"glUniformMatrix4fv\0")?, + gl_uniform1i: *lib_gl.get(b"glUniform1i\0")?, + gl_uniform4f: *lib_gl.get(b"glUniform4f\0")?, + gl_gen_textures: *lib_gl.get(b"glGenTextures\0")?, + gl_bind_texture: *lib_gl.get(b"glBindTexture\0")?, + gl_tex_parameteri: *lib_gl.get(b"glTexParameteri\0")?, + gl_tex_image_2d: *lib_gl.get(b"glTexImage2D\0")?, + gl_active_texture: *lib_gl.get(b"glActiveTexture\0")?, + gl_gen_buffers: *lib_gl.get(b"glGenBuffers\0")?, + gl_bind_buffer: *lib_gl.get(b"glBindBuffer\0")?, + gl_buffer_data: *lib_gl.get(b"glBufferData\0")?, + gl_enable_vertex_attrib_array: *lib_gl.get(b"glEnableVertexAttribArray\0")?, + gl_vertex_attrib_pointer: *lib_gl.get(b"glVertexAttribPointer\0")?, + gl_draw_elements: *lib_gl.get(b"glDrawElements\0")?, + gl_get_error: *lib_gl.get(b"glGetError\0")?, + gl_delete_textures: *lib_gl.get(b"glDeleteTextures\0")?, + gl_delete_buffers: *lib_gl.get(b"glDeleteBuffers\0")?, + + x_open_display: *lib_x11.get(b"XOpenDisplay\0")?, + x_close_display: *lib_x11.get(b"XCloseDisplay\0")?, + x_create_window: *lib_x11.get(b"XCreateWindow\0")?, + x_map_window: *lib_x11.get(b"XMapWindow\0")?, + x_store_name: *lib_x11.get(b"XStoreName\0")?, + x_flush: *lib_x11.get(b"XFlush\0")?, + + _lib_egl: lib_egl, + _lib_gl: lib_gl, + _lib_x11: lib_x11, + }) + } + } +} + +/// EGL/GLES2/X11 渲染上下文 +pub struct RenderContext { + pub gl: Gl, + display: egl::EGLDisplay, + _config: egl::EGLConfig, + context: egl::EGLContext, + surface: egl::EGLSurface, + x_display: *mut x11::Display, + x_window: x11::Window, + pub program: GLuint, + pub attrib_position: GLint, + pub attrib_tex_coord: GLint, + pub uniform_matrix: GLint, + pub uniform_texture: GLint, + pub uniform_base_color: GLint, + pub uniform_multiply_color: GLint, + pub uniform_screen_color: GLint, + pub width: i32, + pub height: i32, +} + +use gl::*; + +/// 创建并链接着色器程序 +fn compile_shader(gl: &Gl, shader_type: GLenum, src: &str) -> Result { + unsafe { + let shader = (gl.gl_create_shader)(shader_type); + if shader == 0 { + return Err(anyhow!("glCreateShader 返回 0")); + } + let src_c = std::ffi::CString::new(src)?; + let src_ptr = src_c.as_ptr(); + (gl.gl_shader_source)(shader, 1, &src_ptr, std::ptr::null()); + (gl.gl_compile_shader)(shader); + + let mut status = 0; + (gl.gl_get_shaderiv)(shader, GL_COMPILE_STATUS, &mut status); + if status == 0 { + let mut log = vec![0i8; 1024]; + (gl.gl_get_shader_info_log)( + shader, + 1024, + std::ptr::null_mut(), + log.as_mut_ptr(), + ); + let log_str = String::from_utf8_lossy( + std::slice::from_raw_parts(log.as_ptr() as *const u8, 1024), + ); + return Err(anyhow!("着色器编译失败: {}", log_str)); + } + Ok(shader) + } +} + +impl RenderContext { + /// 创建全屏 X11 窗口 + EGL 上下文 + pub fn new_fullscreen(width: i32, height: i32) -> Result { + let gl = Gl::load()?; + + // X11 窗口 + let x_display = unsafe { (gl.x_open_display)(std::ptr::null()) }; + if x_display.is_null() { + return Err(anyhow!("XOpenDisplay 失败(DISPLAY 环境变量是否设置?)")); + } + let root_window: x11::Window = 0; // DefaultRootWindow + let mut attrs = x11::XSetWindowAttributes { + background_pixmap: 0, + background_pixel: 0, + border_pixmap: 0, + border_pixel: 0, + bit_gravity: 0, + win_gravity: 0, + backing_store: 0, + backing_planes: 0, + backing_pixel: 0, + save_under: 0, + event_mask: x11::ExposureMask | x11::StructureNotifyMask, + do_not_propagate_mask: 0, + override_redirect: 1, // 全屏无装饰 + colormap: 0, + cursor: 0, + }; + let x_window = unsafe { + (gl.x_create_window)( + x_display, + root_window, + 0, + 0, + width as u32, + height as u32, + 0, + 24, // depth + 1, // InputOutput + std::ptr::null_mut(), + x11::CWOverrideRedirect | x11::CWEventMask, + &mut attrs, + ) + }; + if x_window == 0 { + return Err(anyhow!("XCreateWindow 失败")); + } + unsafe { + let title = std::ffi::CString::new("ShowenV2 Live2D")?; + (gl.x_store_name)(x_display, x_window, title.as_ptr()); + (gl.x_map_window)(x_display, x_window); + (gl.x_flush)(x_display); + } + + // EGL + let display = unsafe { (gl.egl_get_display)(egl::EGL_DEFAULT_DISPLAY_PTR()) }; + if display.is_null() { + return Err(anyhow!("eglGetDisplay 失败")); + } + let mut major = 0; + let mut minor = 0; + let ok = unsafe { (gl.egl_initialize)(display, &mut major, &mut minor) }; + if ok == 0 { + return Err(anyhow!("eglInitialize 失败")); + } + println!("[Live2D] EGL 版本: {}.{}", major, minor); + + // 选配置 + let config_attrs = [ + egl::EGL_RED_SIZE, 8, + egl::EGL_GREEN_SIZE, 8, + egl::EGL_BLUE_SIZE, 8, + egl::EGL_ALPHA_SIZE, 8, + egl::EGL_SURFACE_TYPE, egl::EGL_WINDOW_BIT, + egl::EGL_RENDERABLE_TYPE, egl::EGL_OPENGL_ES2_BIT, + egl::EGL_NONE, + ]; + let mut config: egl::EGLConfig = std::ptr::null_mut(); + let mut num_config = 0; + let ok = unsafe { + (gl.egl_choose_config)( + display, + config_attrs.as_ptr(), + &mut config, + 1, + &mut num_config, + ) + }; + if ok == 0 || num_config == 0 { + return Err(anyhow!("eglChooseConfig 失败")); + } + + // 创建 surface + let surface = unsafe { + (gl.egl_create_window_surface)( + display, + config, + x_window as egl::NativeWindowType, + std::ptr::null(), + ) + }; + if surface.is_null() { + return Err(anyhow!("eglCreateWindowSurface 失败")); + } + + // 创建 context(GLES2) + let ctx_attrs = [egl::EGL_CONTEXT_CLIENT_VERSION, 2, egl::EGL_NONE]; + let context = unsafe { + (gl.egl_create_context)(display, config, std::ptr::null(), ctx_attrs.as_ptr()) + }; + if context.is_null() { + return Err(anyhow!("eglCreateContext 失败")); + } + + // make current + let ok = unsafe { + (gl.egl_make_current)(display, surface, surface, context) + }; + if ok == 0 { + return Err(anyhow!("eglMakeCurrent 失败")); + } + + // 设置视口 + unsafe { + (gl.gl_viewport)(0, 0, width, height); + (gl.gl_clear_color)(0.0, 0.0, 0.0, 0.0); + (gl.gl_enable)(GL_BLEND); + (gl.gl_blend_func)(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } + + // 编译着色器 + let vert = compile_shader(&gl, GL_VERTEX_SHADER, VERT_SHADER_SRC)?; + let frag = compile_shader(&gl, GL_FRAGMENT_SHADER, FRAG_SHADER_SRC)?; + let program = unsafe { + let p = (gl.gl_create_program)(); + (gl.gl_attach_shader)(p, vert); + (gl.gl_attach_shader)(p, frag); + (gl.gl_link_program)(p); + let mut status = 0; + (gl.gl_get_programiv)(p, GL_LINK_STATUS, &mut status); + if status == 0 { + let mut log = vec![0i8; 1024]; + (gl.gl_get_program_info_log)( + p, + 1024, + std::ptr::null_mut(), + log.as_mut_ptr(), + ); + let log_str = String::from_utf8_lossy( + std::slice::from_raw_parts(log.as_ptr() as *const u8, 1024), + ); + return Err(anyhow!("着色器链接失败: {}", log_str)); + } + p + }; + + unsafe { (gl.gl_use_program)(program); } + + // 获取 attribute/uniform 位置 + let attrib_position = unsafe { + let name = std::ffi::CString::new("a_position")?; + (gl.gl_get_attrib_location)(program, name.as_ptr()) + }; + let attrib_tex_coord = unsafe { + let name = std::ffi::CString::new("a_texCoord")?; + (gl.gl_get_attrib_location)(program, name.as_ptr()) + }; + let uniform_matrix = unsafe { + let name = std::ffi::CString::new("u_matrix")?; + (gl.gl_get_uniform_location)(program, name.as_ptr()) + }; + let uniform_texture = unsafe { + let name = std::ffi::CString::new("s_texture0")?; + (gl.gl_get_uniform_location)(program, name.as_ptr()) + }; + let uniform_base_color = unsafe { + let name = std::ffi::CString::new("u_baseColor")?; + (gl.gl_get_uniform_location)(program, name.as_ptr()) + }; + let uniform_multiply_color = unsafe { + let name = std::ffi::CString::new("u_multiplyColor")?; + (gl.gl_get_uniform_location)(program, name.as_ptr()) + }; + let uniform_screen_color = unsafe { + let name = std::ffi::CString::new("u_screenColor")?; + (gl.gl_get_uniform_location)(program, name.as_ptr()) + }; + + println!( + "[Live2D] GL 程序就绪: program={} pos={} tex={} mat={} tex_u={} base={} mult={} scr={}", + program, attrib_position, attrib_tex_coord, uniform_matrix, + uniform_texture, uniform_base_color, uniform_multiply_color, uniform_screen_color + ); + + Ok(Self { + gl, + display, + _config: config, + context, + surface, + x_display, + x_window, + program, + attrib_position, + attrib_tex_coord, + uniform_matrix, + uniform_texture, + uniform_base_color, + uniform_multiply_color, + uniform_screen_color, + width, + height, + }) + } + + /// 上传纹理到 GPU + pub fn create_texture(&self, img: &super::assets::TextureImage) -> GLuint { + unsafe { + let mut tex = 0; + (self.gl.gl_gen_textures)(1, &mut tex); + (self.gl.gl_bind_texture)(GL_TEXTURE_2D, tex); + (self.gl.gl_tex_parameteri)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR as i32); + (self.gl.gl_tex_parameteri)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR as i32); + (self.gl.gl_tex_parameteri)(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE as i32); + (self.gl.gl_tex_parameteri)(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE as i32); + (self.gl.gl_tex_image_2d)( + GL_TEXTURE_2D, + 0, + GL_RGBA as i32, + img.width as i32, + img.height as i32, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + img.rgba.as_ptr() as *const c_void, + ); + let err = (self.gl.gl_get_error)(); + if err != GL_NO_ERROR { + eprintln!("[Live2D] glTexImage2D 错误: 0x{:x}", err); + } + tex + } + } + + /// 交换缓冲区(显示到屏幕) + pub fn swap_buffers(&self) { + unsafe { + (self.gl.gl_clear)(GL_COLOR_BUFFER_BIT); + // 注意:clear 在绘制前调用,这里 swap 只是提交 + // 实际渲染流程:clear → draw → swap + (self.gl.egl_swap_buffers)(self.display, self.surface); + } + } + + /// 清屏 + pub fn clear(&self) { + unsafe { (self.gl.gl_clear)(GL_COLOR_BUFFER_BIT); } + } +} + +impl Drop for RenderContext { + fn drop(&mut self) { + // 简单清理(EGL/X11 资源在进程退出时自动释放) + unsafe { + (self.gl.x_flush)(self.x_display); + } + } +} + +// EGL_DEFAULT_DISPLAY 常量 +impl egl { + pub fn EGL_DEFAULT_DISPLAY_PTR() -> NativeDisplayType { + std::ptr::null_mut() + } +} diff --git a/src/plugins/live2d/mod.rs b/src/plugins/live2d/mod.rs new file mode 100644 index 0000000..eda92b5 --- /dev/null +++ b/src/plugins/live2d/mod.rs @@ -0,0 +1,19 @@ +//! Live2D 原生渲染插件(Rust + Cubism Core FFI + OpenGL ES 2.0) +//! +//! 架构: +//! - `core_ffi`:FFI 绑定 Cubism Core C API(libLive2DCubismCore.so) +//! - `model`:moc3 加载、model 管理、参数/drawable 数据提取 +//! - `assets`:model3.json 解析、PNG 纹理加载 +//! - `gl`:EGL 上下文 + OpenGL ES 2.0 着色器/渲染 +//! - `animation`:参数动画(眨眼、嘴部、待机呼吸) +//! - `renderer`:整合渲染循环 + +pub mod animation; +pub mod assets; +pub mod core_ffi; +pub mod gl; +pub mod model; +pub mod renderer; + +mod plugin; +pub use plugin::Live2DPlugin; diff --git a/src/plugins/live2d/model.rs b/src/plugins/live2d/model.rs new file mode 100644 index 0000000..39a28f7 --- /dev/null +++ b/src/plugins/live2d/model.rs @@ -0,0 +1,316 @@ +//! Cubism Model 加载与管理 +//! +//! 封装 moc3 读取 → revive → model 初始化 → 参数更新 → drawable 数据提取。 + +use super::core_ffi::{ + alloc_aligned, AlignedBuffer, CubismCore, CsmVector2, CsmVector4, CSM_ALIGNOF_MOC, + CSM_ALIGNOF_MODEL, +}; +use anyhow::{anyhow, Context, Result}; +use std::path::Path; + +/// 一个 drawable 的渲染所需数据快照 +#[derive(Clone)] +pub struct DrawableSnapshot { + pub texture_index: i32, + pub draw_order: i32, + pub render_order: i32, + pub opacity: f32, + pub blend_mode: i32, + pub constant_flags: u8, + pub dynamic_flags: u8, + pub vertex_positions: Vec<[f32; 2]>, + pub vertex_uvs: Vec<[f32; 2]>, + pub indices: Vec, + pub multiply_color: [f32; 4], + pub screen_color: [f32; 4], + pub mask_counts: i32, +} + +/// Canvas 信息 +#[derive(Clone, Copy, Debug, Default)] +pub struct CanvasInfo { + pub size: [f32; 2], + pub origin: [f32; 2], + pub pixels_per_unit: f32, +} + +/// 参数信息 +pub struct ParameterInfo { + pub id: String, + pub min_value: f32, + pub max_value: f32, + pub default_value: f32, +} + +/// 活跃的 Cubism Model +pub struct CubismModel { + /// Core 句柄(保持引用防止 .so 卸载) + _core: std::sync::Arc, + /// moc3 原始数据(对齐 64) + _moc_buf: AlignedBuffer, + /// moc 指针 + moc: *mut std::ffi::c_void, + /// model 内存(对齐 16) + _model_buf: AlignedBuffer, + /// model 指针 + model: *mut std::ffi::c_void, + /// 参数数量 + pub param_count: usize, + /// 参数 ID 列表(与 Core 内部顺序一致) + pub param_ids: Vec, + /// 参数索引映射(id → index) + pub param_index: std::collections::HashMap, + /// drawable 数量 + pub drawable_count: usize, + /// canvas 信息 + pub canvas: CanvasInfo, +} + +// model 指针可跨线程发送(渲染线程独占使用) +unsafe impl Send for CubismModel {} + +impl CubismModel { + /// 从 moc3 文件加载 model + pub fn load(core: std::sync::Arc, moc3_path: &Path) -> Result { + let moc_data = std::fs::read(moc3_path) + .with_context(|| format!("读取 moc3 失败: {}", moc3_path.display()))?; + + // 分配 64 对齐内存 + let mut moc_buf = alloc_aligned(moc_data.len(), CSM_ALIGNOF_MOC)?; + unsafe { + std::ptr::copy_nonoverlapping(moc_data.as_ptr(), moc_buf.as_ptr(), moc_data.len()); + } + + // 检查一致性 + let valid = unsafe { + (core.csm_has_moc_consistency)(moc_buf.as_mut_void(), moc_data.len() as u32) + }; + if valid != 1 { + return Err(anyhow!("moc3 一致性检查失败(文件损坏或版本不支持)")); + } + + // Revive moc + let moc = unsafe { + (core.csm_revive_moc_in_place)(moc_buf.as_mut_void(), moc_data.len() as u32) + }; + if moc.is_null() { + return Err(anyhow!("csmReviveMocInPlace 返回空指针")); + } + + // 获取 model 所需大小 + let model_size = unsafe { (core.csm_get_sizeof_model)(moc) }; + if model_size == 0 { + return Err(anyhow!("csmGetSizeofModel 返回 0")); + } + + // 分配 16 对齐内存 + let mut model_buf = alloc_aligned(model_size as usize, CSM_ALIGNOF_MODEL)?; + let model = unsafe { + (core.csm_initialize_model_in_place)( + moc, + model_buf.as_mut_void(), + model_size, + ) + }; + if model.is_null() { + return Err(anyhow!("csmInitializeModelInPlace 返回空指针")); + } + + // 读取参数信息 + let param_count = unsafe { (core.csm_get_parameter_count)(model) } as usize; + let param_ids_ptr = unsafe { (core.csm_get_parameter_ids)(model) }; + let mut param_ids = Vec::with_capacity(param_count); + let mut param_index = std::collections::HashMap::new(); + for i in 0..param_count { + let id = unsafe { + super::core_ffi::cstr_ptr_to_string(*param_ids_ptr.add(i)) + .unwrap_or_default() + }; + param_index.insert(id.clone(), i); + param_ids.push(id); + } + + // 读取 drawable 数量 + let drawable_count = unsafe { (core.csm_get_drawable_count)(model) } as usize; + + // 读取 canvas 信息 + let mut size = CsmVector2::default(); + let mut origin = CsmVector2::default(); + let mut ppu = 0.0f32; + unsafe { + (core.csm_read_canvas_info)(model, &mut size, &mut origin, &mut ppu); + } + + let canvas = CanvasInfo { + size: [size.x, size.y], + origin: [origin.x, origin.y], + pixels_per_unit: ppu, + }; + + println!( + "[Live2D] 模型加载成功: 参数={} drawable={} canvas={:?}x{:.0}ppu", + param_count, drawable_count, canvas.size, canvas.pixels_per_unit + ); + + Ok(Self { + _core: core, + _moc_buf: moc_buf, + moc, + _model_buf: model_buf, + model, + param_count, + param_ids, + param_index, + drawable_count, + canvas, + }) + } + + /// 获取参数最小值 + pub fn param_min_values(&self, core: &CubismCore) -> Vec { + unsafe { + let p = (core.csm_get_parameter_minimum_values)(self.model); + std::slice::from_raw_parts(p, self.param_count).to_vec() + } + } + + /// 获取参数最大值 + pub fn param_max_values(&self, core: &CubismCore) -> Vec { + unsafe { + let p = (core.csm_get_parameter_maximum_values)(self.model); + std::slice::from_raw_parts(p, self.param_count).to_vec() + } + } + + /// 获取参数默认值 + pub fn param_default_values(&self, core: &CubismCore) -> Vec { + unsafe { + let p = (core.csm_get_parameter_default_values)(self.model); + std::slice::from_raw_parts(p, self.param_count).to_vec() + } + } + + /// 获取参数值缓冲区(可读写) + pub fn param_values_mut(&self, core: &CubismCore) -> &mut [f32] { + unsafe { + let p = (core.csm_get_parameter_values)(self.model); + std::slice::from_raw_parts_mut(p, self.param_count) + } + } + + /// 按参数 ID 设置值 + pub fn set_param(&self, core: &CubismCore, id: &str, value: f32) -> bool { + if let Some(&idx) = self.param_index.get(id) { + let slice = self.param_values_mut(core); + slice[idx] = value; + true + } else { + false + } + } + + /// 按参数 ID 获取值 + pub fn get_param(&self, core: &CubismCore, id: &str) -> Option { + let idx = *self.param_index.get(id)?; + let slice = self.param_values_mut(core); + Some(slice[idx]) + } + + /// 将所有参数重置为默认值 + pub fn reset_to_default(&self, core: &CubismCore) { + let defaults = self.param_default_values(core); + let slice = self.param_values_mut(core); + for (i, v) in defaults.into_iter().enumerate() { + slice[i] = v; + } + } + + /// 更新 model(应用参数变化,重新计算顶点/不透明度等) + pub fn update(&self, core: &CubismCore) { + unsafe { (core.csm_update_model)(self.model) }; + } + + /// 重置动态标志 + pub fn reset_dynamic_flags(&self, core: &CubismCore) { + unsafe { (core.csm_reset_drawable_dynamic_flags)(self.model) }; + } + + /// 获取所有 drawable 的渲染数据快照(按 render order 排序) + pub fn drawable_snapshots(&self, core: &CubismCore) -> Vec { + let count = self.drawable_count; + unsafe { + let render_orders = (core.csm_get_render_orders)(self.model); + let texture_indices = (core.csm_get_drawable_texture_indices)(self.model); + let draw_orders = (core.csm_get_drawable_draw_orders)(self.model); + let opacities = (core.csm_get_drawable_opacities)(self.model); + let blend_modes = (core.csm_get_drawable_blend_modes)(self.model); + let const_flags = (core.csm_get_drawable_constant_flags)(self.model); + let dyn_flags = (core.csm_get_drawable_dynamic_flags)(self.model); + let mask_counts = (core.csm_get_drawable_mask_counts)(self.model); + let vertex_counts = (core.csm_get_drawable_vertex_counts)(self.model); + let vertex_positions = (core.csm_get_drawable_vertex_positions)(self.model); + let vertex_uvs = (core.csm_get_drawable_vertex_uvs)(self.model); + let index_counts = (core.csm_get_drawable_index_counts)(self.model); + let indices = (core.csm_get_drawable_indices)(self.model); + let multiply_colors = (core.csm_get_drawable_multiply_colors)(self.model); + let screen_colors = (core.csm_get_drawable_screen_colors)(self.model); + + // 先收集每个 drawable 的原始索引 + let mut items: Vec<(usize, i32)> = (0..count) + .map(|i| (i, *render_orders.add(i))) + .collect(); + // 按 render_order 升序排列 + items.sort_by_key(|&(_, ro)| ro); + + items + .into_iter() + .map(|(i, ro)| { + let vc = *vertex_counts.add(i) as usize; + let ic = *index_counts.add(i) as usize; + let vp = *vertex_positions.add(i); + let vu = *vertex_uvs.add(i); + let idx = *indices.add(i); + + let mut verts = Vec::with_capacity(vc); + let mut uvs = Vec::with_capacity(vc); + for v in 0..vc { + let p = *vp.add(v); + let u = *vu.add(v); + verts.push([p.x, p.y]); + uvs.push([u.x, u.y]); + } + let mut idx_vec = Vec::with_capacity(ic); + for k in 0..ic { + idx_vec.push(*idx.add(k)); + } + + let mc = *multiply_colors.add(i); + let sc = *screen_colors.add(i); + + DrawableSnapshot { + texture_index: *texture_indices.add(i), + draw_order: *draw_orders.add(i), + render_order: ro, + opacity: *opacities.add(i), + blend_mode: *blend_modes.add(i), + constant_flags: *const_flags.add(i), + dynamic_flags: *dyn_flags.add(i), + vertex_positions: verts, + vertex_uvs: uvs, + indices: idx_vec, + multiply_color: [mc.x, mc.y, mc.z, mc.w], + screen_color: [sc.x, sc.y, sc.z, sc.w], + mask_counts: *mask_counts.add(i), + } + }) + .collect() + } + } +} + +impl CubismModel { + // 保留 CsmVector4 引用避免 unused + #[allow(dead_code)] + fn _ensure_v4_linked(_v: CsmVector4) {} +} diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index c87a006..eec5f94 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -2,6 +2,7 @@ pub mod ai; pub mod ble; pub mod device; pub mod http; +pub mod live2d; pub mod screen; pub mod video; pub mod wifi;