mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
perf: share decoded block facts during client startup
This commit is contained in:
1 parent
e422bd138a
commit
a01d47f28f
10 files changed
+476
-54
No files matched your search
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"scope": "Assets + block facts + behaviours loader only; excludes full startup",
|
||||
"resources": ".local/horizon-client/26.3",
|
||||
"repeats": 5,
|
||||
"metrics": {
|
||||
"baseline": {
|
||||
"phase_ms": {
|
||||
"median": 4730.402,
|
||||
"samples": [
|
||||
3258.541,
|
||||
6539.764,
|
||||
4773.735,
|
||||
3980.032,
|
||||
4730.402
|
||||
]
|
||||
},
|
||||
"total_ms": {
|
||||
"median": 6325.026,
|
||||
"samples": [
|
||||
6596.141,
|
||||
7627.893,
|
||||
6296.443,
|
||||
5435.408,
|
||||
6325.026
|
||||
]
|
||||
},
|
||||
"cpu_ms": {
|
||||
"median": 3199.722,
|
||||
"samples": [
|
||||
3322.801,
|
||||
3199.722,
|
||||
3227.438,
|
||||
3042.687,
|
||||
3144.658
|
||||
]
|
||||
},
|
||||
"peak_rss_kib": {
|
||||
"median": 820000,
|
||||
"samples": [
|
||||
818540,
|
||||
820400,
|
||||
819352,
|
||||
820000,
|
||||
820128
|
||||
]
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"phase_ms": {
|
||||
"median": 1180.588,
|
||||
"samples": [
|
||||
869.228,
|
||||
842.213,
|
||||
1180.588,
|
||||
1560.087,
|
||||
1728.575
|
||||
]
|
||||
},
|
||||
"total_ms": {
|
||||
"median": 2260.709,
|
||||
"samples": [
|
||||
1632.489,
|
||||
1553.753,
|
||||
2260.709,
|
||||
2879.728,
|
||||
3388.639
|
||||
]
|
||||
},
|
||||
"cpu_ms": {
|
||||
"median": 1603.393,
|
||||
"samples": [
|
||||
1568.288,
|
||||
1432.106,
|
||||
1603.393,
|
||||
1604.423,
|
||||
1619.02
|
||||
]
|
||||
},
|
||||
"peak_rss_kib": {
|
||||
"median": 351728,
|
||||
"samples": [
|
||||
353256,
|
||||
353664,
|
||||
351728,
|
||||
350724,
|
||||
351668
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"build": "cargo build --release -p horizon-client --example shared_json_startup_profile",
|
||||
"limitations": "Loader-only wall-clock timings under concurrent machine load; not full startup or time to first frame. RSS includes Assets::load."
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Profile the fact and behaviour table phase of client startup.
|
||||
//! Run in separate processes for baseline and shared JSON modes to capture peak RSS.
|
||||
use horizon_client::{assets::Assets, blocks::BlockFacts};
|
||||
use horizon_world::level::Behaviours;
|
||||
use std::{path::Path, time::Instant};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let root = args.next().ok_or("expected resource directory")?;
|
||||
let mode = args.next().ok_or("expected baseline or shared")?;
|
||||
if mode != "baseline" && mode != "shared" {
|
||||
return Err("expected baseline or shared".into());
|
||||
}
|
||||
let root = Path::new(&root);
|
||||
let total = Instant::now();
|
||||
let assets = Assets::load(root)?;
|
||||
let started = Instant::now();
|
||||
let (facts, behaviours) = if mode == "baseline" {
|
||||
(
|
||||
BlockFacts::load(root, assets.registry)?,
|
||||
Behaviours::load(assets.registry, root)?,
|
||||
)
|
||||
} else {
|
||||
let json: serde_json::Value = serde_json::from_reader(std::io::BufReader::new(
|
||||
std::fs::File::open(root.join("block-behaviours.json"))?,
|
||||
))?;
|
||||
BlockFacts::load_with_behaviours(root, assets.registry, &json)?
|
||||
};
|
||||
println!("phase_ms={:.3}", started.elapsed().as_secs_f64() * 1000.0);
|
||||
println!("total_ms={:.3}", total.elapsed().as_secs_f64() * 1000.0);
|
||||
println!("states={}", facts.states.len());
|
||||
std::hint::black_box((facts, behaviours));
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
//! (`gameplay-facts.json`), indexed by registry state ID.
|
||||
mod decode;
|
||||
use crate::Result;
|
||||
use horizon_world::registry::{BlockRegistry, BlockStateId};
|
||||
use horizon_world::{
|
||||
level::{Behaviours, GameplayStateSource},
|
||||
registry::{BlockRegistry, BlockStateId},
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -113,6 +116,39 @@ pub struct BlockFacts {
|
||||
}
|
||||
impl BlockFacts {
|
||||
pub fn load(root: &Path, registry: &BlockRegistry) -> Result<Self> {
|
||||
let behaviours: Value = serde_json::from_reader(BufReader::new(File::open(
|
||||
root.join("block-behaviours.json"),
|
||||
)?))?;
|
||||
Self::load_with_behaviour_json(root, registry, &behaviours)
|
||||
}
|
||||
|
||||
pub fn load_with_behaviour_json(
|
||||
root: &Path,
|
||||
registry: &BlockRegistry,
|
||||
behaviours: &Value,
|
||||
) -> Result<Self> {
|
||||
Ok(Self::load_with_callback(root, registry, behaviours, |_| Ok(()))?.0)
|
||||
}
|
||||
|
||||
/// Build both tables from the same behaviour document and gameplay decoder.
|
||||
pub fn load_with_behaviours(
|
||||
root: &Path,
|
||||
registry: &BlockRegistry,
|
||||
behaviours: &Value,
|
||||
) -> Result<(Self, Behaviours)> {
|
||||
Self::load_with_callback(root, registry, behaviours, |entries| {
|
||||
Ok(Behaviours::load_with_gameplay(
|
||||
registry, root, behaviours, entries,
|
||||
)?)
|
||||
})
|
||||
}
|
||||
|
||||
fn load_with_callback<T>(
|
||||
root: &Path,
|
||||
registry: &BlockRegistry,
|
||||
behaviours: &Value,
|
||||
callback: impl FnOnce(&[decode::Entry<'_>]) -> Result<T>,
|
||||
) -> Result<(Self, T)> {
|
||||
let bytes = fs::read(root.join("gameplay-facts.json"))?;
|
||||
serde_json::from_slice::<decode::Validate>(&bytes)?;
|
||||
let raw: decode::Gameplay<'_> = serde_json::from_slice(&bytes)?;
|
||||
@@ -125,9 +161,6 @@ impl BlockFacts {
|
||||
return Err("gameplay facts do not match the block registry".into());
|
||||
}
|
||||
// Class chains and per-state behaviour flags exported from the actual block objects.
|
||||
let behaviours: Value = serde_json::from_reader(BufReader::new(File::open(
|
||||
root.join("block-behaviours.json"),
|
||||
)?))?;
|
||||
let behaviour_states = behaviours["states"]
|
||||
.as_array()
|
||||
.filter(|s| s.len() == registry.len())
|
||||
@@ -221,13 +254,13 @@ impl BlockFacts {
|
||||
outline,
|
||||
outline_edges,
|
||||
name: intern(&state.name),
|
||||
emission: entry.emission as u8,
|
||||
dampening: entry.dampening as u8,
|
||||
emission: entry.emission.as_f64().unwrap_or(0.0) as u8,
|
||||
dampening: entry.dampening.as_f64().unwrap_or(0.0) as u8,
|
||||
skylight_down: entry.skylight_down,
|
||||
shape_occlusion: entry.shape_occlusion,
|
||||
can_occlude: entry.can_occlude,
|
||||
solid_render: entry.solid_render,
|
||||
hardness: entry.hardness as f32,
|
||||
hardness: entry.hardness.as_f64().unwrap_or(0.0) as f32,
|
||||
requires_tool: entry.requires_tool,
|
||||
replaceable: entry.replaceable,
|
||||
random_ticks: entry.random_ticks,
|
||||
@@ -264,12 +297,16 @@ impl BlockFacts {
|
||||
.ok_or("missing block behaviour class chain")?,
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
states,
|
||||
air: registry
|
||||
.default_state("minecraft:air")
|
||||
.ok_or("missing air")?,
|
||||
})
|
||||
let extra = callback(entries)?;
|
||||
Ok((
|
||||
Self {
|
||||
states,
|
||||
air: registry
|
||||
.default_state("minecraft:air")
|
||||
.ok_or("missing air")?,
|
||||
},
|
||||
extra,
|
||||
))
|
||||
}
|
||||
pub fn get(&self, id: BlockStateId) -> &StateFacts {
|
||||
&self.states[id.0 as usize]
|
||||
@@ -281,3 +318,56 @@ impl BlockFacts {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl GameplayStateSource for decode::Entry<'_> {
|
||||
fn flag(&self, name: &str) -> bool {
|
||||
match name {
|
||||
"replaceable" => self.replaceable,
|
||||
"random_ticks" => self.random_ticks,
|
||||
"solid_render" => self.solid_render,
|
||||
"liquid" => self.liquid,
|
||||
"block_entity" => self.block_entity,
|
||||
"ignited_by_lava" => self.ignited_by_lava,
|
||||
"can_occlude" => self.can_occlude,
|
||||
"full_collision" => self.full_collision,
|
||||
"skylight_down" => self.skylight_down,
|
||||
"requires_tool" => self.requires_tool,
|
||||
"shape_occlusion" => self.shape_occlusion,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
fn unsigned(&self, name: &str) -> u64 {
|
||||
match name {
|
||||
"emission" => self.emission.as_u64().unwrap_or(0),
|
||||
"dampening" => self.dampening.as_u64().unwrap_or(0),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
fn number(&self, name: &str) -> f64 {
|
||||
match name {
|
||||
"hardness" => self.hardness.as_f64().unwrap_or(0.0),
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
fn text(&self, name: &str) -> &str {
|
||||
match name {
|
||||
"push" => self.push.as_deref().unwrap_or(""),
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
fn sound_number(&self, name: &str) -> f64 {
|
||||
match name {
|
||||
"volume" => self.sound.volume,
|
||||
"pitch" => self.sound.pitch,
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
fn sound_id(&self, name: &str) -> &str {
|
||||
match name {
|
||||
"place" => self.sound.place.id.as_deref().unwrap_or(""),
|
||||
"break" => self.sound.r#break.id.as_deref().unwrap_or(""),
|
||||
"fall" => self.sound.fall.id.as_deref().unwrap_or(""),
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,17 +155,20 @@ object!(Sound<'a> {
|
||||
});
|
||||
object!(Entry<'a> {
|
||||
id: Option<u64> = None => value(unsigned),
|
||||
emission: f64 = 0.0 => value(number),
|
||||
dampening: f64 = 0.0 => value(number),
|
||||
emission: Value = Value::Null => typed,
|
||||
dampening: Value = Value::Null => typed,
|
||||
skylight_down: bool = false => value(flag),
|
||||
shape_occlusion: bool = false => value(flag),
|
||||
can_occlude: bool = false => value(flag),
|
||||
solid_render: bool = false => value(flag),
|
||||
hardness: f64 = 0.0 => value(number),
|
||||
hardness: Value = Value::Null => typed,
|
||||
requires_tool: bool = false => value(flag),
|
||||
replaceable: bool = false => value(flag),
|
||||
random_ticks: bool = false => value(flag),
|
||||
block_entity: bool = false => value(flag),
|
||||
liquid: bool = false => value(flag),
|
||||
ignited_by_lava: bool = false => value(flag),
|
||||
push: Option<String> = None => value(text),
|
||||
render_shape: Option<String> = None => value(text),
|
||||
full_collision: bool = false => value(flag),
|
||||
friction: f64 = 0.0 => value(number),
|
||||
|
||||
@@ -89,9 +89,9 @@ fn typed_fields_keep_defaults_duplicates_and_wrong_type_behavior() -> crate::Res
|
||||
assert_eq!(entries[1].id, None);
|
||||
let e = &entries[2];
|
||||
assert_eq!(e.id, Some(2));
|
||||
assert_eq!(e.emission, 4.5);
|
||||
assert_eq!(e.dampening, 0.0);
|
||||
assert_eq!(e.hardness.to_bits(), (-0.0f64).to_bits());
|
||||
assert_eq!(e.emission.as_f64(), Some(4.5));
|
||||
assert_eq!(e.dampening.as_f64().unwrap_or(0.0), 0.0);
|
||||
assert_eq!(e.hardness.as_f64().unwrap().to_bits(), (-0.0f64).to_bits());
|
||||
assert!(e.skylight_down && e.outline_offset.unwrap_or(e.offset));
|
||||
assert_eq!(e.item, None);
|
||||
assert_eq!(e.render_shape, None);
|
||||
|
||||
@@ -10169,7 +10169,11 @@ pub fn run() -> Result<()> {
|
||||
let root = options.resources.clone();
|
||||
let assets = Arc::new(assets);
|
||||
log::info!("load assets: {:.2}s", started.elapsed().as_secs_f32());
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let behaviour_json: serde_json::Value = serde_json::from_reader(std::io::BufReader::new(
|
||||
std::fs::File::open(root.join("block-behaviours.json"))?,
|
||||
))?;
|
||||
let (facts, behaviours) =
|
||||
BlockFacts::load_with_behaviours(&root, assets.registry, &behaviour_json)?;
|
||||
let mob_data = horizon_client::mobs::MobData::load(&root, &facts);
|
||||
horizon_client::mobs::trial_spawner::load_registry(&root);
|
||||
log::info!("load facts: {:.2}s", started.elapsed().as_secs_f32());
|
||||
@@ -10236,7 +10240,7 @@ pub fn run() -> Result<()> {
|
||||
ambient: horizon_client::ambience::AmbientTables::load(&root),
|
||||
nether_environment: Environment::load_dimension(&root, "the_nether", false)?,
|
||||
end_environment: Environment::load_dimension(&root, "the_end", false)?,
|
||||
behaviours: Behaviours::load(registry_owner.registry, &root)?,
|
||||
behaviours,
|
||||
mob_types: MobTypes::load(&root)?
|
||||
.types
|
||||
.into_iter()
|
||||
|
||||
@@ -49,8 +49,13 @@ fn tags_merge_without_repeats() {
|
||||
}
|
||||
|
||||
fn prepared() -> Option<PathBuf> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../.local/horizon-client/26.3");
|
||||
root.join("gameplay-facts.json").exists().then_some(root)
|
||||
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
[
|
||||
manifest.join("../../.local/horizon-client/26.3"),
|
||||
manifest.join("../../../../.local/horizon-client/26.3"),
|
||||
]
|
||||
.into_iter()
|
||||
.find(|root| root.join("gameplay-facts.json").exists())
|
||||
}
|
||||
|
||||
/// A content mod folder: a full block, a door and an item-less glass from
|
||||
@@ -137,6 +142,28 @@ fn content_root_loads_with_every_table() {
|
||||
|
||||
let old_facts = BlockFacts::load(&base, old).unwrap();
|
||||
let facts = BlockFacts::load(&root, new).unwrap();
|
||||
let shared_json: serde_json::Value = serde_json::from_reader(std::io::BufReader::new(
|
||||
fs::File::open(root.join("block-behaviours.json")).unwrap(),
|
||||
))
|
||||
.unwrap();
|
||||
let (shared_facts, shared_behaviours) =
|
||||
BlockFacts::load_with_behaviours(&root, new, &shared_json).unwrap();
|
||||
let independent_behaviours = horizon_world::level::Behaviours::load(new, &root).unwrap();
|
||||
assert_eq!(shared_facts.states.len(), facts.states.len());
|
||||
for state in [ruby, ruby_door, BlockStateId(n + 65)] {
|
||||
assert_eq!(shared_facts.get(state).hardness, facts.get(state).hardness);
|
||||
assert_eq!(shared_facts.get(state).emission, facts.get(state).emission);
|
||||
assert_eq!(shared_facts.get(state).classes, facts.get(state).classes);
|
||||
assert_eq!(
|
||||
shared_behaviours.facts(state).flags,
|
||||
independent_behaviours.facts(state).flags,
|
||||
);
|
||||
assert_eq!(
|
||||
shared_behaviours.data(state).sounds.place,
|
||||
independent_behaviours.data(state).sounds.place,
|
||||
);
|
||||
}
|
||||
|
||||
let (a, b) = (old_facts.get(iron), facts.get(ruby));
|
||||
assert_eq!((b.hardness, b.emission, b.friction), (4.0, 3, 0.8));
|
||||
assert_eq!(b.explosion_resistance, a.explosion_resistance);
|
||||
@@ -216,3 +243,48 @@ fn no_content_keeps_the_base_root() {
|
||||
}
|
||||
|
||||
use horizon_world::registry::BlockStateId;
|
||||
|
||||
#[test]
|
||||
fn shared_behaviour_document_matches_independent_loaders() {
|
||||
let Some(root) = prepared() else { return };
|
||||
let assets = Assets::load(&root).expect("assets");
|
||||
let doc: serde_json::Value = serde_json::from_reader(std::io::BufReader::new(
|
||||
std::fs::File::open(root.join("block-behaviours.json")).unwrap(),
|
||||
))
|
||||
.unwrap();
|
||||
let original = BlockFacts::load(&root, assets.registry).unwrap();
|
||||
let (shared, b) = BlockFacts::load_with_behaviours(&root, assets.registry, &doc).unwrap();
|
||||
assert_eq!(original.states.len(), shared.states.len());
|
||||
for (a, b) in original.states.iter().zip(&shared.states) {
|
||||
assert_eq!(a.classes, b.classes);
|
||||
assert_eq!(a.legacy_solid, b.legacy_solid);
|
||||
assert_eq!(a.sturdy_full, b.sturdy_full);
|
||||
assert_eq!(a.emission, b.emission);
|
||||
assert_eq!(a.dampening, b.dampening);
|
||||
assert_eq!(a.hardness, b.hardness);
|
||||
assert_eq!(a.sounds.volume, b.sounds.volume);
|
||||
assert_eq!(a.sounds.pitch, b.sounds.pitch);
|
||||
}
|
||||
let a = horizon_world::level::Behaviours::load(assets.registry, &root).unwrap();
|
||||
for id in 0..assets.registry.len() {
|
||||
let state = horizon_world::registry::BlockStateId(id as u32);
|
||||
let (a_state, b_state) = (a.facts(state), b.facts(state));
|
||||
assert_eq!(a_state.flags, b_state.flags);
|
||||
assert_eq!(a_state.sturdy, b_state.sturdy);
|
||||
assert_eq!(a_state.weak, b_state.weak);
|
||||
assert_eq!(a_state.strong, b_state.strong);
|
||||
assert_eq!(a_state.fluid, b_state.fluid);
|
||||
assert_eq!(a_state.emission, b_state.emission);
|
||||
assert_eq!(a_state.dampening, b_state.dampening);
|
||||
assert_eq!(a_state.hardness, b_state.hardness);
|
||||
assert_eq!(a_state.push, b_state.push);
|
||||
assert_eq!(a_state.instrument, b_state.instrument);
|
||||
assert_eq!(a_state.shape, b_state.shape);
|
||||
let (a_sound, b_sound) = (&a.data(state).sounds, &b.data(state).sounds);
|
||||
assert_eq!(a_sound.volume, b_sound.volume);
|
||||
assert_eq!(a_sound.pitch, b_sound.pitch);
|
||||
assert_eq!(a_sound.place, b_sound.place);
|
||||
assert_eq!(a_sound.break_, b_sound.break_);
|
||||
assert_eq!(a_sound.fall, b_sound.fall);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Behaviour tables built once from the pinned client resources.
|
||||
use super::{
|
||||
Behaviours,
|
||||
Behaviours, GameplayStateSource,
|
||||
classes::{Class, ClassSet, Interface},
|
||||
states::{BlockId, PropKey, StateTable},
|
||||
};
|
||||
@@ -653,20 +653,37 @@ pub fn load_registry(root: &Path) -> io::Result<BlockRegistry> {
|
||||
}
|
||||
|
||||
pub(super) fn load(registry: &BlockRegistry, root: &Path) -> io::Result<Behaviours> {
|
||||
let behaviour = read_json(&root.join("block-behaviours.json"))?;
|
||||
load_with_behaviour_json(registry, root, &behaviour)
|
||||
}
|
||||
|
||||
pub(super) fn load_with_behaviour_json(
|
||||
registry: &BlockRegistry,
|
||||
root: &Path,
|
||||
behaviour: &Value,
|
||||
) -> io::Result<Behaviours> {
|
||||
let gameplay = read_json(&root.join("gameplay-facts.json"))?;
|
||||
let gameplay_states = gameplay["states"]
|
||||
.as_array()
|
||||
.ok_or_else(|| fail("missing gameplay states"))?;
|
||||
load_with_gameplay(registry, root, behaviour, gameplay_states)
|
||||
}
|
||||
|
||||
pub(super) fn load_with_gameplay<G: GameplayStateSource>(
|
||||
registry: &BlockRegistry,
|
||||
root: &Path,
|
||||
behaviour: &Value,
|
||||
gameplay_states: &[G],
|
||||
) -> io::Result<Behaviours> {
|
||||
let mut table = StateTable::new(registry)?;
|
||||
let props = Props::resolve(&mut table);
|
||||
let named = Named::resolve(&table)?;
|
||||
let behaviour = read_json(&root.join("block-behaviours.json"))?;
|
||||
let gameplay = read_json(&root.join("gameplay-facts.json"))?;
|
||||
let blocks_json = behaviour["blocks"]
|
||||
.as_array()
|
||||
.ok_or_else(|| fail("missing behaviour blocks"))?;
|
||||
let states_json = behaviour["states"]
|
||||
.as_array()
|
||||
.ok_or_else(|| fail("missing behaviour states"))?;
|
||||
let gameplay_states = gameplay["states"]
|
||||
.as_array()
|
||||
.ok_or_else(|| fail("missing gameplay states"))?;
|
||||
if blocks_json.len() != table.block_count()
|
||||
|| states_json.len() != table.state_count()
|
||||
|| gameplay_states.len() != table.state_count()
|
||||
@@ -798,7 +815,7 @@ pub(super) fn load(registry: &BlockRegistry, root: &Path) -> io::Result<Behaviou
|
||||
.as_array()
|
||||
.ok_or_else(|| fail("invalid behaviour state row"))?;
|
||||
let int = |i: usize| row.get(i).and_then(Value::as_i64).unwrap_or(0);
|
||||
let gb = |key: &str| game.get(key).and_then(Value::as_bool).unwrap_or(false);
|
||||
let gb = |key: &str| game.flag(key);
|
||||
let mut flags = int(1) as u32 & 0x7f;
|
||||
for (key, bit) in [
|
||||
("replaceable", state_flags::REPLACEABLE),
|
||||
@@ -832,14 +849,10 @@ pub(super) fn load(registry: &BlockRegistry, root: &Path) -> io::Result<Behaviou
|
||||
weak: int(5) as u32,
|
||||
strong: int(6) as u32,
|
||||
fluid,
|
||||
emission: game.get("emission").and_then(Value::as_u64).unwrap_or(0) as u8,
|
||||
dampening: game.get("dampening").and_then(Value::as_u64).unwrap_or(0) as u8,
|
||||
hardness: game.get("hardness").and_then(Value::as_f64).unwrap_or(0.0) as f32,
|
||||
push: match game
|
||||
.get("push")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("PUSH_PULL")
|
||||
{
|
||||
emission: game.unsigned("emission") as u8,
|
||||
dampening: game.unsigned("dampening") as u8,
|
||||
hardness: game.number("hardness") as f32,
|
||||
push: match game.text("push") {
|
||||
"PUSH" => PushReaction::Push,
|
||||
"POPPED" => PushReaction::Popped,
|
||||
"IMMOVEABLE" => PushReaction::Immoveable,
|
||||
@@ -951,23 +964,13 @@ pub(super) fn load(registry: &BlockRegistry, root: &Path) -> io::Result<Behaviou
|
||||
_ => Direction::Up,
|
||||
};
|
||||
let default = table.default_state(block);
|
||||
let sound = &gameplay_states[default.0 as usize]["sound"];
|
||||
let sound_id = |key: &str, strings: &mut HashMap<String, Arc<str>>| {
|
||||
interned(
|
||||
strings,
|
||||
sound
|
||||
.get(key)
|
||||
.and_then(|s| s.get("id"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or(""),
|
||||
)
|
||||
};
|
||||
let sound = &gameplay_states[default.0 as usize];
|
||||
let sounds = Arc::new(Sounds {
|
||||
volume: sound.get("volume").and_then(Value::as_f64).unwrap_or(1.0) as f32,
|
||||
pitch: sound.get("pitch").and_then(Value::as_f64).unwrap_or(1.0) as f32,
|
||||
place: sound_id("place", &mut strings),
|
||||
break_: sound_id("break", &mut strings),
|
||||
fall: sound_id("fall", &mut strings),
|
||||
volume: sound.sound_number("volume") as f32,
|
||||
pitch: sound.sound_number("pitch") as f32,
|
||||
place: interned(&mut strings, sound.sound_id("place")),
|
||||
break_: interned(&mut strings, sound.sound_id("break")),
|
||||
fall: interned(&mut strings, sound.sound_id("fall")),
|
||||
});
|
||||
blocks.push(BlockData {
|
||||
class,
|
||||
@@ -1145,3 +1148,31 @@ fn instrument_index(name: &str) -> u8 {
|
||||
.position(|n| n.eq_ignore_ascii_case(name))
|
||||
.map_or(0, |i| i as u8)
|
||||
}
|
||||
|
||||
impl GameplayStateSource for Value {
|
||||
fn flag(&self, name: &str) -> bool {
|
||||
self.get(name).and_then(Value::as_bool).unwrap_or(false)
|
||||
}
|
||||
fn unsigned(&self, name: &str) -> u64 {
|
||||
self.get(name).and_then(Value::as_u64).unwrap_or(0)
|
||||
}
|
||||
fn number(&self, name: &str) -> f64 {
|
||||
self.get(name).and_then(Value::as_f64).unwrap_or(0.0)
|
||||
}
|
||||
fn text(&self, name: &str) -> &str {
|
||||
self.get(name).and_then(Value::as_str).unwrap_or("")
|
||||
}
|
||||
fn sound_number(&self, name: &str) -> f64 {
|
||||
self.get("sound")
|
||||
.and_then(|s| s.get(name))
|
||||
.and_then(Value::as_f64)
|
||||
.unwrap_or(1.0)
|
||||
}
|
||||
fn sound_id(&self, name: &str) -> &str {
|
||||
self.get("sound")
|
||||
.and_then(|s| s.get(name))
|
||||
.and_then(|s| s.get("id"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("")
|
||||
}
|
||||
}
|
||||
@@ -431,6 +431,17 @@ pub fn raw_brightness<L: LevelAccess + ?Sized>(level: &L, pos: BlockPos, sky_dar
|
||||
(level.sky_light(pos) - sky_darken).max(level.block_light(pos))
|
||||
}
|
||||
|
||||
/// Access only the gameplay fields needed by level behaviour tables.
|
||||
/// Both the JSON fallback and the client's typed decoder implement this.
|
||||
pub trait GameplayStateSource {
|
||||
fn flag(&self, name: &str) -> bool;
|
||||
fn unsigned(&self, name: &str) -> u64;
|
||||
fn number(&self, name: &str) -> f64;
|
||||
fn text(&self, name: &str) -> &str;
|
||||
fn sound_number(&self, name: &str) -> f64;
|
||||
fn sound_id(&self, name: &str) -> &str;
|
||||
}
|
||||
|
||||
/// Immutable vanilla block-behaviour tables. Build once with [`Behaviours::load`].
|
||||
pub struct Behaviours {
|
||||
pub table: StateTable,
|
||||
@@ -456,6 +467,23 @@ impl Behaviours {
|
||||
pub fn load(registry: &BlockRegistry, resources_dir: &Path) -> io::Result<Self> {
|
||||
data::load(registry, resources_dir)
|
||||
}
|
||||
/// Reuse the client's typed gameplay projection while it is borrowed from the JSON bytes.
|
||||
pub fn load_with_gameplay<G: GameplayStateSource>(
|
||||
registry: &BlockRegistry,
|
||||
resources_dir: &Path,
|
||||
behaviour: &serde_json::Value,
|
||||
gameplay: &[G],
|
||||
) -> io::Result<Self> {
|
||||
data::load_with_gameplay(registry, resources_dir, behaviour, gameplay)
|
||||
}
|
||||
|
||||
pub fn load_with_behaviour_json(
|
||||
registry: &BlockRegistry,
|
||||
resources_dir: &Path,
|
||||
behaviour: &serde_json::Value,
|
||||
) -> io::Result<Self> {
|
||||
data::load_with_behaviour_json(registry, resources_dir, behaviour)
|
||||
}
|
||||
pub fn facts(&self, state: BlockStateId) -> &StateFacts {
|
||||
&self.facts[state.0 as usize]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare baseline and shared JSON parsing in the client startup table phase.
|
||||
|
||||
Build first with `cargo build --release -p horizon-client --example shared_json_startup_profile`.
|
||||
This covers Assets::load plus BlockFacts::load and Behaviours::load, not full game
|
||||
startup, rendering, resource pack setup, or first frame. Peak RSS includes Assets.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import statistics
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run(binary, resources, mode):
|
||||
with tempfile.TemporaryFile(mode="w+t") as output:
|
||||
proc = subprocess.Popen([str(binary), str(resources), mode], stdout=output, stderr=subprocess.PIPE)
|
||||
_, status, usage = os.wait4(proc.pid, 0)
|
||||
proc.returncode = os.waitstatus_to_exitcode(status)
|
||||
_, stderr = proc.communicate()
|
||||
if os.waitstatus_to_exitcode(status):
|
||||
raise RuntimeError(f"{mode}: {stderr.decode(errors='replace')}")
|
||||
output.seek(0)
|
||||
fields = dict(line.rstrip().split("=", 1) for line in output if "=" in line)
|
||||
return {
|
||||
"phase_ms": float(fields["phase_ms"]),
|
||||
"total_ms": float(fields["total_ms"]),
|
||||
"cpu_ms": (usage.ru_utime + usage.ru_stime) * 1000.0,
|
||||
"peak_rss_kib": usage.ru_maxrss,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--binary", type=Path, default=Path("target/release/examples/shared_json_startup_profile"))
|
||||
parser.add_argument("--resources", type=Path, default=Path(".local/horizon-client/26.3"))
|
||||
parser.add_argument("--repeats", type=int, default=5)
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
if args.repeats < 1:
|
||||
parser.error("--repeats must be positive")
|
||||
binary, resources = args.binary.resolve(), args.resources.resolve()
|
||||
if not binary.is_file() or not resources.is_dir():
|
||||
parser.error("binary or resources missing; build the example and prepare resources first")
|
||||
samples = {"baseline": [], "shared": []}
|
||||
for i in range(args.repeats):
|
||||
for mode in (("baseline", "shared") if i % 2 == 0 else ("shared", "baseline")):
|
||||
samples[mode].append(run(binary, resources, mode))
|
||||
result = {
|
||||
"scope": "Assets + block facts + behaviours loader only; excludes full startup",
|
||||
"resources": str(resources),
|
||||
"repeats": args.repeats,
|
||||
"metrics": {mode: {key: {"median": round(statistics.median(row[key] for row in rows), 3),
|
||||
"samples": [round(row[key], 3) for row in rows]}
|
||||
for key in rows[0]} for mode, rows in samples.items()},
|
||||
}
|
||||
data = json.dumps(result, indent=2)
|
||||
print(data)
|
||||
if args.output:
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(data + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user