Skip to content

Commit 19d7ec4

Browse files
authored
fix: 返回大厅时清空遗留的缓存
- 不会再遗留之前世界的渲染 - 会刷新世界列表
2 parents 51a74a7 + 012cd3c commit 19d7ec4

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
// ============================================================
@@ -953,26 +1007,29 @@ fn start_single_player(
9531007
let rd = game.settings.render_distance as i32;
9541008
let total = ((2 * rd + 1) * (2 * rd + 1)) as usize;
9551009

956-
let mut a = app.borrow_mut();
957-
a.game = Some(game);
958-
a.connecting_mode = GameMode::Local;
959-
a.connecting_room_id = String::new();
960-
a.connecting_error = None;
961-
a.preload_state = Some(PreloadState {
962-
total,
963-
received: 0,
964-
meshed: 0,
965-
active: true,
966-
});
967-
a.state = AppState::Connecting;
968-
log::info!("[local] 进入加载界面,开始区块预载 (total={total})");
969-
drop(a);
1010+
let session_id = {
1011+
let mut a = app.borrow_mut();
1012+
let session_id = prepare_world_start(&mut a);
1013+
a.game = Some(game);
1014+
a.connecting_mode = GameMode::Local;
1015+
a.connecting_room_id = String::new();
1016+
a.connecting_error = None;
1017+
a.preload_state = Some(PreloadState {
1018+
total,
1019+
received: 0,
1020+
meshed: 0,
1021+
active: true,
1022+
});
1023+
a.state = AppState::Connecting;
1024+
log::info!("[local] 进入加载界面,开始区块预载 (total={total})");
1025+
session_id
1026+
};
9701027

9711028
// 如果有 save_key,使用 open_by_key 加载;否则创建新存档
9721029
if let Some(key) = save_key {
973-
attach_storage_for_load(app.clone(), key.to_string());
1030+
attach_storage_for_load(app.clone(), key.to_string(), session_id);
9741031
} else {
975-
attach_storage_for_new(app.clone(), seed);
1032+
attach_storage_for_new(app.clone(), seed, session_id);
9761033
}
9771034
}
9781035

