Skip to content

Commit 012cd3c

Browse files
committed
fix: 返回大厅时清空遗留的缓存
1 parent 62d480c commit 012cd3c

5 files changed

Lines changed: 127 additions & 38 deletions

File tree

crates/client/src/lib.rs

Lines changed: 105 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ struct App {
126126
disconnect_reason: Option<String>,
127127

128128
game: Option<Game>,
129+
/// 当前世界会话编号。进入 / 离开世界时递增,异步存档回写前用它识别过期结果。
130+
world_session_id: u64,
129131

130132
/// 上一帧 performance.now()(毫秒)
131133
last_time_ms: f64,
@@ -206,6 +208,7 @@ pub async fn start() -> Result<(), JsValue> {
206208
connecting_error: None,
207209
disconnect_reason: None,
208210
game: None,
211+
world_session_id: 0,
209212
last_time_ms: now_ms(),
210213
fps_frames: 0,
211214
fps_accum: 0.0,
@@ -224,6 +227,57 @@ pub async fn start() -> Result<(), JsValue> {
224227
Ok(())
225228
}
226229

230+
/// 递增世界会话编号。所有跨 await 的世界相关异步任务都应捕获编号,回写前再次比对。
231+
fn bump_world_session(a: &mut App) -> u64 {
232+
a.world_session_id = a.world_session_id.wrapping_add(1);
233+
a.world_session_id
234+
}
235+
236+
/// 清掉当前世界运行时资源。Renderer 是跨状态常驻对象,必须显式清空世界 GPU 缓存。
237+
fn clear_world_runtime(a: &mut App) -> u64 {
238+
let session_id = bump_world_session(a);
239+
a.game = None;
240+
a.preload_state = None;
241+
a.request_pointer_lock_next = false;
242+
a.relayed_peers.clear();
243+
a.notifications.clear();
244+
a.perf = FramePerfStats::default();
245+
a.egui_events.borrow_mut().clear();
246+
a.renderer.clear_world_cache();
247+
a.input.borrow_mut().clear_held();
248+
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
249+
doc.exit_pointer_lock();
250+
}
251+
session_id
252+
}
253+
254+
/// 开始新世界前的统一入口:先结束旧会话,再返回新会话编号给异步存档任务使用。
255+
fn prepare_world_start(a: &mut App) -> u64 {
256+
let session_id = clear_world_runtime(a);
257+
a.disconnect_reason = None;
258+
a.connecting_error = None;
259+
session_id
260+
}
261+
262+
/// 回大厅时让下一帧 Lobby 重新读取 OPFS 世界列表,避免显示退出前的旧缓存。
263+
fn mark_lobby_saves_stale(a: &mut App) {
264+
a.lobby_state.saves_loaded = false;
265+
a.lobby_state.saves_loading = false;
266+
a.lobby_state.saved_worlds.clear();
267+
a.lobby_state.selected_save = None;
268+
}
269+
270+
/// 所有“返回大厅”路径的统一收口。
271+
fn return_to_lobby(a: &mut App) {
272+
clear_world_runtime(a);
273+
a.state = AppState::Lobby;
274+
a.disconnect_reason = None;
275+
a.connecting_error = None;
276+
a.connecting_room_id.clear();
277+
a.connecting_mode = GameMode::Local;
278+
mark_lobby_saves_stale(a);
279+
}
280+
227281
// ============================================================
228282
// 主循环 & 事件
229283
// ============================================================
@@ -946,26 +1000,29 @@ fn start_single_player(
9461000
let rd = game.settings.render_distance as i32;
9471001
let total = ((2 * rd + 1) * (2 * rd + 1)) as usize;
9481002

949-
let mut a = app.borrow_mut();
950-
a.game = Some(game);
951-
a.connecting_mode = GameMode::Local;
952-
a.connecting_room_id = String::new();
953-
a.connecting_error = None;
954-
a.preload_state = Some(PreloadState {
955-
total,
956-
received: 0,
957-
meshed: 0,
958-
active: true,
959-
});
960-
a.state = AppState::Connecting;
961-
log::info!("[local] 进入加载界面,开始区块预载 (total={total})");
962-
drop(a);
1003+
let session_id = {
1004+
let mut a = app.borrow_mut();
1005+
let session_id = prepare_world_start(&mut a);
1006+
a.game = Some(game);
1007+
a.connecting_mode = GameMode::Local;
1008+
a.connecting_room_id = String::new();
1009+
a.connecting_error = None;
1010+
a.preload_state = Some(PreloadState {
1011+
total,
1012+
received: 0,
1013+
meshed: 0,
1014+
active: true,
1015+
});
1016+
a.state = AppState::Connecting;
1017+
log::info!("[local] 进入加载界面,开始区块预载 (total={total})");
1018+
session_id
1019+
};
9631020

9641021
// 如果有 save_key,使用 open_by_key 加载;否则创建新存档
9651022
if let Some(key) = save_key {
966-
attach_storage_for_load(app.clone(), key.to_string());
1023+
attach_storage_for_load(app.clone(), key.to_string(), session_id);
9671024
} else {
968-
attach_storage_for_new(app.clone(), seed);
1025+
attach_storage_for_new(app.clone(), seed, session_id);
9691026
}
9701027
}
9711028

@@ -1096,6 +1153,7 @@ fn start_host(
10961153
game.camera.position = game.physics.eye_position();
10971154
game.camera.pitch = -0.4;
10981155

1156+
let session_id = prepare_world_start(&mut a);
10991157
a.game = Some(game);
11001158
a.state = AppState::Connecting;
11011159
a.connecting_mode = GameMode::Host;
@@ -1107,10 +1165,10 @@ fn start_host(
11071165

11081166
// 如果有 save_key,使用 open_by_key 加载;否则创建新存档
11091167
if let Some(key) = save_key {
1110-
attach_storage_for_load(app.clone(), key.to_string());
1168+
attach_storage_for_load(app.clone(), key.to_string(), session_id);
11111169
} else {
11121170
// Host 模式:使用 room_id + seed 创建存档
1113-
attach_storage_async(app.clone(), room_id.to_string(), seed);
1171+
attach_storage_async(app.clone(), room_id.to_string(), seed, session_id);
11141172
}
11151173
}
11161174
Err(e) => {
@@ -1144,6 +1202,7 @@ fn start_remote(app: &Rc<RefCell<App>>, room_id: &str, display_name: &str) {
11441202
game.camera.position = game.physics.eye_position();
11451203
game.camera.pitch = -0.4;
11461204

1205+
prepare_world_start(&mut a);
11471206
a.game = Some(game);
11481207
a.state = AppState::Connecting;
11491208
a.connecting_mode = GameMode::Remote;
@@ -1159,7 +1218,7 @@ fn start_remote(app: &Rc<RefCell<App>>, room_id: &str, display_name: &str) {
11591218
}
11601219
}
11611220

1162-
fn attach_storage_async(app: Rc<RefCell<App>>, room_id: String, seed: u64) {
1221+
fn attach_storage_async(app: Rc<RefCell<App>>, room_id: String, seed: u64, session_id: u64) {
11631222
wasm_bindgen_futures::spawn_local(async move {
11641223
let _ = OpfsStorage::request_persistence().await;
11651224
match OpfsStorage::open(&room_id, seed).await {
@@ -1186,6 +1245,10 @@ fn attach_storage_async(app: Rc<RefCell<App>>, room_id: String, seed: u64) {
11861245
}
11871246
let quota = storage.quota().await;
11881247
let mut a = app.borrow_mut();
1248+
if a.world_session_id != session_id {
1249+
log::debug!("[storage] 丢弃过期 Host 存档加载结果");
1250+
return;
1251+
}
11891252
if let Some(g) = a.game.as_mut() {
11901253
for (pos, chunk) in loaded {
11911254
g.server.borrow_mut().load_chunk_from_storage(pos, chunk);
@@ -1199,6 +1262,9 @@ fn attach_storage_async(app: Rc<RefCell<App>>, room_id: String, seed: u64) {
11991262
}
12001263
Err(e) => {
12011264
let mut a = app.borrow_mut();
1265+
if a.world_session_id != session_id {
1266+
return;
1267+
}
12021268
if let Some(g) = a.game.as_mut() {
12031269
g.storage_error = Some(format!("{e:?}"));
12041270
}
@@ -1208,7 +1274,7 @@ fn attach_storage_async(app: Rc<RefCell<App>>, room_id: String, seed: u64) {
12081274
}
12091275

12101276
/// 创建新存档(用当前时间戳 + seed 生成 key)
1211-
fn attach_storage_for_new(app: Rc<RefCell<App>>, seed: u64) {
1277+
fn attach_storage_for_new(app: Rc<RefCell<App>>, seed: u64, session_id: u64) {
12121278
wasm_bindgen_futures::spawn_local(async move {
12131279
let _ = OpfsStorage::request_persistence().await;
12141280
match OpfsStorage::create_new(seed).await {
@@ -1235,6 +1301,10 @@ fn attach_storage_for_new(app: Rc<RefCell<App>>, seed: u64) {
12351301
}
12361302
let quota = storage.quota().await;
12371303
let mut a = app.borrow_mut();
1304+
if a.world_session_id != session_id {
1305+
log::debug!("[storage] 丢弃过期新存档加载结果");
1306+
return;
1307+
}
12381308
if let Some(g) = a.game.as_mut() {
12391309
for (pos, chunk) in loaded {
12401310
g.server.borrow_mut().load_chunk_from_storage(pos, chunk);
@@ -1248,6 +1318,9 @@ fn attach_storage_for_new(app: Rc<RefCell<App>>, seed: u64) {
12481318
}
12491319
Err(e) => {
12501320
let mut a = app.borrow_mut();
1321+
if a.world_session_id != session_id {
1322+
return;
1323+
}
12511324
if let Some(g) = a.game.as_mut() {
12521325
g.storage_error = Some(format!("{e:?}"));
12531326
}
@@ -1257,7 +1330,7 @@ fn attach_storage_for_new(app: Rc<RefCell<App>>, seed: u64) {
12571330
}
12581331

12591332
/// 通过 key 加载已有存档
1260-
fn attach_storage_for_load(app: Rc<RefCell<App>>, key: String) {
1333+
fn attach_storage_for_load(app: Rc<RefCell<App>>, key: String, session_id: u64) {
12611334
wasm_bindgen_futures::spawn_local(async move {
12621335
let _ = OpfsStorage::request_persistence().await;
12631336
match OpfsStorage::open_by_key(&key).await {
@@ -1284,6 +1357,10 @@ fn attach_storage_for_load(app: Rc<RefCell<App>>, key: String) {
12841357
}
12851358
let quota = storage.quota().await;
12861359
let mut a = app.borrow_mut();
1360+
if a.world_session_id != session_id {
1361+
log::debug!("[storage] 丢弃过期已有存档加载结果");
1362+
return;
1363+
}
12871364
if let Some(g) = a.game.as_mut() {
12881365
for (pos, chunk) in loaded {
12891366
g.server.borrow_mut().load_chunk_from_storage(pos, chunk);
@@ -1297,6 +1374,9 @@ fn attach_storage_for_load(app: Rc<RefCell<App>>, key: String) {
12971374
}
12981375
Err(e) => {
12991376
let mut a = app.borrow_mut();
1377+
if a.world_session_id != session_id {
1378+
return;
1379+
}
13001380
if let Some(g) = a.game.as_mut() {
13011381
g.storage_error = Some(format!("{e:?}"));
13021382
}
@@ -1477,12 +1557,8 @@ fn render_connecting_frame(app: &Rc<RefCell<App>>, cw: u32, ch: u32) -> Result<(
14771557
};
14781558

14791559
if matches!(cancel, Some(ConnectingAction::Cancel)) {
1480-
// 直接丢掉 game,回 Lobby
14811560
let mut a = app.borrow_mut();
1482-
a.game = None;
1483-
a.preload_state = None;
1484-
a.state = AppState::Lobby;
1485-
a.connecting_error = None;
1561+
return_to_lobby(&mut a);
14861562
return Ok(());
14871563
}
14881564

@@ -1620,9 +1696,7 @@ fn render_disconnected_frame(app: &Rc<RefCell<App>>, cw: u32, ch: u32) -> Result
16201696

16211697
if matches!(action, ui::disconnected::DisconnectedAction::BackToLobby) {
16221698
let mut a = app.borrow_mut();
1623-
a.state = AppState::Lobby;
1624-
a.disconnect_reason = None;
1625-
a.connecting_error = None;
1699+
return_to_lobby(&mut a);
16261700
return Ok(());
16271701
}
16281702

@@ -1780,13 +1854,11 @@ fn apply_room_event(app: &Rc<RefCell<App>>, ev: RoomEvent) {
17801854
}
17811855
RoomEvent::Disconnected { reason } => {
17821856
log::warn!("[net] Disconnected: {reason}");
1857+
clear_world_runtime(&mut a);
17831858
a.disconnect_reason = Some(reason.clone());
17841859
a.connecting_error = Some(reason);
1785-
a.preload_state = None;
1786-
a.relayed_peers.clear();
17871860
// Phase 6:Connecting / InGame 失联都跳到 Disconnected 页让用户看到原因
17881861
a.state = AppState::Disconnected;
1789-
a.game = None;
17901862
}
17911863
RoomEvent::RemoteLeft { peer_id } => {
17921864
log::info!("[net] RemoteLeft: peer {peer_id}");
@@ -2331,10 +2403,7 @@ fn render_game_frame(
23312403
}
23322404
if pause_exit_to_lobby {
23332405
let mut a = app.borrow_mut();
2334-
a.game = None;
2335-
a.state = AppState::Lobby;
2336-
a.disconnect_reason = None;
2337-
a.preload_state = None;
2406+
return_to_lobby(&mut a);
23382407
return Ok(());
23392408
}
23402409

crates/client/src/ui/disconnected.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
#[derive(Clone, Debug, PartialEq, Eq)]
99
pub enum DisconnectedAction {
1010
None,
11-
/// 用户点了"返回大厅"。调用方设 `app.state = AppState::Lobby` 并清 `disconnect_reason`
11+
/// 用户点了"返回大厅"。调用方走统一的大厅重置流程
1212
BackToLobby,
1313
}
1414

crates/render/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,16 @@ impl Renderer {
168168
self.transparent_meshes.remove(&pos);
169169
}
170170

171+
/// 清空当前世界上传过的渲染缓存。
172+
///
173+
/// Renderer 跨 AppState 常驻;退出世界只销毁 client::Game 不会自动释放这里的
174+
/// GPU 缓存,所以切换世界前必须显式清空,避免上一局内容继续参与绘制。
175+
pub fn clear_world_cache(&mut self) {
176+
self.chunk_meshes.clear();
177+
self.transparent_meshes.clear();
178+
self.player_pass.upload_instances(&self.queue, &[]);
179+
}
180+
171181
/// 查询某个 chunk 是否已有 GPU mesh。
172182
/// Phase 2 ChunkLoader 用其决定邻居是否需重网格化(跨区块剔除生效条件)。
173183
pub fn has_chunk_mesh(&self, pos: ChunkPos) -> bool {

docs/modules/client.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ struct App {
172172
disconnect_reason: Option<String>,
173173

174174
game: Option<Game>,
175+
world_session_id: u64, // 每次进入/离开世界递增,用于丢弃旧异步回调
175176

176177
last_time_ms: f64,
177178
fps_frames: u32, fps_accum: f32, fps_display: f32,
@@ -241,6 +242,12 @@ pub struct FrameClock { /* accumulator: f32, step: f32 */ }
241242

242243
> Phase 2 不引入 `world_view`:Local 模式下 mesh 回调直接借 `server.world.get_block_world`。Phase 5 加入 Remote 模式时才有 `WorldView` 副本(由 ChunkSnapshot 喂数据)。
243244
245+
### 世界会话清理
246+
247+
进入新世界或离开当前世界时,client 会递增 `world_session_id`,并清理 `Game`、区块预载状态、输入事件、性能统计与 `Renderer` 内的世界 GPU 缓存。OPFS 存档加载等异步任务在回写前必须比对启动时捕获的 `world_session_id`;若会话已变化,直接丢弃结果,避免“退出再进入”后旧世界的 chunk 或 mesh 混入新世界。
248+
249+
返回大厅统一清空当前存档列表缓存并标记为未加载,让下一帧 Lobby 自动重新读取 OPFS 世界列表。
250+
244251
---
245252

246253
## 五、主循环

docs/modules/render.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,9 @@ impl Renderer {
295295
/// 卸载远处 chunk 网格
296296
pub fn drop_chunk_mesh(&mut self, pos: ChunkPos);
297297

298+
/// 退出当前世界 / 进入新世界前清空所有世界渲染缓存
299+
pub fn clear_world_cache(&mut self);
300+
298301
/// 查询某个 chunk 是否已有 GPU mesh
299302
pub fn has_chunk_mesh(&self, pos: ChunkPos) -> bool;
300303

@@ -327,7 +330,7 @@ impl Renderer {
327330
|---|---|---|
328331
| `Surface` / `Device` / `Queue` | `Renderer::new` 一次 | Tab 关闭 |
329332
| `depth_texture` | 启动 + 每次 resize | 重建时 |
330-
| `chunk_mesh_gpu` | `upload_chunk_mesh` | `drop_chunk_mesh`(玩家走远)/ chunk 修改时(重建) |
333+
| `chunk_mesh_gpu` | `upload_chunk_mesh` | `drop_chunk_mesh`(玩家走远)/ `clear_world_cache`(退出或切换世界)/ chunk 修改时(重建) |
331334
| Pass pipelines | 各 Pass `new` 一次 | 程序退出 |
332335

333336
**Chunk 网格更新规则**

0 commit comments

Comments
 (0)