fix(client): the window keeps drawing while single player's server starts

Minecraft.doWorldLoad shows the LevelLoadingScreen and keeps running the
client while its server prepares the world. Horizon's window waited on the
server thread's ready message instead, frozen (not responding on Windows)
for the whole load. The server's answer is now polled each frame from
resume_world_entry, the LevelLoadingScreen's title is drawn meanwhile,
and the window joins once the server is ready. Runs without a window and
the synchronous harness and control runs still wait for it, as before.
This commit is contained in:
feeldev committed 2026-10-27 18:02:04 -05:00
1 parent 795490271d
commit 356b69aea7
2 files changed
+80 -10

No files matched your search

+28 -1
View File
@@ -1381,6 +1381,8 @@ struct Game<'a> {
/// The link a chat click asks to open (`ConfirmLinkScreen`).
link_prompt: Option<String>,
dialogs: dialogs::DialogState,
/// Single player's server while it loads its world (`Minecraft.doWorldLoad`).
integrated_starting: Option<integrated::Starting>,
world_renderer_ready: bool,
collision: &'a BlockCollisionRegistry<'a>,
motion: &'a MotionResources,
@@ -11414,6 +11416,27 @@ impl Game<'_> {
Ok(())
}
fn render(&mut self) -> Result<bool> {
// Minecraft.doWorldLoad: the LevelLoadingScreen while single player's
// server starts. Its chunk map and progress are the server's, which
// the window doesn't see yet: the title alone, over the menu background.
if self.integrated_starting.is_some() {
if let Some(renderer) = &mut self.renderer {
let mut hud = HudBatch::new([renderer.config.width, renderer.config.height]);
let (w, h) = (hud.width, hud.height);
horizon_client::ui::menu_background(
&mut hud,
&self.gui.atlas,
0.0,
0.0,
w,
h,
false,
);
horizon_client::level_loading::draw(&mut hud, &self.gui.atlas, None, None);
return renderer.render(&loading_frame(&hud));
}
return Ok(false);
}
if let Some(loader) = &self.world_loader {
if let Some(renderer) = &mut self.renderer {
let mut hud = HudBatch::new([renderer.config.width, renderer.config.height]);
@@ -12584,7 +12607,9 @@ impl ApplicationHandler for Game<'_> {
self.perf.cancel_world_opening();
return;
}
if (self.pending_entry.is_some() || self.world_loader.is_some())
if (self.pending_entry.is_some()
|| self.world_loader.is_some()
|| self.integrated_starting.is_some())
&& !matches!(
&event,
WindowEvent::CloseRequested
@@ -13573,6 +13598,7 @@ impl<'a> Game<'a> {
server_packs: Default::default(),
link_prompt: None,
dialogs: Default::default(),
integrated_starting: None,
world_renderer_ready: false,
collision: res.collision(),
motion: res.motion(),
@@ -14046,6 +14072,7 @@ impl Game<'_> {
}
fn resume_world_entry(&mut self) -> Result<()> {
self.poll_integrated_start()?;
self.poll_world_rest()?;
if let Some(loader) = &self.world_loader {
if !loader.ready() {
+52 -9
View File
@@ -169,6 +169,16 @@ enum Publish {
/// of the connection and the world's name, or why it failed.
type Ready = std::result::Result<(Pipe, String), String>;
/// An integrated server loading its world: the window keeps drawing its
/// loading screen and joins once the server is ready, as vanilla's client
/// does while its server prepares (`Minecraft.doWorldLoad`), instead of
/// waiting for it frozen for the whole load.
pub(super) struct Starting {
handle: Integrated,
ready: Receiver<Ready>,
player: String,
}
/// The world to open and the player who owns it.
struct Launch {
folder: String,
@@ -197,8 +207,8 @@ impl Game<'_> {
.as_ref()
.is_some_and(|s| s.published().is_some())
}
/// `Minecraft.doWorldLoad`: start the server thread on the world, wait
/// until it can be joined, then join it through memory.
/// `Minecraft.doWorldLoad`: start the server thread on the world; the
/// window joins it through memory once it is ready (`poll_integrated_start`).
pub(super) fn open_integrated(
&mut self,
folder: &str,
@@ -267,20 +277,53 @@ impl Game<'_> {
let _ = ready_tx.send(Err(e.to_string()));
}
})?;
let mut handle = Integrated {
let handle = Integrated {
flags,
thread: Some(thread),
tasks,
world_name: String::new(),
};
let pipe = match ready.recv() {
Ok(Ok((pipe, world_name))) => {
handle.world_name = world_name;
pipe
self.integrated_starting = Some(Starting {
handle,
ready,
player,
});
// Harnesses, control scripts and runs without a window go on with the world open.
if self.window.is_none() || super::synchronous_startup(&self.options) {
while self.integrated_starting.is_some() {
self.poll_integrated_start()?;
if self.integrated_starting.is_some() {
std::thread::sleep(Duration::from_millis(5));
}
}
Ok(Err(e)) => return Err(e.into()),
Err(_) => return Err("the integrated server stopped".into()),
}
Ok(())
}
/// The starting server's answer, polled each frame: joined once it is ready.
pub(super) fn poll_integrated_start(&mut self) -> Result<()> {
let Some(starting) = &self.integrated_starting else {
return Ok(());
};
let (pipe, world_name) = match starting.ready.try_recv() {
Ok(Ok(ready)) => ready,
Err(std::sync::mpsc::TryRecvError::Empty) => return Ok(()),
Ok(Err(e)) => {
self.integrated_starting = None;
return Err(e.into());
}
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
self.integrated_starting = None;
return Err("the integrated server stopped".into());
}
};
let Some(Starting {
mut handle, player, ..
}) = self.integrated_starting.take()
else {
return Ok(());
};
handle.world_name = world_name;
self.connect_local(pipe, &player)?;
self.integrated = Some(handle);
Ok(())