@@ -1103,6 +1160,7 @@ fn start_host(
11031160
game.camera.position = game.physics.eye_position();
11041161
game.camera.pitch = -0.4;
11051162

1163+
let session_id = prepare_world_start(&mut a);
11061164
a.game = Some(game);
11071165
a.state = AppState::Connecting;
11081166
a.connecting_mode = GameMode::Host;
@@ -1114,10 +1172,10 @@ fn start_host(
11141172

11151173
// 如果有 save_key,使用 open_by_key 加载;否则创建新存档
11161174
if let Some(key) = save_key {
1117-
attach_storage_for_load(app.clone(), key.to_string());
1175+
attach_storage_for_load(app.clone(), key.to_string(), session_id);
11181176
} else {
11191177
// Host 模式:使用 room_id + seed 创建存档
1120-
attach_storage_async(app.clone(), room_id.to_string(), seed);
1178+
attach_storage_async(app.clone(), room_id.to_string(), seed, session_id);
11211179
}
11221180
}
11231181
Err(e) => {
@@ -1151,6 +1209,7 @@ fn start_remote(app: &Rc<RefCell<App>>, room_id: &str, display_name: &str) {
11511209
game.camera.position = game.physics.eye_position();
11521210
game.camera.pitch = -0.4;
11531211

1212+
prepare_world_start(&mut a);
11541213
a.game = Some(game);
11551214
a.state = AppState::Connecting;
11561215
a.connecting_mode = GameMode::Remote;
@@ -1166,7 +1225,7 @@ fn start_remote(app: &Rc<RefCell<App>>, room_id: &str, display_name: &str) {
11661225
}
11671226
}
11681227

1169-
fn attach_storage_async(app: Rc<RefCell<App>>, room_id: String, seed: u64) {
1228+
fn attach_storage_async(app: Rc<RefCell<App>>, room_id: String, seed: u64, session_id: u64) {
11701229
wasm_bindgen_futures::spawn_local(async move {
11711230
let _ = OpfsStorage::request_persistence().await;
11721231
match OpfsStorage::open(&room_id, seed).await {
@@ -1193,6 +1252,10 @@ fn attach_storage_async(app: Rc<RefCell<App>>, room_id: String, seed: u64) {
11931252
}
11941253
let quota = storage.quota().await;
11951254
let mut a = app.borrow_mut();
1255+
if a.world_session_id != session_id {
1256+
log::debug!("[storage] 丢弃过期 Host 存档加载结果");
1257+
return;
1258+
}
11961259
if let Some(g) = a.game.as_mut() {
11971260
for (pos, chunk) in loaded {
11981261
g.server.borrow_mut().load_chunk_from_storage(pos, chunk);
@@ -1206,6 +1269,9 @@ fn attach_storage_async(app: Rc<RefCell<App>>, room_id: String, seed: u64) {
12061269
}
12071270
Err(e) => {
12081271
let mut a = app.borrow_mut();
1272+
if a.world_session_id != session_id {
1273+
return;
1274+
}
12091275
if let Some(g) = a.game.as_mut() {
12101276
g.storage_error = Some(format!("{e:?}"));
12111277
}
@@ -1215,7 +1281,7 @@ fn attach_storage_async(app: Rc<RefCell<App>>, room_id: String, seed: u64) {
12151281
}
12161282

12171283
/// 创建新存档(用当前时间戳 + seed 生成 key)
1218-
fn attach_storage_for_new(app: Rc<RefCell<App>>, seed: u64) {
1284+
fn attach_storage_for_new(app: Rc<RefCell<App>>, seed: u64, session_id: u64) {
12191285
wasm_bindgen_futures::spawn_local(async move {
12201286
let _ = OpfsStorage::request_persistence().await;
12211287
match OpfsStorage::create_new(seed).await {
@@ -1242,6 +1308,10 @@ fn attach_storage_for_new(app: Rc<RefCell<App>>, seed: u64) {
12421308
}
12431309
let quota = storage.quota().await;
12441310
let mut a = app.borrow_mut();
1311+
if a.world_session_id != session_id {
1312+
log::debug!("[storage] 丢弃过期新存档加载结果");
1313+
return;
1314+
}
12451315
if let Some(g) = a.game.as_mut() {
12461316
for (pos, chunk) in loaded {
12471317
g.server.borrow_mut().load_chunk_from_storage(pos, chunk);
@@ -1255,6 +1325,9 @@ fn attach_storage_for_new(app: Rc<RefCell<App>>, seed: u64) {
12551325
}
12561326
Err(e) => {
12571327
let mut a = app.borrow_mut();
1328+
if a.world_session_id != session_id {
1329+
return;
1330+
}
12581331
if let Some(g) = a.game.as_mut() {
12591332
g.storage_error = Some(format!("{e:?}"));
12601333
}
@@ -1264,7 +1337,7 @@ fn attach_storage_for_new(app: Rc<RefCell<App>>, seed: u64) {
12641337
}
12651338

12661339
/// 通过 key 加载已有存档
1267-
fn attach_storage_for_load(app: Rc<RefCell<App>>, key: String) {
1340+
fn attach_storage_for_load(app: Rc<RefCell<App>>, key: String, session_id: u64) {
12681341
wasm_bindgen_futures::spawn_local(async move {
12691342
let _ = OpfsStorage::request_persistence().await;
12701343
match OpfsStorage::open_by_key(&key).await {
@@ -1291,6 +1364,10 @@ fn attach_storage_for_load(app: Rc<RefCell<App>>, key: String) {
12911364
}
12921365
let quota = storage.quota().await;
12931366
let mut a = app.borrow_mut();
1367+
if a.world_session_id != session_id {
1368+
log::debug!("[storage] 丢弃过期已有存档加载结果");
1369+
return;
1370+
}
12941371
if let Some(g) = a.game.as_mut() {
12951372
for (pos, chunk) in loaded {
12961373
g.server.borrow_mut().load_chunk_from_storage(pos, chunk);
@@ -1304,6 +1381,9 @@ fn attach_storage_for_load(app: Rc<RefCell<App>>, key: String) {
13041381
}
13051382
Err(e) => {
13061383
let mut a = app.borrow_mut();
1384+
if a.world_session_id != session_id {
1385+
return;
1386+
}
13071387
if let Some(g) = a.game.as_mut() {
13081388
g.storage_error = Some(format!("{e:?}"));
13091389
}
@@ -1484,12 +1564,8 @@ fn render_connecting_frame(app: &Rc<RefCell<App>>, cw: u32, ch: u32) -> Result<(
14841564
};
14851565

14861566
if matches!(cancel, Some(ConnectingAction::Cancel)) {
1487-
// 直接丢掉 game,回 Lobby
14881567
let mut a = app.borrow_mut();
1489-
a.game = None;
1490-
a.preload_state = None;
1491-
a.state = AppState::Lobby;
1492-
a.connecting_error = None;
1568+
return_to_lobby(&mut a);
14931569
return Ok(());
14941570
}
14951571

@@ -1627,9 +1703,7 @@ fn render_disconnected_frame(app: &Rc<RefCell<App>>, cw: u32, ch: u32) -> Result
16271703

16281704
if matches!(action, ui::disconnected::DisconnectedAction::BackToLobby) {
16291705
let mut a = app.borrow_mut();
1630-
a.state = AppState::Lobby;
1631-
a.disconnect_reason = None;
1632-
a.connecting_error = None;
1706+
return_to_lobby(&mut a);
16331707
return Ok(());
16341708
}
16351709

@@ -1787,13 +1861,11 @@ fn apply_room_event(app: &Rc<RefCell<App>>, ev: RoomEvent) {
17871861
}
17881862
RoomEvent::Disconnected { reason } => {
17891863
log::warn!("[net] Disconnected: {reason}");
1864+
clear_world_runtime(&mut a);
17901865
a.disconnect_reason = Some(reason.clone());
17911866
a.connecting_error = Some(reason);
1792-
a.preload_state = None;
1793-
a.relayed_peers.clear();
17941867
// Phase 6:Connecting / InGame 失联都跳到 Disconnected 页让用户看到原因
17951868
a.state = AppState::Disconnected;
1796-
a.game = None;
17971869
}
17981870
RoomEvent::RemoteLeft { peer_id } => {
17991871
log::info!("[net] RemoteLeft: peer {peer_id}");
@@ -2338,10 +2410,7 @@ fn render_game_frame(
23382410
}
23392411
if pause_exit_to_lobby {
23402412
let mut a = app.borrow_mut();
2341-
a.game = None;
2342-
a.state = AppState::Lobby;
2343-
a.disconnect_reason = None;
2344-
a.preload_state = None;
2413+
return_to_lobby(&mut a);
23452414
return Ok(());
23462415
}
23472416

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)