perf: 优化实例启动性能并修复过渡动画对齐

依赖校验戳缓存(18.6s→0.1s)、并发化库/资产校验、启动计时埋点、过渡动画200ms、窗口客户区精确对齐、遮罩即时显示、debug构建写文件日志。
This commit is contained in:
Xiao-no-love
2026-09-16 13:10:37 +08:00
parent 776f621436
commit ccb921f4bd
7 changed files with 457 additions and 535 deletions

View File

@ -249,10 +249,16 @@ impl LightweightMode {
}
None if payload.event == "launched" => {
let app = app.clone();
// 最大化游戏窗口会轮询等待 MC 窗口出现(最多数秒)。它必须
// 与过渡动画并行,否则遮罩要等它跑完才出现,用户会看到一段
// 毫无反馈的空白期。
if payload.maximize_window {
let pid = payload.pid;
tauri::async_runtime::spawn(async move {
maximize_minecraft_window(pid).await;
});
}
tauri::async_runtime::spawn(async move {
if payload.maximize_window {
maximize_minecraft_window(payload.pid).await;
}
// 窗口过渡:把启动器全屏遮罩 → 缩放 → 淡出 → 最小化
crate::mc_transition::run(
&app,

View File

@ -1,7 +1,7 @@
//! Minecraft 启动时的窗口过渡动画(仅 Windows 生效)。
//!
//! 流程(四阶段):
//! 阶段A启动器放大到全屏1s 动画),全程置顶 + 聚焦;
//! 阶段A启动器放大到全屏0.2s 动画),全程置顶 + 聚焦;
//! 阶段B等待 MC 窗口出现(最多 60s期间启动器盖住游戏
//! 阶段C获取 MC 窗口位置/大小1s 动画缩放到相同大小;
//! 阶段D大小一致后0.5s 淡出(不与缩放同步);
@ -22,9 +22,9 @@ pub const MAIN_WINDOW_LABEL: &str = "main";
/// 等待 MC 窗口出现的轮询次数与间隔120 × 500ms = 60s
const FIND_RETRIES: u32 = 120;
const FIND_INTERVAL_MS: u64 = 500;
/// 缩放动画总时长与帧数(1000ms / 50 帧 = 20ms 一帧)。
const ANIM_DURATION_MS: u64 = 400;
const ANIM_STEPS: u32 = 50;
/// 缩放动画总时长与帧数(200ms / 25 帧 = 8ms 一帧)。
const ANIM_DURATION_MS: u64 = 200;
const ANIM_STEPS: u32 = 25;
/// 等待前端完成 CSS 淡出的超时兜底(毫秒)。淡出 500ms留足余量。
const FADE_TIMEOUT_MS: u64 = 700;
@ -48,26 +48,11 @@ static LOGO_OUT_DONE: std::sync::Mutex<
Option<tokio::sync::oneshot::Sender<()>>,
> = std::sync::Mutex::new(None);
/// 调试日志:直接追加到用户目录下固定文件,绕过 tracing 配置。
/// TODO(上线前清理):连同所有 `dbg(...)` 调用一起删除。
fn dbg(msg: &str) {
use std::io::Write;
let base =
std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\".to_string());
let path = format!("{base}\\mc_transition_debug.log");
if let Ok(mut f) =
std::fs::OpenOptions::new().create(true).append(true).open(path)
{
let _ = writeln!(f, "{msg}");
}
}
/// 前端完成淡出动画后的回调命令。
#[tauri::command]
pub fn mc_transition_fade_done() {
#[cfg(target_os = "windows")]
{
dbg("fade_done callback");
if let Ok(mut guard) = FADE_DONE.lock()
&& let Some(tx) = guard.take()
{
@ -81,7 +66,6 @@ pub fn mc_transition_fade_done() {
pub fn mc_transition_cover_ready() {
#[cfg(target_os = "windows")]
{
dbg("cover_ready callback");
if let Ok(mut guard) = COVER_DONE.lock()
&& let Some(tx) = guard.take()
{
@ -95,7 +79,6 @@ pub fn mc_transition_cover_ready() {
pub fn mc_transition_logo_in_done() {
#[cfg(target_os = "windows")]
{
dbg("logo_in_done callback");
if let Ok(mut guard) = LOGO_IN_DONE.lock()
&& let Some(tx) = guard.take()
{
@ -109,7 +92,6 @@ pub fn mc_transition_logo_in_done() {
pub fn mc_transition_logo_out_done() {
#[cfg(target_os = "windows")]
{
dbg("logo_out_done callback");
if let Ok(mut guard) = LOGO_OUT_DONE.lock()
&& let Some(tx) = guard.take()
{
@ -132,13 +114,12 @@ fn ease_in_out(t: f64) -> f64 {
/// 若起止相同则直接返回;结束时补一帧精确坐标,避免累积误差。
#[cfg(target_os = "windows")]
async fn animate_resize(
window: &WebviewWindow,
_window: &WebviewWindow,
hwnd: usize,
from: (i32, i32, u32, u32),
to: (i32, i32, u32, u32),
duration_ms: u64,
) {
use tauri::{PhysicalPosition, PhysicalSize};
if from == to {
return;
}
@ -150,37 +131,70 @@ async fn animate_resize(
let y = from.1 as f64 + (to.1 - from.1) as f64 * e;
let w = from.2 as f64 + (to.2 as i64 - from.2 as i64) as f64 * e;
let h = from.3 as f64 + (to.3 as i64 - from.3 as i64) as f64 * e;
let _ =
window.set_position(PhysicalPosition::new(x as i32, y as i32));
let _ = window.set_size(PhysicalSize::new(
w.max(1.0) as u32,
h.max(1.0) as u32,
));
// 单次 SetWindowPos移动+缩放一把过)比重绘两次的
// set_position/set_size 更顺滑,也避免无边框窗口的
// 不可见 resize border 造成的偏移。
win_impl::set_client_rect(
hwnd,
x as i32,
y as i32,
w.max(1.0) as i32,
h.max(1.0) as i32,
);
tokio::time::sleep(std::time::Duration::from_millis(step_ms)).await;
}
// 补最后一帧精确值
let _ = window.set_position(PhysicalPosition::new(to.0, to.1));
let _ = window.set_size(PhysicalSize::new(to.2.max(1), to.3.max(1)));
// 补最后一帧精确值(同样只设可见客户区,避免无边框边框造成偏移)
win_impl::set_client_rect(
hwnd,
to.0,
to.1,
to.2.max(1) as i32,
to.3.max(1) as i32,
);
}
pub async fn run(app: &AppHandle, pid: u32, _maximize: bool) {
if pid == 0 {
dbg("run() aborted: pid=0");
return;
}
let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) else {
dbg("run() aborted: main window not found");
return;
};
dbg(&format!("run() invoked pid={pid}"));
// 记录启动器当前矩形作为起点
#[cfg(target_os = "windows")]
let hwnd_raw = window.hwnd().map(|h| h.0 as usize).unwrap_or(0);
#[cfg(not(target_os = "windows"))]
let hwnd_raw = 0usize;
// 记录启动器当前「可见客户区」作为起点(与最终对齐用的是同一套
// 坐标系,避免无边框窗口边框带来的偏移累积)。
let start = {
let p = window.outer_position().map(|p| (p.x, p.y)).unwrap_or((0, 0));
let s =
window.outer_size().map(|s| (s.width, s.height)).unwrap_or((1280, 800));
(p.0, p.1, s.0, s.1)
#[cfg(target_os = "windows")]
{
win_impl::get_client_rect(hwnd_raw).unwrap_or_else(|| {
let p = window
.outer_position()
.map(|p| (p.x, p.y))
.unwrap_or((0, 0));
let s = window
.outer_size()
.map(|s| (s.width, s.height))
.unwrap_or((1280, 800));
(p.0, p.1, s.0, s.1)
})
}
#[cfg(not(target_os = "windows"))]
{
let p = window
.outer_position()
.map(|p| (p.x, p.y))
.unwrap_or((0, 0));
let s = window
.outer_size()
.map(|s| (s.width, s.height))
.unwrap_or((1280, 800));
(p.0, p.1, s.0, s.1)
}
};
dbg(&format!("launcher start rect = {start:?}"));
// 全屏目标 = 启动器当前所在显示器的完整矩形
let fullscreen = match window
@ -193,88 +207,74 @@ pub async fn run(app: &AppHandle, pid: u32, _maximize: bool) {
(m.position().x, m.position().y, m.size().width, m.size().height)
}
None => {
dbg("no monitor info, abort");
return;
}
};
dbg(&format!("fullscreen target = {fullscreen:?}"));
// 窗口可能处于最小化/隐藏(轻量模式遗留),先恢复可见再动画,
// 否则遮罩虽然在 DOM 里建好,用户却看不到。
let _ = window.show();
let _ = window.unminimize();
// 全程置顶 + 聚焦
let _ = window.set_always_on_top(true);
let _ = window.set_focus();
// ---------- 第①步:显示纯色遮罩(无图,主题色) ----------
dbg("step 1: show plain cover");
let (cover_tx, cover_rx) = tokio::sync::oneshot::channel::<()>();
if let Ok(mut guard) = COVER_DONE.lock() {
*guard = Some(cover_tx);
}
let _ = app.emit("mc-transition-cover-show", ());
let covered = tokio::time::timeout(
let _ = tokio::time::timeout(
std::time::Duration::from_millis(1000),
cover_rx,
)
.await;
dbg(&format!("step 1: cover ready ok={}", covered.is_ok()));
// ---------- 第②步:放大到全屏(1s ----------
dbg("step 2: expand to fullscreen");
animate_resize(&window, start, fullscreen, ANIM_DURATION_MS).await;
dbg("step 2 done");
// ---------- 第②步:放大到全屏(0.2s ----------
animate_resize(&window, hwnd_raw, start, fullscreen, ANIM_DURATION_MS)
.await;
// ---------- 第③步:全屏后,渐显 SVG logo ----------
dbg("step 3: fade in logo");
let (lin_tx, lin_rx) = tokio::sync::oneshot::channel::<()>();
if let Ok(mut guard) = LOGO_IN_DONE.lock() {
*guard = Some(lin_tx);
}
let _ = app.emit("mc-transition-logo-in", ());
let logo_in = tokio::time::timeout(
let _ = tokio::time::timeout(
std::time::Duration::from_millis(1500),
lin_rx,
)
.await;
dbg(&format!("step 3: logo in done ok={}", logo_in.is_ok()));
// ---------- 等待 MC 窗口出现(最多 60s ----------
dbg("waiting for MC window");
let mut mc_raw: usize = 0;
for i in 0..FIND_RETRIES {
for _ in 0..FIND_RETRIES {
if let Some(raw) = win_impl::find_window(pid) {
mc_raw = raw;
dbg(&format!("MC window found at try#{i} hwnd={raw:#x}"));
break;
}
if i % 4 == 0 {
dbg(&format!(
"try#{i} pid={pid} candidates: {:?}",
win_impl::dump_pid_windows(pid)
));
}
tokio::time::sleep(std::time::Duration::from_millis(FIND_INTERVAL_MS))
.await;
}
if mc_raw == 0 {
dbg("MC window NOT FOUND -> restore launcher");
let _ = window.set_always_on_top(false);
animate_resize(&window, fullscreen, start, 300).await;
animate_resize(&window, hwnd_raw, fullscreen, start, 300).await;
let _ = app.emit("mc-transition-reset-opacity", ());
return;
}
// ---------- 第④步:缩小前,背景变 Mojang 红 + 渐隐 logo ----------
dbg("step 4: turn red + fade out logo");
let (lout_tx, lout_rx) = tokio::sync::oneshot::channel::<()>();
if let Ok(mut guard) = LOGO_OUT_DONE.lock() {
*guard = Some(lout_tx);
}
let _ = app.emit("mc-transition-logo-out", ());
let logo_out = tokio::time::timeout(
let _ = tokio::time::timeout(
std::time::Duration::from_millis(1500),
lout_rx,
)
.await;
dbg(&format!("step 4: logo out done ok={}", logo_out.is_ok()));
// ---------- 第⑤步:缩小到 MC 窗口矩形1s ----------
let mc_rect = win_impl::get_rect(mc_raw).unwrap_or((
@ -283,8 +283,6 @@ pub async fn run(app: &AppHandle, pid: u32, _maximize: bool) {
fullscreen.2 as i32,
fullscreen.3 as i32,
));
let covers = win_impl::covers_monitor(mc_raw);
dbg(&format!("step 5: MC rect={mc_rect:?} covers_monitor={covers}"));
let target = (
mc_rect.0,
mc_rect.1,
@ -293,36 +291,30 @@ pub async fn run(app: &AppHandle, pid: u32, _maximize: bool) {
);
let _ = window.set_always_on_top(true);
let _ = window.set_focus();
animate_resize(&window, fullscreen, target, ANIM_DURATION_MS).await;
dbg("step 5 done");
animate_resize(&window, hwnd_raw, fullscreen, target, ANIM_DURATION_MS)
.await;
// ---------- 第⑥步:等待 1s游戏稳定遮罩仍在 ----------
dbg("step 6: settle 1s");
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
dbg("step 6 done");
// ---------- 第⑦步淡出0.5s,含遮罩一起) ----------
dbg("step 7: fade out, waiting frontend");
let (done_tx, done_rx) = tokio::sync::oneshot::channel::<()>();
if let Ok(mut guard) = FADE_DONE.lock() {
*guard = Some(done_tx);
}
let _ = app.emit("mc-transition-fadeout", ());
let faded = tokio::time::timeout(
let _ = tokio::time::timeout(
std::time::Duration::from_millis(FADE_TIMEOUT_MS),
done_rx,
)
.await;
dbg(&format!("step 7: fade wait ok={}", faded.is_ok()));
// ---------- 第⑧步:聚焦游戏 + 最小化启动器 ----------
dbg("step 8: focus game + minimize launcher");
let _ = window.set_always_on_top(false);
win_impl::focus(mc_raw);
tokio::time::sleep(std::time::Duration::from_millis(80)).await;
let _ = window.minimize();
let _ = app.emit("mc-transition-reset-opacity", ());
dbg("step 8 done (launcher minimized)");
}
#[cfg(not(target_os = "windows"))]
pub async fn run(_app: &AppHandle, _pid: u32, _maximize: bool) {}
@ -333,13 +325,11 @@ mod win_impl {
Mutex,
atomic::{AtomicU32, AtomicUsize, Ordering},
};
use windows::Win32::Foundation::{HWND, LPARAM, RECT};
use windows::Win32::Graphics::Gdi::{
GetMonitorInfoW, MONITOR_DEFAULTTONEAREST, MONITORINFO, MonitorFromWindow,
};
use windows::Win32::Foundation::{HWND, LPARAM, POINT, RECT};
use windows::Win32::Graphics::Gdi::ClientToScreen;
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GetWindowRect, GetWindowThreadProcessId, IsWindowVisible,
SetForegroundWindow,
EnumWindows, GetClientRect, GetWindowRect, GetWindowThreadProcessId,
IsWindowVisible, SetForegroundWindow,
};
use windows::core::BOOL;
@ -376,45 +366,6 @@ mod win_impl {
BOOL(1)
}
/// 诊断用:列出属于 `pid` 的所有顶层窗口(含不可见/小窗口)。
/// 返回 (hwnd, 宽, 高, 是否可见)。
pub fn dump_pid_windows(pid: u32) -> Vec<(usize, i32, i32, bool)> {
use windows::core::BOOL as B;
static DUMP_PID: AtomicU32 = AtomicU32::new(0);
static DUMP: Mutex<Vec<(usize, i32, i32, bool)>> =
Mutex::new(Vec::new());
unsafe extern "system" fn cb(hwnd: HWND, _: LPARAM) -> B {
let mut p = 0u32;
unsafe { GetWindowThreadProcessId(hwnd, Some(&mut p)) };
if p != DUMP_PID.load(Ordering::Relaxed) {
return B(1);
}
let mut r = RECT::default();
let _ = unsafe { GetWindowRect(hwnd, &mut r) };
let vis = unsafe { IsWindowVisible(hwnd).as_bool() };
if let Ok(mut v) = DUMP.lock() {
v.push((hwnd.0 as usize, r.right - r.left, r.bottom - r.top, vis));
}
B(1)
}
let _g = ENUM_LOCK.lock();
let _g = match _g {
Ok(g) => g,
Err(_) => return Vec::new(),
};
DUMP_PID.store(pid, Ordering::Relaxed);
if let Ok(mut v) = DUMP.lock() {
v.clear();
}
unsafe {
let _ = EnumWindows(Some(cb), LPARAM(0));
}
DUMP.lock().map(|v| v.clone()).unwrap_or_default()
}
/// 查找属于 `pid` 的可见顶层窗口返回句柄原始值0 表示未找到)。
pub fn find_window(pid: u32) -> Option<usize> {
let _guard = ENUM_LOCK.lock().ok()?;
@ -434,31 +385,63 @@ mod win_impl {
Some((r.left, r.top, r.right - r.left, r.bottom - r.top))
}
/// 取窗口所在显示器的完整矩形,返回 (x, y, width, height)。
pub fn monitor_rect(raw: usize) -> Option<(i32, i32, i32, i32)> {
let mon =
unsafe { MonitorFromWindow(to_hwnd(raw), MONITOR_DEFAULTTONEAREST) };
let mut mi = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
rcMonitor: RECT::default(),
rcWork: RECT::default(),
dwFlags: 0,
};
if !unsafe { GetMonitorInfoW(mon, &mut mi) }.as_bool() {
/// 取窗口「可见客户区」在屏幕坐标下的矩形,返回 (x, y, w, h)。
/// 无边框窗口仍带不可见的 resize border客户区比窗口矩形内缩
/// 这正是全屏后左侧留缝、整体偏右的根源。
pub fn get_client_rect(raw: usize) -> Option<(i32, i32, u32, u32)> {
if raw == 0 {
return None;
}
let r = mi.rcMonitor;
Some((r.left, r.top, r.right - r.left, r.bottom - r.top))
let hwnd = to_hwnd(raw);
let mut cr = RECT::default();
if unsafe { GetClientRect(hwnd, &mut cr) }.is_err() {
return None;
}
let mut origin = POINT { x: 0, y: 0 };
if !unsafe { ClientToScreen(hwnd, &mut origin) }.as_bool() {
return None;
}
Some((
origin.x,
origin.y,
(cr.right - cr.left) as u32,
(cr.bottom - cr.top) as u32,
))
}
/// 判断窗口是否铺满其所在显示器(全屏 / 无边框全屏 / 最大化)。
pub fn covers_monitor(raw: usize) -> bool {
let (Some((_, _, w, h)), Some((_, _, mw, mh))) =
(get_rect(raw), monitor_rect(raw))
else {
return false;
/// 让「可见客户区」精确落在屏幕坐标 (x, y, w, h),自动补偿无边框
/// 窗口的不可见边框。动画与对齐统一用它,避免左/上缝隙。
pub fn set_client_rect(raw: usize, x: i32, y: i32, w: i32, h: i32) {
use windows::Win32::UI::WindowsAndMessaging::{
SWP_NOACTIVATE, SWP_NOZORDER, SetWindowPos,
};
w >= mw && h >= mh
if raw == 0 {
return;
}
let hwnd = to_hwnd(raw);
let mut wr = RECT::default();
let mut cr = RECT::default();
let mut origin = POINT { x: 0, y: 0 };
unsafe {
let _ = GetWindowRect(hwnd, &mut wr);
let _ = GetClientRect(hwnd, &mut cr);
let _ = ClientToScreen(hwnd, &mut origin);
}
let border_w = (wr.right - wr.left) - (cr.right - cr.left);
let border_h = (wr.bottom - wr.top) - (cr.bottom - cr.top);
let inset_x = origin.x - wr.left;
let inset_y = origin.y - wr.top;
unsafe {
let _ = SetWindowPos(
hwnd,
None,
x - inset_x,
y - inset_y,
w + border_w,
h + border_h,
SWP_NOZORDER | SWP_NOACTIVATE,
);
}
}
/// 把前台焦点交给游戏窗口。