桌面应用【免费下载链接】BongoCat BongoCat — A cross-platform interactive desktop pet that brings fun to your desktop!项目地址https://gitcode.com/gh_mirrors/bong/BongoCat点击查看免费下载本文基于 docs/adr/0062-live2d-render-resource-boundary.md 展开讲解 BongoCat 将模型包 → 渲染资源的准备层从 Cubism Core 耦合中剥离出来的架构决策。读者将掌握bongocat-live2d-render的职责划分、键位图清单与候选词表的工作原理、错误映射链路以及它如何让键位绑定与渲染绘制共享同一事实来源。背景为什么需要一条独立的渲染资源准备边界在 ADR-0062 之前bongocat-live2d把两件完全不同的事情揉在了一起一是 Cubism Core 的所有权模型、Moc、Core 参数 ID、drawable 快照二是模型包渲染资源的准备工作键位图清点、背景图发现、PNG 尺寸校验、按键覆盖层解析。这两类工作的运行前提截然不同Core 相关的工作需要已提交的模型CommittedModel、Moc、Core 参数 ID 与 drawable 快照必须编译并链接供应商 SDKCubism Core纯 CPU 的渲染资源工作只需要已提交的模型包和bongocat-render中不可变的资源类型——键位图清单、背景图发现、PNG 尺寸检查、按键覆盖层解析这些都不需要 Core也不需要 GPU。把 CPU 资源工作留在bongocat-live2d里带来两个直接后果ADR-0062 Context 部分应用bongocat-app和运行时bongocat-runtime为了拿到键位图契约不得不间接依赖 Core crate想要测试模型包到渲染资源这一边界就得先编译整个供应商 SDK。决策新建 bongocat-live2d-render crateADR-0062 的决策是创建一个平台无关的模型到渲染资源准备层即bongocat-live2d-render。它拥有以下职责从CommittedModel的索引构造RenderResources背景图与键位图的发现、常规文件过滤与 PNG 尺寸校验KeyImageInventory按手组织的 HID 到美术资源的候选词表与resolve_key_overlays一个轻量的资源准备错误类型由 Core 适配层映射到既有的Live2dErrorCode::ResourceIo。该 crate 只依赖bongocat-model、bongocat-render、image和标准库不依赖 Cubism Core、bongocat-live2d、runtime、GPUI、平台 API 或任何 GPU 后端见 crates/bongocat-live2d-render/Cargo.toml。它也不决定输入绑定、不改变运行时状态应用在构造绑定时应用清单运行时只消费最终不可变的RenderSnapshot。bongocat-live2d-render - bongocat-model / bongocat-render / image / std bongocat-live2d -------- bongocat-live2d-render模型加载时调用 prepare_render_resources bongocat-app / runtime - bongocat-live2d-render直接导入清单与覆盖层解析器RenderResources 的构造从模型索引到不可变资源入口函数是 crates/bongocat-live2d-render/src/lib.rs 中的prepare_render_resourcespub fn prepare_render_resources( model: CommittedModel, ) - ResultRenderResources, RenderResourceError { let textures model .index() .textures .iter() .enumerate() .map(|(index, texture)| TextureAsset { id: TextureId::new(index), path: model.root().join(texture.file), width: texture.width, height: texture.height, }) .collect(); Ok(RenderResources { textures, key_assets: load_key_assets(model.root())?, background: load_background_asset(model.root())?, }) }它一次性完成三件事纹理资产按模型索引中的声明顺序生成TextureAssetTextureId::new(index)以索引编号路径基于model.root()拼接键位图资产调用load_key_assets扫描并解码键位图背景资产调用load_background_asset处理可选的背景图。RenderResources本身定义在 crates/bongocat-render/src/resources.rs由三部分组成pub struct RenderResources { pub textures: VecTextureAsset, pub key_assets: VecKeyAsset, pub background: OptionBackgroundAsset, }TextureAsset、KeyAsset、BackgroundAsset都携带path、width、height键位图还有id、side、name全部是已校验过的不可变数据。这正是 crates/bongocat-render/src/lib.rs 的定位——命名路径、加载时校验一次快照中引用一个资源里不存在的纹理会在校验步骤被拒绝而不是在绘制调用时越界。键位图与背景图的发现规则目录扫描的唯一事实来源key_image_files是清单与加载器共享的私有扫描函数crates/bongocat-live2d-render/src/lib.rsfn key_image_files(root: Path) - Vec(KeySide, String, PathBuf) { let mut images Vec::new(); for (side, directory) in [(KeySide::Left, left-keys), (KeySide::Right, right-keys)] { let path root.join(resources).join(directory); let Ok(entries) fs::read_dir(path) else { continue; }; let mut files entries .filter_map(Result::ok) .map(|entry| entry.path()) .filter(|file| { file.is_file() file .extension() .is_some_and(|extension| extension.eq_ignore_ascii_case(png)) }) .collect::Vec_(); files.sort(); for file in files { let Some(name) file.file_stem().and_then(|name| name.to_str()) else { continue; }; images.push((side, name.to_owned(), file)); } } images }规则非常明确只有resources/left-keys与resources/right-keys目录下的常规.png文件才算数扩展名大小写不敏感资源名取文件名主干file stem目录缺失时贡献为空集。左右键位图目录按顺序扫描、各自按路径排序这是加载器消费的固定顺序。加载与尺寸校验load_key_assets对每个扫描到的文件用ImageReader打开并解码产出KeyAssetload_background_asset则只认resources/background.png这一个路径文件不存在时返回Ok(None)pub fn load_background_asset(root: Path) - ResultOptionBackgroundAsset, RenderResourceError { let path root.join(resources/background.png); if !path.is_file() { return Ok(None); } let image ImageReader::open(path) .map_err(|error| RenderResourceError::new(error.to_string()))? .decode() .map_err(|error| RenderResourceError::new(error.to_string()))?; Ok(Some(BackgroundAsset { path, width: image.width(), height: image.height(), })) }任何解码失败都以RenderResourceError形式返回——这是本 crate 唯一的错误类型且只携带一段字符串描述。清单读取不解码图片与加载器相对的是KeyImageInventory::read它只做目录扫描、不解码任何图片因此每次模型激活时构建一次的成本不超过渲染器本来就要做的目录列举。ADR-0042 指出正是这个清单说有的与渲染加载到的共用同一扫描函数的设计保证了二者不可能漂移。KeyImageInventory绑定层与渲染层共享的能否绘制答案KeyImageInventory按手left/right各持有一个BTreeSetString提供四个核心查询provides(side, name)某一手是否提供该名字的图can_draw(side, hid_usage)键盘键在这一手能否画出来can_draw_key(side, key)针对KeyIdentity键盘或手柄的统一回答names(side)某一手可用的图片名集合。其中can_draw_key的判定逻辑是pub fn can_draw_key(self, side: KeySide, key: KeyIdentity) - bool { key_image_name_candidates(key) .iter() .any(|name| self.provides(side, name)) }也就是说能画的判断走的是与resolve_key_overlays完全相同的候选顺序——候选名中的任何一个存在于该手目录中即为可绘制。这正是 ADR-0042 决策 5 的落点可绘制与实际绘制必须同源否则会出现以为画得出来却没画或画得出来却不响应。候选词表一个键可以对应多张图键盘键的候选顺序key_name_candidates(hid_usage)crates/bongocat-live2d-render/src/lib.rs为每个 HID usage 生成按优先级排列的候选名列表。其设计要点包括精确名优先字母区KeyA…KeyZ、数字区Num1…Num0、标点区、导航区、修饰键ControlLeft/ShiftLeft/AltLeft/MetaLeft及右侧对应都有精确名功能键回退F1…F24 优先匹配专属F1.png…F24.png缺失时回退到共享的Fn.png——Fn是共享功能行图片的名字不是 Fn 键本身。物理 Fn / globe 键是另一个键Globe两者不共享任何候选小键盘回退Kp1…Kp9回退到Num1…Num9Kp0回退到Num0KpEnter回退到EnterKpDivide回退到SlashKpMinus回退到MinusKpDecimal回退到DotNumLock、KpMultiply、KpPlus没有主键盘对应键*和只能通过Shift触达只保留精确名旧名别名AltGr右 Alt 旧名仅右 Alt 使用、Return主 Enter 旧拼写排在规范Enter之后、BackslashBackSlash的旧拼写、Functionglobe 键在旧rdev输入层的名字排在Globe之后——这些别名保证未经过导入规范化处理的存量模型包仍能绘制家族共享图四个修饰键对在精确名之后回退到共享的Control、Shift、Alt、Meta。整个词表覆盖了标准 104/105 键布局加小键盘而不仅限于预置模型实际绘制的那批键。ADR-0062 沿用了 ADR-0041 确立的原则名字是与模型作者的契约——Dot.png、Minus.png、Insert.png今天没有预置模型携带但任何提供了它们的模型都必须在不改产品代码的情况下被正确绘制。手柄键的候选单一名字、无回退手柄按钮走gamepad_key_name_candidates只返回GamepadButton::key_image_name()一个名字没有任何回退。原因写在源码注释里手柄按钮没有可回退的对应键也没有可共享的家族图——把 D-pad 的图借给模型单独画的一个按钮比什么都不画更糟。值得注意的边界旧gilrs派生拼写DPadUp、LeftTrigger2、LeftThumb以及含义曾是肩键的LeftTrigger不是候选。肩键与模拟扳机是两个不同的按钮旧拼写下肩键已经占用了LeftTrigger这个名字别名列表会把其中一个按钮解析成另一个的图。这些主干名由模型仓库bongocat-model-store::key_names在导入时重写那是包自身文件唯一可以被改动的地方。resolve_key_overlays从按键集合到覆盖层resolve_key_overlays把一次KeyPressSet最多 64 个按键、去重、有界解析为要绘制的覆盖层列表pub fn resolve_key_overlays(resources: RenderResources, presses: KeyPressSet) - VecKeyOverlay { let mut selected [None, None]; for press in presses.iter() { let side_index match press.side { KeySide::Left 0, KeySide::Right 1, }; // 先清空该侧槽位当前键不可用时绝不复用该侧更早的覆盖层 selected[side_index] None; let candidates key_image_name_candidates(press.key); let Some(asset) candidates.iter().find_map(|candidate| { resources .key_assets .iter() .find(|asset| asset.side press.side asset.name *candidate) }) else { continue; }; selected[side_index] Some(KeyOverlay { asset_id: asset.id, side: press.side, }); } selected.into_iter().flatten().collect() }关键行为每一侧只保留最近一次按下的键的覆盖层且按下新键前先清空槽位保证当前键不可用时不会错误复用该侧更早的覆盖层。按侧side严格匹配资产同侧缺失不绘制——这是 ADR-0040/0042 一直保持的 side 严格性契约。测试 crates/bongocat-live2d-render/src/lib.rs 中的key_overlay_resolution_uses_the_render_resource_contract验证了这一点左手按KeyA0x04、右手按右 Meta0xe7分别解析出KeyA与共享Meta资产。the_shipped_gamepad_model_draws_each_button_it_ships则逐按钮断言预置 gamepad 模型的手柄按钮都能解析出恰好一个覆盖层且资产名、side 与按钮语义完全一致a_gamepad_button_with_no_artwork_draws_nothing断言Select、Start、双摇杆在无图时解析结果为空——缺图即无动作而不是借用别的按钮的图。错误映射RenderResourceError 到 Live2dErrorCode::ResourceIobongocat-live2d-render只定义了一个轻量错误RenderResourceError内部就是一段字符串它自身不依赖任何 Live2D 错误体系。映射发生在 Core 适配层bongocat-live2d在Live2dModel::load里调用prepare_render_resources并把失败映射为既有稳定错误码let resources Arc::new(prepare_render_resources(model).map_err( |error: RenderResourceError| { Live2dError::new(Live2dErrorCode::ResourceIo, error.to_string()) }, )?);见 crates/bongocat-live2d/src/lib.rs。这样资源准备失败这一类错误在稳定的Live2dErrorCode目录中有了统一出口同时资源准备层本身保持与错误体系、与 Cubism SDK 的双重解耦。应用与运行时的消费方式绑定由清单收窄ADR-0062 明确bongocat-app与bongocat-runtime直接导入清单和覆盖层解析器不再通过 Core crate 间接获取这些契约。应用侧的核心消费逻辑在 crates/bongocat-app/src/model_input.rs 的input_bindings_for_model写入 runtime 的每模型绑定 静态 hand 表 ∩ 该模型的键位图KeyImageInventory::read(model.root())在模型激活时构建input_bindings_for_committed_model随后bind_drawable_key对静态 hand 表中的每个键做can_draw检查只有模型真能画出来的键才写入InputBindings。这一检查发生在按键到达模型之前而不是渲染器里——渲染器只消费不可变的RenderSnapshot不允许决定动作。gamepad_hands_for_model同样以清单为准按钮放在left-keys就是左爪绘制、放在right-keys就是右爪绘制没有美术的按钮保持未绑定预置模型的 L3/R3 摇杆按下只移动摇杆美术不产生按键动作。运行时侧bongocat-runtime的渲染模块crates/bongocat-runtime/src/rendering/只消费不可变的RenderSnapshot与RenderResources不感知资源准备细节。后果与验证ADR-0062 记录的直接后果模型资源准备与键位图契约可以在不编译 Cubism SDK 的情况下测试bongocat-live2d不再拥有图片解码器与键位图目录扫描app/runtime 不再通过 Core crate 间接获取这些契约缺图即无绑定行为与按侧覆盖层规则保持不变没有引入新的渲染后端、窗口所有者或 GPU 生命周期。这些后果在代码中可验证。集成测试key_image_inventory_lists_exactly_the_assets_the_renderer_loadscrates/bongocat-live2d/src/tests/vocabulary.rs对三个预置模型断言清单与load_key_assets加载到的(side, name)集合逐侧相等应用侧测试keyboard_models_bind_every_drawable_key_of_the_standard_layout与a_key_the_active_model_cannot_draw_never_moves_the_pawcrates/bongocat-app/src/tests/model_input.rs验证了绑定 静态表 ∩ 键位图以及按下无图键如.既不驱动CatParamLeftHandDown/CatParamRightHandDown也不产生按键 press。被否决的备选方案ADR-0062 记录了几条被拒绝的路线每一条都有明确理由把整个bongocat-overlay搬进新 crate否决因为其平台文件还拥有窗口、输入路由、放置与关停拆分会需要单独的平台接缝把 PNG 解码搬进bongocat-render否决因为该 crate 是不可变契约必须保持与文件系统/图片细节无关让渲染器决定某个键是否有绑定被 ADR-0042 否决应用使用共享清单契约来门禁绑定在 app 和 runtime 里各自重写键位图扫描否决清单与加载器必须保持单一事实来源。配合 docs/adr/0060-model-package-store-boundary.md模型包与存储边界与 docs/adr/0061-live2d-playback-boundary.mdLive2D 播放边界可以看出这三条 ADR 构成了同一轮解耦的完整图景bongocat-model只管只读模型域、bongocat-model-store管可变持久化、bongocat-live2d-playback管纯数值播放、bongocat-live2d-render管纯 CPU 渲染资源准备、bongocat-live2d保留 Core 生命周期与参数写回、bongocat-render保持不可变契约。每一层都按需要编译什么依赖来划分纯逻辑层不再被迫链接供应商 SDK 或 GPU 后端。赞分享桌面应用【免费下载链接】BongoCat BongoCat — A cross-platform interactive desktop pet that brings fun to your desktop!项目地址https://gitcode.com/gh_mirrors/bong/BongoCat点击查看免费下载相关推荐BongoCat 平台无关输入契约ADR-0058bongocat-input crate 的边界设计、可靠事件传输与实现剖析BongoCat 平台无关输入契约ADR 0058bongocat input crate 的边界设计、可靠事件传输与实现剖析 导读本文围绕 Bongo桌面应用深入解读 Live2D Cubism Core 版本演进与 APIawesome-digital-human-live2d 中 Live2D 渲染核心的技术脉络深入解读 Live2D Cubism Core 版本演进与 APIawesome digital human live2d 中 Live2D 渲染核心的技术脉人工智能AI 应用数字人语音AI Agent交互助手深入解析 awesome-digital-human-live2d 中的 Live2D Cubism Web Framework模型渲染、动画与工程构建全指南深入解析 awesome digital human live2d 中的 Live2D Cubism Web Framework模型渲染、动画与工程构建全指南人工智能AI 应用数字人语音AI Agent交互助手上一篇PaddleSpeech 源码解析Transformer DecoderLayer 解码器层设计与增量解码实现下一篇Longhorn 实例管理器整合aio 类型全解析从 engine/replica 双 Pod 到 All-in-One 的架构演进与实践创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考