mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 17:19:15 -10:00
Merge main into #7149, regenerating the mod API host
This commit is contained in:
commit
a7c3be8ebd
16 files changed
+604
-52
No files matched your search
@@ -1447,6 +1447,13 @@ fn instance_descriptor(
|
||||
}
|
||||
descriptor.backends = backends.with_env();
|
||||
let dx12 = cfg!(windows) && descriptor.backends.contains(wgpu::Backends::DX12);
|
||||
// FXC spends about 2.6 s on every start compiling the pipelines (DXC about
|
||||
// 1 s); what it compiled once is kept in the game folder and read back
|
||||
// after that (third_party/wgpu-hal/src/dx12/disk_cache.rs).
|
||||
#[cfg(windows)]
|
||||
if dx12 {
|
||||
wgpu::hal::dx12::disk_cache::set_file(dx12_shader_cache(&crate::logging::game_home()));
|
||||
}
|
||||
if !cfg!(debug_assertions) && !dx12 {
|
||||
descriptor
|
||||
.flags
|
||||
@@ -1460,6 +1467,11 @@ fn instance_descriptor(
|
||||
}
|
||||
descriptor
|
||||
}
|
||||
/// Where DX12 keeps the shaders it compiled, inside a game folder.
|
||||
pub fn dx12_shader_cache(home: &std::path::Path) -> std::path::PathBuf {
|
||||
home.join("cache").join("dx12-shaders.bin")
|
||||
}
|
||||
|
||||
/// The window's display connection, so the OpenGL backend creates its EGL
|
||||
/// display from the X11 or Wayland connection the window lives on. Without
|
||||
/// one, wgpu-hal falls back to `EGL_MESA_platform_surfaceless`, whose configs
|
||||
|
||||
@@ -8098,6 +8098,10 @@ impl Game<'_> {
|
||||
self.settings.file.bool("hideSplashTexts"),
|
||||
);
|
||||
}
|
||||
// ScreenEvent.Render.Post over the title's own screens: a mod draws
|
||||
// over them here (a pack browser over Create New World, say), and the
|
||||
// toasts still come after, as over any screen.
|
||||
self.fire_title_screen_render_post(&mut batch);
|
||||
self.draw_toasts(&mut batch, false);
|
||||
self.draw_tutorial_toasts(&mut batch, false);
|
||||
self.draw_pack_toasts(&mut batch, false);
|
||||
@@ -8443,6 +8447,21 @@ impl Game<'_> {
|
||||
let view = self.screen_view(batch, screen);
|
||||
self.native_screen_render_post(batch, &view);
|
||||
}
|
||||
/// `native_screen_render_post` (`ScreenEvent.Render.Post`) for the title's
|
||||
/// own screen (`title`, `world_select`, `world_create`, `multiplayer`…),
|
||||
/// once a frame, after it has drawn and before the toasts. Not when a
|
||||
/// screen of the game's own is over the title (Options draws and fires
|
||||
/// its own), nor while a dialog, a prompt or a connect or load hides it.
|
||||
/// Client only, read-only: what a mod draws is its own.
|
||||
fn fire_title_screen_render_post(&self, batch: &mut HudBatch) {
|
||||
if self.screen != Screen::Playing || self.screen_over_playing() || self.remote_loading() {
|
||||
return;
|
||||
}
|
||||
let Some(view) = self.title_screen_view(batch) else {
|
||||
return;
|
||||
};
|
||||
self.native_screen_render_post(batch, &view);
|
||||
}
|
||||
/// `CreativeModeInventoryScreen.hasPermissions` (`canUseGameMasterBlocks`
|
||||
/// and the Operator Items Tab option) given to `tryRefreshInvalidatedTabs`,
|
||||
/// as the screen's constructor and `containerTick` do.
|
||||
@@ -16810,14 +16829,19 @@ impl Game<'_> {
|
||||
// the screen that is open then (`ScreenEvent.CharacterTyped.Pre`).
|
||||
// Options keeps its text on the key: its own key path routes a key's
|
||||
// text between its boxes and its key-bind capture, and no key that
|
||||
// carries text closes that screen or moves its focus.
|
||||
// carries text closes that screen or moves its focus. The title's
|
||||
// screens keep theirs for the same reason: their key path types into
|
||||
// the world name, the seed and a server's address.
|
||||
let mut typed: Option<String> = None;
|
||||
if let WindowEvent::KeyboardInput {
|
||||
event: key_event, ..
|
||||
} = &mut event
|
||||
&& key_event.state == ElementState::Pressed
|
||||
{
|
||||
if self.screen_hooks_open() && self.screen != Screen::Options {
|
||||
if self.screen_hooks_open()
|
||||
&& self.screen != Screen::Options
|
||||
&& !control::title_owns_text(&self.screen, self.title.is_some())
|
||||
{
|
||||
typed = key_event.text.take().map(|text| text.to_string());
|
||||
}
|
||||
let canceled = match key_event.physical_key {
|
||||
|
||||
@@ -233,6 +233,14 @@ impl Game<'_> {
|
||||
self.sign_input(None, Some(text));
|
||||
return Ok(());
|
||||
}
|
||||
// The title's screens: the world name, the seed, a server's address.
|
||||
// A screen opened over the title (Options, say) keeps its own text.
|
||||
if title_owns_text(&self.screen, self.title.is_some())
|
||||
&& let Some(title) = &mut self.title
|
||||
{
|
||||
title.type_text(text);
|
||||
return Ok(());
|
||||
}
|
||||
match self.screen {
|
||||
Screen::Chat => self.chat_type(text),
|
||||
Screen::CommandBlock => self.command_block_type(text),
|
||||
@@ -1459,10 +1467,24 @@ fn key_code(name: &str) -> Option<KeyCode> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The title's own screens (not one opened over it, as Options is) take the
|
||||
/// text: the world name, the seed, a server's address.
|
||||
pub(super) fn title_owns_text(screen: &Screen, title_up: bool) -> bool {
|
||||
title_up && *screen == Screen::Playing
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_title_takes_text_only_on_its_own_screens() {
|
||||
assert!(title_owns_text(&Screen::Playing, true));
|
||||
assert!(!title_owns_text(&Screen::Playing, false));
|
||||
assert!(!title_owns_text(&Screen::Options, true));
|
||||
assert!(!title_owns_text(&Screen::Chat, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hello_names_the_protocol_the_release_and_the_command_groups() {
|
||||
let reply = hello();
|
||||
|
||||
@@ -486,4 +486,7 @@ impl Host for super::Game<'_> {
|
||||
fn mod_image(&mut self, width: u32, height: u32, rgba: &[u8]) -> bool {
|
||||
self.native_mod_image(width, height, rgba)
|
||||
}
|
||||
fn instance_dir(&self) -> PathBuf {
|
||||
self.native_instance_dir()
|
||||
}
|
||||
}
|
||||
@@ -2419,13 +2419,15 @@ impl Game<'_> {
|
||||
/// `&self`, so it reads and draws only. The Win and Options screens call
|
||||
/// it on their own path, after their drawing.
|
||||
///
|
||||
/// It does not fire on the title screen, which returns before its point,
|
||||
/// nor for the screens drawn over gameplay after it (a server dialog, the
|
||||
/// server pack, link and code-of-conduct prompts, a book, a sign editor):
|
||||
/// those keep `Screen::Playing`, which it skips (the server pack prompt
|
||||
/// hides an open screen too, and that screen does not fire under it),
|
||||
/// and draw over what it drew, while vanilla's `ScreenEvent.Render.Post`
|
||||
/// fires for them.
|
||||
/// It does not fire for the screens drawn over gameplay after it (a
|
||||
/// server dialog, the server pack, link and code-of-conduct prompts, a
|
||||
/// book, a sign editor): those keep `Screen::Playing`, which it skips (the
|
||||
/// server pack prompt hides an open screen too, and that screen does not
|
||||
/// fire under it), and draw over what it drew, while vanilla's
|
||||
/// `ScreenEvent.Render.Post` fires for them. The title's own screens
|
||||
/// (`title`, `world_select`, `world_create`, `multiplayer`…) fire it too,
|
||||
/// their `screen.id` from the title state machine and the whole window
|
||||
/// their `rect` (API @API:native_instance_dir@), and no dialog or prompt over them.
|
||||
/// API 78.
|
||||
pub(super) fn native_screen_render_post(
|
||||
&self,
|
||||
@@ -2445,10 +2447,11 @@ impl Game<'_> {
|
||||
/// no screen button, no drag, no hotbar key, no sort or header button.
|
||||
///
|
||||
/// It fires before anything the screen does with the press, on the window
|
||||
/// only, and only while a screen of the game's is open (Options and the
|
||||
/// pack list opened from the title screen count, with no player and no
|
||||
/// `menu`). It does not fire on the title screen itself, the connect and
|
||||
/// loading screens, nor for a server
|
||||
/// only, and while a screen of the game's or the title's is open (Options
|
||||
/// and the pack list opened from the title screen count, with no player
|
||||
/// and no `menu`; the title's own screens since API @API:native_instance_dir@, their
|
||||
/// `screen.id` from the title state machine). It does not fire on the
|
||||
/// connect and loading screens, nor for a server
|
||||
/// dialog, the server pack, link and code-of-conduct prompts, a book or a
|
||||
/// sign editor drawn over gameplay, nor while a world loads or saves: the
|
||||
/// click there stays the game's own. A gamepad click or a click the
|
||||
@@ -2535,9 +2538,10 @@ impl Game<'_> {
|
||||
///
|
||||
/// It fires before the screen scrolls anything, for the window's wheel
|
||||
/// (horizon-ctl's `scroll-at` sends it through the window too), and
|
||||
/// only while a screen of the game's is open (Options and the pack list
|
||||
/// opened from the title screen count, with no player and no `menu`). It
|
||||
/// does not fire on the title screen itself, the connect and loading
|
||||
/// while a screen of the game's or the title's is open (Options and the
|
||||
/// pack list opened from the title screen count, with no player and no
|
||||
/// `menu`; the title's own since API @API:native_instance_dir@). It
|
||||
/// does not fire on the connect and loading
|
||||
/// screens, nor for a server dialog, the server pack, link and
|
||||
/// code-of-conduct prompts, a book or a sign editor drawn over gameplay,
|
||||
/// nor while a world loads or saves: the scroll there stays the game's
|
||||
@@ -2573,10 +2577,10 @@ impl Game<'_> {
|
||||
/// It fires for the window's presses and their repeats, and for the
|
||||
/// control socket's `key` (a release fires nothing), after the window's
|
||||
/// own shortcuts have run — the fullscreen and screenshot keys, the debug
|
||||
/// keys and the narrator — and only while a screen of the game's is open
|
||||
/// (Options and the pack list opened from the title screen count, with no
|
||||
/// player and no `menu`). It does not fire on the title screen itself, the
|
||||
/// connect and loading screens, nor for a server dialog, the server pack,
|
||||
/// keys and the narrator — and while a screen of the game's or the title's
|
||||
/// is open (Options and the pack list opened from the title screen count,
|
||||
/// with no player and no `menu`; the title's own since API @API:native_instance_dir@). It does
|
||||
/// not fire on the connect and loading screens, nor for a server dialog, the server pack,
|
||||
/// link and code-of-conduct prompts, a book or a sign editor drawn over
|
||||
/// gameplay, nor while a world loads or saves: the key there stays the
|
||||
/// game's own. A cancelled press's release still runs, so a `native_key`
|
||||
@@ -2675,6 +2679,23 @@ impl Game<'_> {
|
||||
partial_tick: self.clock.fraction() as f32,
|
||||
}
|
||||
}
|
||||
/// The title's own screen as a native mod sees it (`NativeScreenView`):
|
||||
/// its id from the title state machine (`title`, `world_select`,
|
||||
/// `world_create`, `multiplayer`, `add_server`…), no menu and no menu
|
||||
/// title, the whole window as its rectangle and no slots (no menu is
|
||||
/// open). None while no title screen is up. API @API:native_instance_dir@.
|
||||
pub(super) fn title_screen_view(&self, batch: &HudBatch) -> Option<NativeScreenView<'_>> {
|
||||
let title = self.title.as_ref()?;
|
||||
Some(NativeScreenView {
|
||||
id: title.mode.id(),
|
||||
menu: None,
|
||||
title: None,
|
||||
rect: [0.0, 0.0, batch.width, batch.height],
|
||||
slots: 0,
|
||||
mouse: self.native_cursor(),
|
||||
partial_tick: self.clock.fraction() as f32,
|
||||
})
|
||||
}
|
||||
/// The open screen's container panel in GUI pixels (`screen`'s, the one
|
||||
/// this frame draws), or the whole window for a screen with none
|
||||
/// (`NativeScreenView.rect`).
|
||||
@@ -2721,15 +2742,16 @@ impl Game<'_> {
|
||||
canceled
|
||||
}
|
||||
/// A screen the native mods' input hooks are for is open: the window has a
|
||||
/// renderer, `Screen::Playing` is behind it (so not the title screen, nor a
|
||||
/// book, a sign editor, a server dialog or a prompt drawn over gameplay)
|
||||
/// and no connect or world load is in progress. The one gate they all ask
|
||||
/// before they fire.
|
||||
/// renderer, either the title's own (`title`, `world_select`,
|
||||
/// `world_create`, `multiplayer`…) or a screen over the world
|
||||
/// (`Screen::Playing` behind it, so not a book, a sign editor, a server
|
||||
/// dialog or a prompt drawn over gameplay), and no connect or world load
|
||||
/// is in progress. The one gate they all ask before they fire.
|
||||
pub(super) fn screen_hooks_open(&self) -> bool {
|
||||
self.renderer.is_some()
|
||||
&& self.screen != Screen::Playing
|
||||
&& !self.screen_over_playing()
|
||||
&& !self.remote_loading()
|
||||
&& (self.screen != Screen::Playing || self.title.is_some())
|
||||
}
|
||||
/// The open screen as an input hook sees it (`NativeScreenView`), or None
|
||||
/// when `screen_hooks_open` says no screen is. `screen.mouse` is the
|
||||
@@ -2743,7 +2765,10 @@ impl Game<'_> {
|
||||
}
|
||||
let renderer = self.renderer.as_ref()?;
|
||||
let batch = HudBatch::new([renderer.config.width, renderer.config.height]);
|
||||
Some(self.screen_view(&batch, self.screen))
|
||||
if self.screen != Screen::Playing {
|
||||
return Some(self.screen_view(&batch, self.screen));
|
||||
}
|
||||
self.title_screen_view(&batch)
|
||||
}
|
||||
/// Whether the key press `key` over the open screen went no further
|
||||
/// (`native_screen_key_pressed_pre`): what `handle_window_event` and the
|
||||
@@ -7398,6 +7423,15 @@ impl Game<'_> {
|
||||
.chat
|
||||
.add(atlas, line, None, ChatSource::Client, self.ticks, focused);
|
||||
}
|
||||
/// The instance (game) folder, which its players share: `saves/`,
|
||||
/// `resourcepacks/`, `shaderpacks/` and `horizon-mods/` are in it, and the
|
||||
/// title's screens create worlds and read packs there. A world's own
|
||||
/// `datapacks/` is in that world's folder (`saves/<world>/datapacks`).
|
||||
/// The folder the game runs in (`--player-dir` moves a player's options
|
||||
/// and servers only, not the instance). Nothing is created. API @API:native_instance_dir@.
|
||||
pub(super) fn native_instance_dir(&self) -> PathBuf {
|
||||
Self::game_dir()
|
||||
}
|
||||
/// A native mod's own folder: `horizon-mods/.horizon-mod-data/<id>/`,
|
||||
/// where the WebAssembly mod of the same id kept its storage, so a
|
||||
/// player's settings carry over. Not created here.
|
||||
|
||||
@@ -110,7 +110,10 @@ pub struct NativeScreenView<'a> {
|
||||
/// on it. Every container, station and mod window draws on the inventory
|
||||
/// screen and so reports `inventory`: tell them apart by `menu` and
|
||||
/// `title` (both None on a window the client opened itself) and by
|
||||
/// `slots`.
|
||||
/// `slots`. The title's own screens report theirs (API @API:native_instance_dir@): `title`,
|
||||
/// `world_select`, `world_create`, `world_create_flat`, `world_edit`,
|
||||
/// `multiplayer`, `add_server`, `direct_connect`…, with no menu, no title
|
||||
/// and no slots.
|
||||
pub id: &'static str,
|
||||
/// The type id of the menu the server opened for this screen
|
||||
/// (`minecraft:generic_9x3`, `minecraft:furnace`, a mod's own id; a mount's
|
||||
|
||||
+22
-18
@@ -1187,13 +1187,15 @@ pub trait Hooks: Send + Sync {
|
||||
/// it is. Once a frame on the window, never while playing with no screen;
|
||||
/// `&self`, so it reads and draws only. The Win and Options screens call
|
||||
/// it on their own path, after their drawing.
|
||||
/// It does not fire on the title screen, which returns before its point,
|
||||
/// nor for the screens drawn over gameplay after it (a server dialog, the
|
||||
/// server pack, link and code-of-conduct prompts, a book, a sign editor):
|
||||
/// those keep `Screen::Playing`, which it skips (the server pack prompt
|
||||
/// hides an open screen too, and that screen does not fire under it),
|
||||
/// and draw over what it drew, while vanilla's `ScreenEvent.Render.Post`
|
||||
/// fires for them.
|
||||
/// It does not fire for the screens drawn over gameplay after it (a
|
||||
/// server dialog, the server pack, link and code-of-conduct prompts, a
|
||||
/// book, a sign editor): those keep `Screen::Playing`, which it skips (the
|
||||
/// server pack prompt hides an open screen too, and that screen does not
|
||||
/// fire under it), and draw over what it drew, while vanilla's
|
||||
/// `ScreenEvent.Render.Post` fires for them. The title's own screens
|
||||
/// (`title`, `world_select`, `world_create`, `multiplayer`…) fire it too,
|
||||
/// their `screen.id` from the title state machine and the whole window
|
||||
/// their `rect` (API @API:native_instance_dir@), and no dialog or prompt over them.
|
||||
/// API 78.
|
||||
fn screen_render_post(&self, host: &dyn Host, event: &mut ScreenRenderPostEvent<'_>) {
|
||||
let _ = host;
|
||||
@@ -1225,10 +1227,11 @@ pub trait Hooks: Send + Sync {
|
||||
/// alt 4, super 8). Set `canceled` and the press goes no further: no slot,
|
||||
/// no screen button, no drag, no hotbar key, no sort or header button.
|
||||
/// It fires before anything the screen does with the press, on the window
|
||||
/// only, and only while a screen of the game's is open (Options and the
|
||||
/// pack list opened from the title screen count, with no player and no
|
||||
/// `menu`). It does not fire on the title screen itself, the connect and
|
||||
/// loading screens, nor for a server
|
||||
/// only, and while a screen of the game's or the title's is open (Options
|
||||
/// and the pack list opened from the title screen count, with no player
|
||||
/// and no `menu`; the title's own screens since API @API:native_instance_dir@, their
|
||||
/// `screen.id` from the title state machine). It does not fire on the
|
||||
/// connect and loading screens, nor for a server
|
||||
/// dialog, the server pack, link and code-of-conduct prompts, a book or a
|
||||
/// sign editor drawn over gameplay, nor while a world loads or saves: the
|
||||
/// click there stays the game's own. A gamepad click or a click the
|
||||
@@ -1418,9 +1421,10 @@ pub trait Hooks: Send + Sync {
|
||||
/// changes either.
|
||||
/// It fires before the screen scrolls anything, for the window's wheel
|
||||
/// (horizon-ctl's `scroll-at` sends it through the window too), and
|
||||
/// only while a screen of the game's is open (Options and the pack list
|
||||
/// opened from the title screen count, with no player and no `menu`). It
|
||||
/// does not fire on the title screen itself, the connect and loading
|
||||
/// while a screen of the game's or the title's is open (Options and the
|
||||
/// pack list opened from the title screen count, with no player and no
|
||||
/// `menu`; the title's own since API @API:native_instance_dir@). It
|
||||
/// does not fire on the connect and loading
|
||||
/// screens, nor for a server dialog, the server pack, link and
|
||||
/// code-of-conduct prompts, a book or a sign editor drawn over gameplay,
|
||||
/// nor while a world loads or saves: the scroll there stays the game's
|
||||
@@ -1488,10 +1492,10 @@ pub trait Hooks: Send + Sync {
|
||||
/// It fires for the window's presses and their repeats, and for the
|
||||
/// control socket's `key` (a release fires nothing), after the window's
|
||||
/// own shortcuts have run — the fullscreen and screenshot keys, the debug
|
||||
/// keys and the narrator — and only while a screen of the game's is open
|
||||
/// (Options and the pack list opened from the title screen count, with no
|
||||
/// player and no `menu`). It does not fire on the title screen itself, the
|
||||
/// connect and loading screens, nor for a server dialog, the server pack,
|
||||
/// keys and the narrator — and while a screen of the game's or the title's
|
||||
/// is open (Options and the pack list opened from the title screen count,
|
||||
/// with no player and no `menu`; the title's own since API @API:native_instance_dir@). It does
|
||||
/// not fire on the connect and loading screens, nor for a server dialog, the server pack,
|
||||
/// link and code-of-conduct prompts, a book or a sign editor drawn over
|
||||
/// gameplay, nor while a world loads or saves: the key there stays the
|
||||
/// game's own. A cancelled press's release still runs, so a `native_key`
|
||||
|
||||
@@ -1193,4 +1193,13 @@ pub trait Host {
|
||||
let _ = rgba;
|
||||
unimplemented!("Host::mod_image")
|
||||
}
|
||||
/// The instance (game) folder, which its players share: `saves/`,
|
||||
/// `resourcepacks/`, `shaderpacks/` and `horizon-mods/` are in it, and the
|
||||
/// title's screens create worlds and read packs there. A world's own
|
||||
/// `datapacks/` is in that world's folder (`saves/<world>/datapacks`).
|
||||
/// The folder the game runs in (`--player-dir` moves a player's options
|
||||
/// and servers only, not the instance). Nothing is created. API @API:native_instance_dir@.
|
||||
fn instance_dir(&self) -> PathBuf {
|
||||
unimplemented!("Host::instance_dir")
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,24 @@ pub enum Outcome {
|
||||
Back,
|
||||
Connect { address: String, lan: bool },
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
/// The id native mods see for this screen (`NativeScreenView.id`, the
|
||||
/// screen hooks): one per variant, a hook's contract, so it stays the
|
||||
/// same from release to release. A new variant must add one.
|
||||
pub fn id(&self) -> &'static str {
|
||||
match self {
|
||||
Self::List { .. } => "multiplayer",
|
||||
Self::Edit { index: None, .. } => "add_server",
|
||||
Self::Edit { index: Some(_), .. } => "edit_server",
|
||||
Self::Direct { .. } => "direct_connect",
|
||||
Self::ConfirmDelete { .. } => "server_delete",
|
||||
Self::Friend { .. } => "friends_join",
|
||||
Self::Disconnected { .. } => "disconnected",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A button: x, y, width, label, enabled, id.
|
||||
pub type ButtonSpec = (f32, f32, f32, String, bool, u8);
|
||||
|
||||
|
||||
@@ -699,6 +699,35 @@ fn rolls_user_splash(text: &str, seed: u64, date: (u32, u32)) -> bool {
|
||||
.is_ok_and(|size| size > 0 && LegacyRandom::new(seed as i64).next_int_bound(size) == Ok(42))
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
/// The id native mods see for this screen (`NativeScreenView.id`, the
|
||||
/// screen hooks): one per variant, a hook's contract, so it stays the
|
||||
/// same from release to release. A new variant must add one.
|
||||
pub fn id(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Main => "title",
|
||||
Self::Select { .. } => "world_select",
|
||||
Self::Create { .. } => "world_create",
|
||||
Self::CreateFlat { .. } => "world_create_flat",
|
||||
Self::CreateBuffet { .. } => "world_create_buffet",
|
||||
Self::Edit { .. } => "world_edit",
|
||||
Self::Backup { .. } => "world_backup",
|
||||
Self::BackupBeforeUpgrade { .. } => "world_backup_confirm",
|
||||
Self::OptimizeConfirm { .. } => "world_optimize_confirm",
|
||||
Self::Optimize { .. } => "world_optimize",
|
||||
Self::LowDiskSpace { .. } => "low_disk_space",
|
||||
Self::Delete { .. } => "world_delete",
|
||||
Self::Recover { .. } => "world_recover",
|
||||
Self::Recovered { .. } => "world_recovered",
|
||||
Self::Error { .. } => "error",
|
||||
Self::Multiplayer(screen) => screen.id(),
|
||||
Self::MultiplayerWarning { .. } => "multiplayer_warning",
|
||||
Self::Onboarding(_) => "onboarding",
|
||||
Self::BanNotice(_) => "ban_notice",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Title {
|
||||
/// Names the player for the `<NAME> IS YOU` splash; `name` is only asked for when that splash was rolled.
|
||||
pub fn with_user(mut self, name: impl FnOnce() -> String) -> Self {
|
||||
|
||||
@@ -2278,3 +2278,76 @@ fn a_create_world_tab_label_wider_than_its_tab_stays_inside_it() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The ids native mods see for the title's screens (`Mode::id`, the screen
|
||||
/// hooks' `NativeScreenView.id`): one per screen, stable across releases.
|
||||
#[test]
|
||||
fn the_title_screens_have_their_own_ids() {
|
||||
let mut f = Fixture::new();
|
||||
assert_eq!(f.title.mode.id(), "title");
|
||||
// An empty list opens Create New World (an empty list is why vanilla
|
||||
// starts one), a world in it opens the list.
|
||||
f.title.enter_singleplayer();
|
||||
assert!(matches!(f.title.mode, Mode::Create { .. }));
|
||||
assert_eq!(f.title.mode.id(), "world_create");
|
||||
f.title.escape();
|
||||
assert_eq!(f.title.mode.id(), "title");
|
||||
f.world("World", "World");
|
||||
f.select();
|
||||
assert_eq!(f.title.mode.id(), "world_select");
|
||||
f.click("Edit");
|
||||
assert_eq!(f.title.mode.id(), "world_edit");
|
||||
}
|
||||
|
||||
/// The multiplayer screens' ids, the ones the title's `Multiplayer` mode
|
||||
/// passes through.
|
||||
#[test]
|
||||
fn the_multiplayer_screens_have_their_own_ids() {
|
||||
use crate::multiplayer::Screen as M;
|
||||
assert_eq!(
|
||||
M::Direct {
|
||||
address: String::new()
|
||||
}
|
||||
.id(),
|
||||
"direct_connect"
|
||||
);
|
||||
assert_eq!(
|
||||
M::Edit {
|
||||
index: None,
|
||||
name: String::new(),
|
||||
address: String::new(),
|
||||
focus: 0,
|
||||
packs: None,
|
||||
}
|
||||
.id(),
|
||||
"add_server"
|
||||
);
|
||||
assert_eq!(
|
||||
M::Edit {
|
||||
index: Some(0),
|
||||
name: String::new(),
|
||||
address: String::new(),
|
||||
focus: 0,
|
||||
packs: None,
|
||||
}
|
||||
.id(),
|
||||
"edit_server"
|
||||
);
|
||||
assert_eq!(
|
||||
M::Friend {
|
||||
code: String::new(),
|
||||
relay: String::new(),
|
||||
focus: 0,
|
||||
}
|
||||
.id(),
|
||||
"friends_join"
|
||||
);
|
||||
assert_eq!(
|
||||
M::Disconnected {
|
||||
reason: String::new(),
|
||||
retry: None,
|
||||
}
|
||||
.id(),
|
||||
"disconnected"
|
||||
);
|
||||
}
|
||||
+22
-7
@@ -424,14 +424,29 @@ impl super::Device {
|
||||
key.shader_model.to_str()
|
||||
);
|
||||
|
||||
let compiled_shader = self.compiler_container.compile(
|
||||
self,
|
||||
&key.source,
|
||||
source_name,
|
||||
// Horizon: a shader compiled in an earlier run comes from the disk.
|
||||
let disk_key = super::disk_cache::key(
|
||||
self.compiler_container.cache_name(),
|
||||
key.shader_model.to_str(),
|
||||
naga::back::hlsl::shader_stage_to_hlsl_str(naga_stage),
|
||||
&key.entry_point,
|
||||
stage_bit,
|
||||
&full_stage,
|
||||
)?;
|
||||
&key.source,
|
||||
);
|
||||
let compiled_shader = match super::disk_cache::get(disk_key) {
|
||||
Some(bytes) => super::CompiledShader::Precompiled(bytes),
|
||||
None => {
|
||||
let compiled = self.compiler_container.compile(
|
||||
self,
|
||||
&key.source,
|
||||
source_name,
|
||||
&key.entry_point,
|
||||
stage_bit,
|
||||
&full_stage,
|
||||
)?;
|
||||
super::disk_cache::put(disk_key, compiled.bytes());
|
||||
compiled
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
let mut shader_cache = self.shader_cache.lock();
|
||||
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
//! Horizon: compiled shaders kept on disk between runs.
|
||||
//!
|
||||
//! FXC takes about 2.6 s to compile Horizon's ~65 pipelines on every start
|
||||
//! (DXC about 1 s), while the in-memory [`super::ShaderCache`] only helps
|
||||
//! within one device. The application names one file with [`set_file`]. A
|
||||
//! shader compiled once is appended there, and the next run reads its bytecode
|
||||
//! back as [`super::CompiledShader::Precompiled`] instead of compiling it.
|
||||
//!
|
||||
//! The key is a 128-bit FNV-1a hash of the compiler, the shader model, the
|
||||
//! stage, the entry point and the HLSL source naga wrote. Any change to
|
||||
//! naga's output, the shader, or the compiler choice is a different key, so
|
||||
//! the file never needs explicit invalidation. Each record carries a checksum
|
||||
//! of its bytes; reading stops at the first record that is short or does not
|
||||
//! match (a run killed mid-write, two games writing at once), and the next
|
||||
//! append starts there. A file past [`MAX_BYTES`] is started over.
|
||||
//!
|
||||
//! File: `MAGIC`, then records of `key: u128 LE`, `len: u32 LE`,
|
||||
//! `check: u64 LE` (FNV-1a 64 of the bytes), `bytes`.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use std::{
|
||||
fs,
|
||||
io::{Seek as _, SeekFrom, Write as _},
|
||||
path::PathBuf,
|
||||
sync::{Mutex, OnceLock},
|
||||
};
|
||||
|
||||
use hashbrown::HashMap;
|
||||
|
||||
const MAGIC: &[u8; 8] = b"HZDXSC1\n";
|
||||
const HEADER: usize = 16 + 4 + 8;
|
||||
/// Past this the file is started over: Horizon's shaders take about 1 MB.
|
||||
pub const MAX_BYTES: u64 = 64 << 20;
|
||||
|
||||
static FILE: OnceLock<PathBuf> = OnceLock::new();
|
||||
static CACHE: OnceLock<Mutex<Cache>> = OnceLock::new();
|
||||
|
||||
/// Keep compiled shaders in `path` for the rest of the run. Only the first
|
||||
/// call counts; without one nothing is kept on disk.
|
||||
pub fn set_file(path: PathBuf) {
|
||||
let _ = FILE.set(path);
|
||||
}
|
||||
|
||||
pub(super) struct Cache {
|
||||
path: PathBuf,
|
||||
entries: HashMap<u128, Vec<u8>>,
|
||||
/// Where the next record goes: the end of the last good record.
|
||||
end: u64,
|
||||
}
|
||||
|
||||
fn cache() -> Option<&'static Mutex<Cache>> {
|
||||
let path = FILE.get()?;
|
||||
Some(CACHE.get_or_init(|| Mutex::new(Cache::load(path.clone()))))
|
||||
}
|
||||
|
||||
/// The bytecode compiled for `key` in an earlier run.
|
||||
pub(super) fn get(key: u128) -> Option<Vec<u8>> {
|
||||
let cache = cache()?.lock().ok()?;
|
||||
cache.entries.get(&key).cloned()
|
||||
}
|
||||
|
||||
/// Keep `bytes` for `key` for the next runs.
|
||||
pub(super) fn put(key: u128, bytes: &[u8]) {
|
||||
let Some(cache) = cache() else { return };
|
||||
let Ok(mut cache) = cache.lock() else { return };
|
||||
if let Err(error) = cache.append(key, bytes) {
|
||||
log::warn!("DX12 shader cache {}: {error}", cache.path.display());
|
||||
}
|
||||
}
|
||||
|
||||
/// The key of one compile.
|
||||
pub(super) fn key(
|
||||
compiler: &str,
|
||||
shader_model: &str,
|
||||
stage: &str,
|
||||
entry: &str,
|
||||
source: &str,
|
||||
) -> u128 {
|
||||
let mut hash = Fnv128::new();
|
||||
for part in [compiler, shader_model, stage, entry, source] {
|
||||
hash.write(&(part.len() as u64).to_le_bytes());
|
||||
hash.write(part.as_bytes());
|
||||
}
|
||||
hash.0
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
fn load(path: PathBuf) -> Self {
|
||||
let bytes = match fs::metadata(&path) {
|
||||
Ok(meta) if meta.len() <= MAX_BYTES => fs::read(&path).unwrap_or_default(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let (entries, end) = parse(&bytes);
|
||||
Self { path, entries, end }
|
||||
}
|
||||
|
||||
fn append(&mut self, key: u128, bytes: &[u8]) -> std::io::Result<()> {
|
||||
if self.entries.contains_key(&key) {
|
||||
return Ok(());
|
||||
}
|
||||
let len = u32::try_from(bytes.len()).map_err(std::io::Error::other)?;
|
||||
if self.end + (HEADER + bytes.len()) as u64 > MAX_BYTES {
|
||||
self.end = 0;
|
||||
self.entries.clear();
|
||||
}
|
||||
if let Some(dir) = self.path.parent() {
|
||||
fs::create_dir_all(dir)?;
|
||||
}
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.write(true)
|
||||
.open(&self.path)?;
|
||||
let mut record = Vec::with_capacity(MAGIC.len() + HEADER + bytes.len());
|
||||
if self.end == 0 {
|
||||
record.extend_from_slice(MAGIC);
|
||||
}
|
||||
record.extend_from_slice(&key.to_le_bytes());
|
||||
record.extend_from_slice(&len.to_le_bytes());
|
||||
record.extend_from_slice(&fnv64(bytes).to_le_bytes());
|
||||
record.extend_from_slice(bytes);
|
||||
// Drop whatever follows the last good record, then write after it.
|
||||
file.set_len(self.end)?;
|
||||
file.seek(SeekFrom::Start(self.end))?;
|
||||
file.write_all(&record)?;
|
||||
self.end += record.len() as u64;
|
||||
self.entries.insert(key, bytes.to_vec());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The records of a cache file and where its good part ends.
|
||||
fn parse(bytes: &[u8]) -> (HashMap<u128, Vec<u8>>, u64) {
|
||||
let mut entries = HashMap::new();
|
||||
if bytes.get(..MAGIC.len()) != Some(MAGIC.as_slice()) {
|
||||
return (entries, 0);
|
||||
}
|
||||
let mut at = MAGIC.len();
|
||||
while let Some(header) = bytes.get(at..at + HEADER) {
|
||||
let key = u128::from_le_bytes(header[..16].try_into().unwrap());
|
||||
let len = u32::from_le_bytes(header[16..20].try_into().unwrap()) as usize;
|
||||
let check = u64::from_le_bytes(header[20..28].try_into().unwrap());
|
||||
let Some(body) = bytes.get(at + HEADER..at + HEADER + len) else {
|
||||
break;
|
||||
};
|
||||
if fnv64(body) != check {
|
||||
break;
|
||||
}
|
||||
entries.insert(key, body.to_vec());
|
||||
at += HEADER + len;
|
||||
}
|
||||
(entries, at as u64)
|
||||
}
|
||||
|
||||
struct Fnv128(u128);
|
||||
|
||||
impl Fnv128 {
|
||||
fn new() -> Self {
|
||||
Self(0x6c62272e07bb014262b821756295c58d)
|
||||
}
|
||||
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
for &b in bytes {
|
||||
self.0 ^= u128::from(b);
|
||||
self.0 = self.0.wrapping_mul(0x0000000001000000000000000000013b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fnv64(bytes: &[u8]) -> u64 {
|
||||
let mut hash = 0xcbf29ce484222325u64;
|
||||
for &b in bytes {
|
||||
hash ^= u64::from(b);
|
||||
hash = hash.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn scratch(name: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("hz-dx12-cache-{name}-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
dir.join("cache/dx12-shaders.bin")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_shader_kept_in_one_run_is_read_back_in_the_next() {
|
||||
let path = scratch("next");
|
||||
let vs = key(
|
||||
"fxc",
|
||||
"5_1",
|
||||
"vs",
|
||||
"main",
|
||||
"float4 main() : SV_Position { return 0; }",
|
||||
);
|
||||
let fs_ = key(
|
||||
"fxc",
|
||||
"5_1",
|
||||
"ps",
|
||||
"main",
|
||||
"float4 main() : SV_Target { return 1; }",
|
||||
);
|
||||
let mut run = Cache::load(path.clone());
|
||||
run.append(vs, b"DXBC vertex").unwrap();
|
||||
run.append(fs_, b"DXBC fragment").unwrap();
|
||||
let next = Cache::load(path.clone());
|
||||
assert_eq!(
|
||||
next.entries.get(&vs).map(Vec::as_slice),
|
||||
Some(&b"DXBC vertex"[..])
|
||||
);
|
||||
assert_eq!(
|
||||
next.entries.get(&fs_).map(Vec::as_slice),
|
||||
Some(&b"DXBC fragment"[..])
|
||||
);
|
||||
let _ = fs::remove_dir_all(path.parent().unwrap().parent().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_part_of_the_compile_changes_the_key() {
|
||||
let base = key("fxc", "5_1", "vs", "main", "src");
|
||||
assert_ne!(base, key("dxc", "5_1", "vs", "main", "src"));
|
||||
assert_ne!(base, key("fxc", "6_0", "vs", "main", "src"));
|
||||
assert_ne!(base, key("fxc", "5_1", "ps", "main", "src"));
|
||||
assert_ne!(base, key("fxc", "5_1", "vs", "main2", "src"));
|
||||
assert_ne!(base, key("fxc", "5_1", "vs", "main", "src "));
|
||||
// Lengths are hashed, so moving a byte between parts changes the key.
|
||||
assert_ne!(key("ab", "c", "", "", ""), key("a", "bc", "", "", ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_torn_record_is_dropped_and_the_next_one_written_over_it() {
|
||||
let path = scratch("torn");
|
||||
let a = key("fxc", "5_1", "vs", "a", "a");
|
||||
let b = key("fxc", "5_1", "vs", "b", "b");
|
||||
let c = key("fxc", "5_1", "vs", "c", "c");
|
||||
let mut run = Cache::load(path.clone());
|
||||
run.append(a, b"first").unwrap();
|
||||
run.append(b, b"second").unwrap();
|
||||
// The run is killed while it writes `b`: its last bytes never land.
|
||||
let full = fs::read(&path).unwrap();
|
||||
fs::write(&path, &full[..full.len() - 3]).unwrap();
|
||||
let mut next = Cache::load(path.clone());
|
||||
assert_eq!(next.entries.len(), 1);
|
||||
assert!(next.entries.contains_key(&a));
|
||||
next.append(c, b"third").unwrap();
|
||||
let last = Cache::load(path.clone());
|
||||
assert_eq!(last.entries.len(), 2);
|
||||
assert_eq!(last.entries.get(&c).map(Vec::as_slice), Some(&b"third"[..]));
|
||||
|
||||
// A flipped byte fails the checksum the same way.
|
||||
let mut bytes = fs::read(&path).unwrap();
|
||||
let at = bytes.len() - 1;
|
||||
bytes[at] ^= 1;
|
||||
fs::write(&path, bytes).unwrap();
|
||||
assert_eq!(Cache::load(path.clone()).entries.len(), 1);
|
||||
let _ = fs::remove_dir_all(path.parent().unwrap().parent().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_that_is_not_a_cache_is_started_over() {
|
||||
let path = scratch("foreign");
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(&path, b"not a shader cache at all").unwrap();
|
||||
let mut run = Cache::load(path.clone());
|
||||
assert!(run.entries.is_empty());
|
||||
let k = key("dxc", "6_0", "cs", "main", "x");
|
||||
run.append(k, b"DXIL").unwrap();
|
||||
let next = Cache::load(path.clone());
|
||||
assert_eq!(next.entries.get(&k).map(Vec::as_slice), Some(&b"DXIL"[..]));
|
||||
let _ = fs::remove_dir_all(path.parent().unwrap().parent().unwrap());
|
||||
}
|
||||
}
|
||||
Vendored
+11
@@ -83,6 +83,7 @@ mod conv;
|
||||
mod dcomp;
|
||||
mod descriptor;
|
||||
mod device;
|
||||
pub mod disk_cache;
|
||||
mod device_creation;
|
||||
mod instance;
|
||||
mod pipeline_desc;
|
||||
@@ -1353,6 +1354,16 @@ pub(super) enum CompiledShader {
|
||||
}
|
||||
|
||||
impl CompiledShader {
|
||||
/// The bytecode, as [`disk_cache`] keeps it.
|
||||
fn bytes(&self) -> &[u8] {
|
||||
let code = self.create_native_shader();
|
||||
if code.BytecodeLength == 0 {
|
||||
return &[];
|
||||
}
|
||||
// SAFETY: the blob or vector `code` points into lives as long as `self`.
|
||||
unsafe { core::slice::from_raw_parts(code.pShaderBytecode.cast(), code.BytecodeLength) }
|
||||
}
|
||||
|
||||
fn create_native_shader(&self) -> Direct3D12::D3D12_SHADER_BYTECODE {
|
||||
match self {
|
||||
CompiledShader::Dxc(shader) => Direct3D12::D3D12_SHADER_BYTECODE {
|
||||
|
||||
@@ -104,6 +104,15 @@ impl CompilerContainer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Which compiler made a shader, for [`super::disk_cache`]'s keys.
|
||||
pub(super) fn cache_name(&self) -> &'static str {
|
||||
match self {
|
||||
CompilerContainer::Fxc(_) => "fxc",
|
||||
CompilerContainer::DynamicDxc(_) => "dxc",
|
||||
CompilerContainer::StaticDxc(_) => "static-dxc",
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn compile(
|
||||
&self,
|
||||
device: &super::Device,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"hooks": [],
|
||||
"helpers": [
|
||||
{
|
||||
"name": "instance_dir",
|
||||
"target": "horizon_client::game::native_hooks::Game<'_>::native_instance_dir"
|
||||
}
|
||||
],
|
||||
"rust_summary": "the title's own screens to the screen hooks, and the instance folder helper (`native_instance_dir`).",
|
||||
"doc_summary": "the title's own screens to the screen hooks. `NativeScreenView.id` gains one id per title state (`title`, `world_select`, `world_create`, `world_create_flat`, `world_create_buffet`, `world_edit`, `world_backup`, `world_backup_confirm`, `world_optimize_confirm`, `world_optimize`, `low_disk_space`, `world_delete`, `world_recover`, `world_recovered`, `error`, `multiplayer`, `add_server`, `edit_server`, `direct_connect`, `server_delete`, `friends_join`, `disconnected`, `multiplayer_warning`, `onboarding`, `ban_notice`), with no `menu`, no `title` and no `slots`, and the whole window as `rect`. `native_screen_render_post` fires after the title draws its screen (before its toasts), and the `_pre` input hooks — mouse buttons, drag, wheel, key press and release, character typed — fire on it and can cancel it as on any screen, so a mod draws its own button or browser over Create New World and takes the clicks. A dialog, a prompt or a server's pack prompt over the title keeps them away, and typing still goes to the title's own boxes (the key's text stays on the key there, as on Options, so `native_screen_char_typed_pre` does not fire from the keyboard on the title's own screens; the control socket's `type` now types into them too). `native_instance_dir()` is the instance folder, where `options.txt`, `saves/`, `resourcepacks/`, `shaderpacks/` and `horizon-mods/` are; a world's `datapacks/` is in that world's folder. Bundles that use them need `\"hooks\": @API:native_instance_dir@`."
|
||||
}
|
||||
Reference in new issue
Block a user